blob: 08698a21ed531f50b4de4221eebaa04f8b5442c1 [file] [log] [blame]
/*-
* Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle Berkeley
* DB Java Edition made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle Berkeley DB Java Edition for a copy of the
* license and additional information.
*/
package com.sleepycat.persist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.sleepycat.bind.EntityBinding;
import com.sleepycat.bind.EntryBinding;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
/* <!-- begin JE only --> */
import com.sleepycat.je.EnvironmentFailureException;
/* <!-- end JE only --> */
import com.sleepycat.je.JoinCursor;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationFailureException;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;
/**
* Performs an equality join on two or more secondary keys.
*
* <p>{@code EntityJoin} objects are thread-safe. Multiple threads may safely
* call the methods of a shared {@code EntityJoin} object.</p>
*
* <p>An equality join is a match on all entities in a given primary index that
* have two or more specific secondary key values. Note that key ranges may
* not be matched by an equality join, only exact keys are matched.</p>
*
* <p>For example:</p>
* <pre class="code">
* // Index declarations -- see <a href="package-summary.html#example">package summary example</a>.
* //
* {@literal PrimaryIndex<String, Person> personBySsn;}
* {@literal SecondaryIndex<String, String, Person> personByParentSsn;}
* {@literal SecondaryIndex<Long, String, Person> personByEmployerIds;}
* Employer employer = ...;
*
* // Match on all Person objects having parentSsn "111-11-1111" and also
* // containing an employerId of employer.id. In other words, match on all
* // of Bob's children that work for a given employer.
* //
* {@literal EntityJoin<String, Person> join = new EntityJoin(personBySsn);}
* join.addCondition(personByParentSsn, "111-11-1111");
* join.addCondition(personByEmployerIds, employer.id);
*
* // Perform the join operation by traversing the results with a cursor.
* //
* {@literal ForwardCursor<Person> results = join.entities();}
* try {
* for (Person person : results) {
* System.out.println(person.ssn + ' ' + person.name);
* }
* } finally {
* results.close();
* }</pre>
*
* @author Mark Hayes
*/
public class EntityJoin<PK, E> {
private PrimaryIndex<PK, E> primary;
private List<Condition> conditions;
/**
* Creates a join object for a given primary index.
*
* @param index the primary index on which the join will operate.
*/
public EntityJoin(PrimaryIndex<PK, E> index) {
primary = index;
conditions = new ArrayList<Condition>();
}
/**
* Adds a secondary key condition to the equality join. Only entities
* having the given key value in the given secondary index will be returned
* by the join operation.
*
* @param index the secondary index containing the given key value.
*
* @param key the key value to match during the join.
*
* @param <SK> the secondary key class.
*/
public <SK> void addCondition(SecondaryIndex<SK, PK, E> index, SK key) {
/* Make key entry. */
DatabaseEntry keyEntry = new DatabaseEntry();
index.getKeyBinding().objectToEntry(key, keyEntry);
/* Use keys database if available. */
Database db = index.getKeysDatabase();
if (db == null) {
db = index.getDatabase();
}
/* Add condition. */
conditions.add(new Condition(db, keyEntry));
}
/**
* Opens a cursor that returns the entities qualifying for the join. The
* join operation is performed as the returned cursor is accessed.
*
* <p>The operations performed with the cursor will not be transaction
* protected, and {@link CursorConfig#DEFAULT} is used implicitly.</p>
*
* @return the cursor.
*
* <!-- begin JE only -->
* @throws OperationFailureException if one of the <a
* href="../je/OperationFailureException.html#readFailures">Read Operation
* Failures</a> occurs.
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs.
* <!-- end JE only -->
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor<E> entities()
throws DatabaseException {
return entities(null, null);
}
/**
* Opens a cursor that returns the entities qualifying for the join. The
* join operation is performed as the returned cursor is accessed.
*
* @param txn the transaction used to protect all operations performed with
* the cursor, or null if the operations should not be transaction
* protected. If the store is non-transactional, null must be specified.
* For a transactional store the transaction is optional for read-only
* access and required for read-write access.
*
* @param config the cursor configuration that determines the default lock
* mode used for all cursor operations, or null to implicitly use {@link
* CursorConfig#DEFAULT}.
*
* @return the cursor.
*
* <!-- begin JE only -->
* @throws OperationFailureException if one of the <a
* href="../je/OperationFailureException.html#readFailures">Read Operation
* Failures</a> occurs.
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs.
* <!-- end JE only -->
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor<E> entities(Transaction txn, CursorConfig config)
throws DatabaseException {
return new JoinForwardCursor<E>(txn, config, false);
}
/**
* Opens a cursor that returns the primary keys of entities qualifying for
* the join. The join operation is performed as the returned cursor is
* accessed.
*
* <p>The operations performed with the cursor will not be transaction
* protected, and {@link CursorConfig#DEFAULT} is used implicitly.</p>
*
* @return the cursor.
*
* <!-- begin JE only -->
* @throws OperationFailureException if one of the <a
* href="../je/OperationFailureException.html#readFailures">Read Operation
* Failures</a> occurs.
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs.
* <!-- end JE only -->
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor<PK> keys()
throws DatabaseException {
return keys(null, null);
}
/**
* Opens a cursor that returns the primary keys of entities qualifying for
* the join. The join operation is performed as the returned cursor is
* accessed.
*
* @param txn the transaction used to protect all operations performed with
* the cursor, or null if the operations should not be transaction
* protected. If the store is non-transactional, null must be specified.
* For a transactional store the transaction is optional for read-only
* access and required for read-write access.
*
* @param config the cursor configuration that determines the default lock
* mode used for all cursor operations, or null to implicitly use {@link
* CursorConfig#DEFAULT}.
*
* @return the cursor.
*
* <!-- begin JE only -->
* @throws OperationFailureException if one of the <a
* href="../je/OperationFailureException.html#readFailures">Read Operation
* Failures</a> occurs.
*
* @throws EnvironmentFailureException if an unexpected, internal or
* environment-wide failure occurs.
* <!-- end JE only -->
*
* @throws IllegalStateException if less than two conditions were added.
*
* @throws DatabaseException the base class for all BDB exceptions.
*/
public ForwardCursor<PK> keys(Transaction txn, CursorConfig config)
throws DatabaseException {
return new JoinForwardCursor<PK>(txn, config, true);
}
private static class Condition {
private Database db;
private DatabaseEntry key;
Condition(Database db, DatabaseEntry key) {
this.db = db;
this.key = key;
}
Cursor openCursor(Transaction txn, CursorConfig config)
throws DatabaseException {
OperationStatus status;
Cursor cursor = db.openCursor(txn, config);
try {
DatabaseEntry data = BasicIndex.NO_RETURN_ENTRY;
status = cursor.getSearchKey(key, data, null);
} catch (DatabaseException e) {
try {
cursor.close();
} catch (DatabaseException ignored) {}
throw e;
}
if (status == OperationStatus.SUCCESS) {
return cursor;
} else {
cursor.close();
return null;
}
}
}
private class JoinForwardCursor<V> implements ForwardCursor<V> {
private Cursor[] cursors;
private JoinCursor joinCursor;
private boolean doKeys;
JoinForwardCursor(Transaction txn, CursorConfig config, boolean doKeys)
throws DatabaseException {
this.doKeys = doKeys;
try {
cursors = new Cursor[conditions.size()];
for (int i = 0; i < cursors.length; i += 1) {
Condition cond = conditions.get(i);
Cursor cursor = cond.openCursor(txn, config);
if (cursor == null) {
/* Leave joinCursor null. */
doClose(null);
return;
}
cursors[i] = cursor;
}
joinCursor = primary.getDatabase().join(cursors, null);
} catch (DatabaseException e) {
/* doClose will throw e. */
doClose(e);
}
}
public V next()
throws DatabaseException {
return next(null);
}
public V next(LockMode lockMode)
throws DatabaseException {
if (joinCursor == null) {
return null;
}
if (doKeys) {
DatabaseEntry key = new DatabaseEntry();
OperationStatus status = joinCursor.getNext(key, lockMode);
if (status == OperationStatus.SUCCESS) {
EntryBinding binding = primary.getKeyBinding();
return (V) binding.entryToObject(key);
}
} else {
DatabaseEntry key = new DatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
OperationStatus status =
joinCursor.getNext(key, data, lockMode);
if (status == OperationStatus.SUCCESS) {
EntityBinding binding = primary.getEntityBinding();
return (V) binding.entryToObject(key, data);
}
}
return null;
}
public Iterator<V> iterator() {
return iterator(null);
}
public Iterator<V> iterator(LockMode lockMode) {
return new BasicIterator<V>(this, lockMode);
}
public void close()
throws DatabaseException {
doClose(null);
}
private void doClose(DatabaseException firstException)
throws DatabaseException {
if (joinCursor != null) {
try {
joinCursor.close();
joinCursor = null;
} catch (DatabaseException e) {
if (firstException == null) {
firstException = e;
}
}
}
for (int i = 0; i < cursors.length; i += 1) {
Cursor cursor = cursors[i];
if (cursor != null) {
try {
cursor.close();
cursors[i] = null;
} catch (DatabaseException e) {
if (firstException == null) {
firstException = e;
}
}
}
}
if (firstException != null) {
throw firstException;
}
}
}
}