1/*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2002,2008 Oracle. All rights reserved. 5 * 6 * $Id: EntityJoin.java,v 1.1 2008/02/07 17:12:26 mark Exp $ 7 */ 8 9package com.sleepycat.persist; 10 11import java.util.ArrayList; 12import java.util.Iterator; 13import java.util.List; 14 15import com.sleepycat.bind.EntityBinding; 16import com.sleepycat.bind.EntryBinding; 17import com.sleepycat.db.Cursor; 18import com.sleepycat.db.CursorConfig; 19import com.sleepycat.db.Database; 20import com.sleepycat.db.DatabaseEntry; 21import com.sleepycat.db.DatabaseException; 22import com.sleepycat.db.JoinCursor; 23import com.sleepycat.db.LockMode; 24import com.sleepycat.db.OperationStatus; 25import com.sleepycat.db.Transaction; 26 27/** 28 * Performs an equality join on two or more secondary keys. 29 * 30 * <p>{@code EntityJoin} objects are thread-safe. Multiple threads may safely 31 * call the methods of a shared {@code EntityJoin} object.</p> 32 * 33 * <p>An equality join is a match on all entities in a given primary index that 34 * have two or more specific secondary key values. Note that key ranges may 35 * not be matched by an equality join, only exact keys are matched.</p> 36 * 37 * <p>For example:</p> 38 * <pre class="code"> 39 * // Index declarations -- see {@link <a href="package-summary.html#example">package summary example</a>}. 40 * // 41 * {@literal PrimaryIndex<String,Person> personBySsn;} 42 * {@literal SecondaryIndex<String,String,Person> personByParentSsn;} 43 * {@literal SecondaryIndex<Long,String,Person> personByEmployerIds;} 44 * Employer employer = ...; 45 * 46 * // Match on all Person objects having parentSsn "111-11-1111" and also 47 * // containing an employerId of employer.id. In other words, match on all 48 * // of Bob's children that work for a given employer. 49 * // 50 * {@literal EntityJoin<String,Person> join = new EntityJoin(personBySsn);} 51 * join.addCondition(personByParentSsn, "111-11-1111"); 52 * join.addCondition(personByEmployerIds, employer.id); 53 * 54 * // Perform the join operation by traversing the results with a cursor. 55 * // 56 * {@literal ForwardCursor<Person> results = join.entities();} 57 * try { 58 * for (Person person : results) { 59 * System.out.println(person.ssn + ' ' + person.name); 60 * } 61 * } finally { 62 * results.close(); 63 * }</pre> 64 * 65 * @author Mark Hayes 66 */ 67public class EntityJoin<PK,E> { 68 69 private PrimaryIndex<PK,E> primary; 70 private List<Condition> conditions; 71 72 /** 73 * Creates a join object for a given primary index. 74 * 75 * @param index the primary index on which the join will operate. 76 */ 77 public EntityJoin(PrimaryIndex<PK,E> index) { 78 primary = index; 79 conditions = new ArrayList<Condition>(); 80 } 81 82 /** 83 * Adds a secondary key condition to the equality join. Only entities 84 * having the given key value in the given secondary index will be returned 85 * by the join operation. 86 * 87 * @param index the secondary index containing the given key value. 88 * 89 * @param key the key value to match during the join. 90 */ 91 public <SK> void addCondition(SecondaryIndex<SK,PK,E> index, SK key) { 92 93 /* Make key entry. */ 94 DatabaseEntry keyEntry = new DatabaseEntry(); 95 index.getKeyBinding().objectToEntry(key, keyEntry); 96 97 /* Use keys database if available. */ 98 Database db = index.getKeysDatabase(); 99 if (db == null) { 100 db = index.getDatabase(); 101 } 102 103 /* Add condition. */ 104 conditions.add(new Condition(db, keyEntry)); 105 } 106 107 /** 108 * Opens a cursor that returns the entities qualifying for the join. The 109 * join operation is performed as the returned cursor is accessed. 110 * 111 * <p>The operations performed with the cursor will not be transaction 112 * protected, and {@link CursorConfig#DEFAULT} is used implicitly.</p> 113 * 114 * @return the cursor. 115 * 116 * @throws IllegalStateException if less than two conditions were added. 117 */ 118 public ForwardCursor<E> entities() 119 throws DatabaseException { 120 121 return entities(null, null); 122 } 123 124 /** 125 * Opens a cursor that returns the entities qualifying for the join. The 126 * join operation is performed as the returned cursor is accessed. 127 * 128 * @param txn the transaction used to protect all operations performed with 129 * the cursor, or null if the operations should not be transaction 130 * protected. 131 * 132 * @param config the cursor configuration that determines the default lock 133 * mode used for all cursor operations, or null to implicitly use {@link 134 * CursorConfig#DEFAULT}. 135 * 136 * @return the cursor. 137 * 138 * @throws IllegalStateException if less than two conditions were added. 139 */ 140 public ForwardCursor<E> entities(Transaction txn, CursorConfig config) 141 throws DatabaseException { 142 143 return new JoinForwardCursor<E>(txn, config, false); 144 } 145 146 /** 147 * Opens a cursor that returns the primary keys of entities qualifying for 148 * the join. The join operation is performed as the returned cursor is 149 * accessed. 150 * 151 * <p>The operations performed with the cursor will not be transaction 152 * protected, and {@link CursorConfig#DEFAULT} is used implicitly.</p> 153 * 154 * @return the cursor. 155 * 156 * @throws IllegalStateException if less than two conditions were added. 157 */ 158 public ForwardCursor<PK> keys() 159 throws DatabaseException { 160 161 return keys(null, null); 162 } 163 164 /** 165 * Opens a cursor that returns the primary keys of entities qualifying for 166 * the join. The join operation is performed as the returned cursor is 167 * accessed. 168 * 169 * @param txn the transaction used to protect all operations performed with 170 * the cursor, or null if the operations should not be transaction 171 * protected. 172 * 173 * @param config the cursor configuration that determines the default lock 174 * mode used for all cursor operations, or null to implicitly use {@link 175 * CursorConfig#DEFAULT}. 176 * 177 * @return the cursor. 178 * 179 * @throws IllegalStateException if less than two conditions were added. 180 */ 181 public ForwardCursor<PK> keys(Transaction txn, CursorConfig config) 182 throws DatabaseException { 183 184 return new JoinForwardCursor<PK>(txn, config, true); 185 } 186 187 private static class Condition { 188 189 private Database db; 190 private DatabaseEntry key; 191 192 Condition(Database db, DatabaseEntry key) { 193 this.db = db; 194 this.key = key; 195 } 196 197 Cursor openCursor(Transaction txn, CursorConfig config) 198 throws DatabaseException { 199 200 OperationStatus status; 201 Cursor cursor = db.openCursor(txn, config); 202 try { 203 DatabaseEntry data = BasicIndex.NO_RETURN_ENTRY; 204 status = cursor.getSearchKey(key, data, null); 205 } catch (DatabaseException e) { 206 try { 207 cursor.close(); 208 } catch (DatabaseException ignored) {} 209 throw e; 210 } 211 if (status == OperationStatus.SUCCESS) { 212 return cursor; 213 } else { 214 cursor.close(); 215 return null; 216 } 217 } 218 } 219 220 private class JoinForwardCursor<V> implements ForwardCursor<V> { 221 222 private Cursor[] cursors; 223 private JoinCursor joinCursor; 224 private boolean doKeys; 225 226 JoinForwardCursor(Transaction txn, CursorConfig config, boolean doKeys) 227 throws DatabaseException { 228 229 this.doKeys = doKeys; 230 try { 231 cursors = new Cursor[conditions.size()]; 232 for (int i = 0; i < cursors.length; i += 1) { 233 Condition cond = conditions.get(i); 234 Cursor cursor = cond.openCursor(txn, config); 235 if (cursor == null) { 236 /* Leave joinCursor null. */ 237 doClose(null); 238 return; 239 } 240 cursors[i] = cursor; 241 } 242 joinCursor = primary.getDatabase().join(cursors, null); 243 } catch (DatabaseException e) { 244 /* doClose will throw e. */ 245 doClose(e); 246 } 247 } 248 249 public V next() 250 throws DatabaseException { 251 252 return next(null); 253 } 254 255 public V next(LockMode lockMode) 256 throws DatabaseException { 257 258 if (joinCursor == null) { 259 return null; 260 } 261 if (doKeys) { 262 DatabaseEntry key = new DatabaseEntry(); 263 OperationStatus status = joinCursor.getNext(key, lockMode); 264 if (status == OperationStatus.SUCCESS) { 265 EntryBinding binding = primary.getKeyBinding(); 266 return (V) binding.entryToObject(key); 267 } 268 } else { 269 DatabaseEntry key = new DatabaseEntry(); 270 DatabaseEntry data = new DatabaseEntry(); 271 OperationStatus status = 272 joinCursor.getNext(key, data, lockMode); 273 if (status == OperationStatus.SUCCESS) { 274 EntityBinding binding = primary.getEntityBinding(); 275 return (V) binding.entryToObject(key, data); 276 } 277 } 278 return null; 279 } 280 281 public Iterator<V> iterator() { 282 return iterator(null); 283 } 284 285 public Iterator<V> iterator(LockMode lockMode) { 286 return new BasicIterator<V>(this, lockMode); 287 } 288 289 public void close() 290 throws DatabaseException { 291 292 doClose(null); 293 } 294 295 private void doClose(DatabaseException firstException) 296 throws DatabaseException { 297 298 if (joinCursor != null) { 299 try { 300 joinCursor.close(); 301 joinCursor = null; 302 } catch (DatabaseException e) { 303 if (firstException == null) { 304 firstException = e; 305 } 306 } 307 } 308 for (int i = 0; i < cursors.length; i += 1) { 309 Cursor cursor = cursors[i]; 310 if (cursor != null) { 311 try { 312 cursor.close(); 313 cursors[i] = null; 314 } catch (DatabaseException e) { 315 if (firstException == null) { 316 firstException = e; 317 } 318 } 319 } 320 } 321 if (firstException != null) { 322 throw firstException; 323 } 324 } 325 } 326} 327