/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2002, 2010 Oracle and/or its affiliates. All rights reserved. * */ 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.db.Cursor; import com.sleepycat.db.CursorConfig; import com.sleepycat.db.Database; import com.sleepycat.db.DatabaseEntry; import com.sleepycat.db.DatabaseException; import com.sleepycat.db.JoinCursor; import com.sleepycat.db.LockMode; import com.sleepycat.db.OperationStatus; import com.sleepycat.db.Transaction; /** * Performs an equality join on two or more secondary keys. * *

{@code EntityJoin} objects are thread-safe. Multiple threads may safely * call the methods of a shared {@code EntityJoin} object.

* *

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.

* *

For example:

*
 *  // Index declarations -- see {@link package summary example}.
 *  //
 *  {@literal PrimaryIndex personBySsn;}
 *  {@literal SecondaryIndex personByParentSsn;}
 *  {@literal SecondaryIndex 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 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 results = join.entities();}
 *  try {
 *      for (Person person : results) {
 *          System.out.println(person.ssn + ' ' + person.name);
 *      }
 *  } finally {
 *      results.close();
 *  }
* * @author Mark Hayes */ public class EntityJoin { private PrimaryIndex primary; private List conditions; /** * Creates a join object for a given primary index. * * @param index the primary index on which the join will operate. */ public EntityJoin(PrimaryIndex index) { primary = index; conditions = new ArrayList(); } /** * 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. */ public void addCondition(SecondaryIndex 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. * *

The operations performed with the cursor will not be transaction * protected, and {@link CursorConfig#DEFAULT} is used implicitly.

* * @return the cursor. * * * @throws IllegalStateException if less than two conditions were added. * * @throws DatabaseException the base class for all BDB exceptions. */ public ForwardCursor 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. * * * @throws IllegalStateException if less than two conditions were added. * * @throws DatabaseException the base class for all BDB exceptions. */ public ForwardCursor entities(Transaction txn, CursorConfig config) throws DatabaseException { return new JoinForwardCursor(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. * *

The operations performed with the cursor will not be transaction * protected, and {@link CursorConfig#DEFAULT} is used implicitly.

* * @return the cursor. * * * @throws IllegalStateException if less than two conditions were added. * * @throws DatabaseException the base class for all BDB exceptions. */ public ForwardCursor 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. * * * @throws IllegalStateException if less than two conditions were added. * * @throws DatabaseException the base class for all BDB exceptions. */ public ForwardCursor keys(Transaction txn, CursorConfig config) throws DatabaseException { return new JoinForwardCursor(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 implements ForwardCursor { 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 iterator() { return iterator(null); } public Iterator iterator(LockMode lockMode) { return new BasicIterator(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; } } } }