1 /*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2002, 2014 Oracle and/or its affiliates. All rights reserved. 5 * 6 */ 7 8 package com.sleepycat.je; 9 10 import static com.sleepycat.je.dbi.DbiStatDefinition.THROUGHPUT_SECONDARYDB_DELETE; 11 import static com.sleepycat.je.dbi.DbiStatDefinition.THROUGHPUT_SECONDARYDB_GET; 12 import static com.sleepycat.je.dbi.DbiStatDefinition.THROUGHPUT_SECONDARYDB_GETSEARCHBOTH; 13 14 import java.util.Collection; 15 import java.util.Collections; 16 import java.util.HashSet; 17 import java.util.Iterator; 18 import java.util.List; 19 import java.util.Set; 20 import java.util.logging.Level; 21 22 import com.sleepycat.je.dbi.CursorImpl.SearchMode; 23 import com.sleepycat.je.dbi.DatabaseImpl; 24 import com.sleepycat.je.dbi.EnvironmentImpl; 25 import com.sleepycat.je.dbi.GetMode; 26 import com.sleepycat.je.dbi.PutMode; 27 import com.sleepycat.je.txn.Locker; 28 import com.sleepycat.je.txn.LockerFactory; 29 import com.sleepycat.je.utilint.AtomicLongStat; 30 import com.sleepycat.je.utilint.DatabaseUtil; 31 import com.sleepycat.je.utilint.LoggerUtils; 32 33 /** 34 * A secondary database handle. 35 * 36 * <p>Secondary databases are opened with {@link 37 * Environment#openSecondaryDatabase Environment.openSecondaryDatabase} and are 38 * always associated with a single primary database. The distinguishing 39 * characteristics of a secondary database are:</p> 40 * 41 * <ul> <li>Records are automatically added to a secondary database when 42 * records are added, modified and deleted in the primary database. Direct 43 * calls to <code>put()</code> methods on a secondary database are 44 * prohibited.</li> 45 * <li>The {@link #delete delete} method of a secondary database will delete 46 * the primary record and as well as all its associated secondary records.</li> 47 * <li>Calls to all <code>get()</code> methods will return the data from the 48 * associated primary database.</li> 49 * <li>Additional <code>get()</code> method signatures are provided to return 50 * the primary key in an additional <code>pKey</code> parameter.</li> 51 * <li>Calls to {@link #openCursor openCursor} will return a {@link 52 * SecondaryCursor}, which itself has <code>get()</code> methods that return 53 * the data of the primary database and additional <code>get()</code> method 54 * signatures for returning the primary key.</li> 55 * </ul> 56 * <p>Before opening or creating a secondary database you must implement 57 * the {@link SecondaryKeyCreator} or {@link SecondaryMultiKeyCreator} 58 * interface.</p> 59 * 60 * <p>For example, to create a secondary database that supports duplicates:</p> 61 * 62 * <pre> 63 * Database primaryDb; // The primary database must already be open. 64 * SecondaryKeyCreator keyCreator; // Your key creator implementation. 65 * SecondaryConfig secConfig = new SecondaryConfig(); 66 * secConfig.setAllowCreate(true); 67 * secConfig.setSortedDuplicates(true); 68 * secConfig.setKeyCreator(keyCreator); 69 * SecondaryDatabase newDb = env.openSecondaryDatabase(transaction, 70 * "myDatabaseName", 71 * primaryDb, 72 * secConfig) 73 * </pre> 74 * 75 * <p>If a primary database is to be associated with one or more secondary 76 * databases, it may not be configured for duplicates.</p> 77 * 78 * <p><b>WARNING:</b> The associations between primary and secondary databases 79 * are not stored persistently. Whenever a primary database is opened for 80 * write access by the application, the appropriate associated secondary 81 * databases should also be opened by the application. This is necessary to 82 * ensure data integrity when changes are made to the primary database. If the 83 * secondary database is not opened, it will not be updated when the primary is 84 * updated, and the references between the databases will become invalid. 85 * (Note that this warning does not apply when using the {@link 86 * com.sleepycat.persist DPL}, which does store secondary relationships 87 * persistently.)</p> 88 * 89 * <h3><a name="transactions">Special considerations for using Secondary 90 * Databases with and without Transactions</a></h3> 91 * 92 * <p>Normally, during a primary database write operation (insert, update or 93 * delete), all associated secondary databases are also updated. However, when 94 * an exception occurs during the write operation, the updates may be 95 * incomplete. If the databases are transactional, this is handled by aborting 96 * the transaction to undo the incomplete operation. If an auto-commit 97 * transaction is used (null is passed for the transaction), the transaction 98 * will be aborted automatically. If an explicit transaction is used, it 99 * must be aborted by the application caller after the exception is caught.</p> 100 * 101 * <p>However, if the databases are non-transactional, integrity problems can 102 * result when an exception occurs during the write operation. Because the 103 * write operation is not made atomic by a transaction, references between the 104 * databases will become invalid if the operation is incomplete. This results 105 * in a {@link SecondaryIntegrityException} when attempting to access the 106 * databases later.</p> 107 * 108 * <p>A secondary integrity problem is persistent; it cannot be resolved by 109 * reopening the databases or the environment. The only way to resolve the 110 * problem is to restore the environment from a valid backup, or, if the 111 * integrity of the primary database is assumed, to remove and recreate all 112 * secondary databases.</p> 113 * 114 * <p>Therefore, secondary databases and indexes should always be used in 115 * conjunction with transactional databases and stores. Without transactions, 116 * it is the responsibility of the application to handle the results of the 117 * incomplete write operation or to take steps to prevent this situation from 118 * happening in the first place.</p> 119 * 120 * <p>The following exceptions may be thrown during a write operation, and may 121 * cause an integrity problem in the absence of transactions.</p> 122 * <ul> 123 * <li>{@link SecondaryConstraintException}, see its subclasses for more 124 * information.</li> 125 * <li>{@link LockConflictException}, when more than one thread is accessing 126 * the databases.</li> 127 * <li>{@link EnvironmentFailureException}, if an unexpected or system failure 128 * occurs.</li> 129 * <li>There is always the possibility of an {@link Error} or an unintended 130 * {@link RuntimeException}.</li> 131 * </ul> 132 */ 133 public class SecondaryDatabase extends Database { 134 135 /* For type-safe check against EMPTY_SET */ 136 private static final Set<DatabaseEntry> EMPTY_SET = 137 Collections.emptySet(); 138 139 private final Database primaryDatabase; // May be null. 140 private SecondaryConfig secondaryConfig; 141 private volatile boolean isFullyPopulated = true; 142 143 private AtomicLongStat deleteStat; 144 private AtomicLongStat getStat; 145 private AtomicLongStat getSearchBothStat; 146 147 /** 148 * Creates a secondary database but does not open or fully initialize it. 149 * 150 * @throws IllegalArgumentException via Environment.openSecondaryDatabase. 151 */ SecondaryDatabase(final Environment env, final SecondaryConfig secConfig, final Database primaryDatabase)152 SecondaryDatabase(final Environment env, 153 final SecondaryConfig secConfig, 154 final Database primaryDatabase) 155 throws DatabaseException { 156 157 super(env); 158 this.primaryDatabase = primaryDatabase; 159 if (primaryDatabase == null) { 160 if (secConfig.getSecondaryAssociation() == null) { 161 throw new IllegalArgumentException( 162 "Exactly one must be non-null: " + 163 "PrimaryDatabase or SecondaryAssociation"); 164 } 165 if (secConfig.getAllowPopulate()) { 166 throw new IllegalArgumentException( 167 "AllowPopulate must be false when a SecondaryAssociation" + 168 " is configured"); 169 } 170 } else { 171 if (secConfig.getSecondaryAssociation() != null) { 172 throw new IllegalArgumentException( 173 "Exactly one must be non-null: " + 174 "PrimaryDatabase or SecondaryAssociation"); 175 } 176 primaryDatabase.checkOpen("Can't use as primary:"); 177 if (primaryDatabase.configuration.getSortedDuplicates()) { 178 throw new IllegalArgumentException( 179 "Duplicates not allowed for a primary database: " + 180 primaryDatabase.getDebugName()); 181 } 182 if (env.getEnvironmentImpl() != 183 primaryDatabase.getEnvironment().getEnvironmentImpl()) { 184 throw new IllegalArgumentException( 185 "Primary and secondary databases must be in the same" + 186 " environment"); 187 } 188 if (!primaryDatabase.configuration.getReadOnly() && 189 secConfig.getKeyCreator() == null && 190 secConfig.getMultiKeyCreator() == null) { 191 throw new IllegalArgumentException( 192 "SecondaryConfig.getKeyCreator()/getMultiKeyCreator()" + 193 " may be null only if the primary database is read-only"); 194 } 195 } 196 if (secConfig.getKeyCreator() != null && 197 secConfig.getMultiKeyCreator() != null) { 198 throw new IllegalArgumentException( 199 "secConfig.getKeyCreator() and getMultiKeyCreator() may not" + 200 " both be non-null"); 201 } 202 if (secConfig.getForeignKeyNullifier() != null && 203 secConfig.getForeignMultiKeyNullifier() != null) { 204 throw new IllegalArgumentException( 205 "secConfig.getForeignKeyNullifier() and" + 206 " getForeignMultiKeyNullifier() may not both be non-null"); 207 } 208 if (secConfig.getForeignKeyDeleteAction() == 209 ForeignKeyDeleteAction.NULLIFY && 210 secConfig.getForeignKeyNullifier() == null && 211 secConfig.getForeignMultiKeyNullifier() == null) { 212 throw new IllegalArgumentException( 213 "ForeignKeyNullifier or ForeignMultiKeyNullifier must be" + 214 " non-null when ForeignKeyDeleteAction is NULLIFY"); 215 } 216 if (secConfig.getForeignKeyNullifier() != null && 217 secConfig.getMultiKeyCreator() != null) { 218 throw new IllegalArgumentException( 219 "ForeignKeyNullifier may not be used with" + 220 " SecondaryMultiKeyCreator -- use" + 221 " ForeignMultiKeyNullifier instead"); 222 } 223 if (secConfig.getForeignKeyDatabase() != null) { 224 Database foreignDb = secConfig.getForeignKeyDatabase(); 225 if (foreignDb.getDatabaseImpl().getSortedDuplicates()) { 226 throw new IllegalArgumentException( 227 "Duplicates must not be allowed for a foreign key " + 228 " database: " + foreignDb.getDebugName()); 229 } 230 } 231 setupThroughputStats(env.getEnvironmentImpl()); 232 } 233 234 /** 235 * Create a database, called by Environment 236 */ 237 @Override initNew(final Environment env, final Locker locker, final String databaseName, final DatabaseConfig dbConfig)238 DatabaseImpl initNew(final Environment env, 239 final Locker locker, 240 final String databaseName, 241 final DatabaseConfig dbConfig) 242 throws DatabaseException { 243 244 final DatabaseImpl dbImpl = 245 super.initNew(env, locker, databaseName, dbConfig); 246 init(locker); 247 return dbImpl; 248 } 249 250 /** 251 * Open a database, called by Environment 252 * 253 * @throws IllegalArgumentException via Environment.openSecondaryDatabase. 254 */ 255 @Override initExisting(final Environment env, final Locker locker, final DatabaseImpl database, final String databaseName, final DatabaseConfig dbConfig)256 void initExisting(final Environment env, 257 final Locker locker, 258 final DatabaseImpl database, 259 final String databaseName, 260 final DatabaseConfig dbConfig) 261 throws DatabaseException { 262 263 /* Disallow one secondary associated with two different primaries. */ 264 if (primaryDatabase != null) { 265 Database otherPriDb = database.findPrimaryDatabase(); 266 if (otherPriDb != null && 267 otherPriDb.getDatabaseImpl() != 268 primaryDatabase.getDatabaseImpl()) { 269 throw new IllegalArgumentException( 270 "Secondary already associated with different primary: " + 271 otherPriDb.getDebugName()); 272 } 273 } 274 275 super.initExisting(env, locker, database, databaseName, dbConfig); 276 init(locker); 277 } 278 279 /** 280 * Adds secondary to primary's list, and populates the secondary if needed. 281 * 282 * @param locker should be the locker used to open the database. If a 283 * transactional locker, the population operations will occur in the same 284 * transaction; this may result in a large number of retained locks. If a 285 * non-transactional locker, the Cursor will create a ThreadLocker (even if 286 * a BasicLocker used for handle locking is passed), and locks will not be 287 * retained. 288 */ init(final Locker locker)289 private void init(final Locker locker) 290 throws DatabaseException { 291 292 trace(Level.FINEST, "SecondaryDatabase open"); 293 294 secondaryConfig = (SecondaryConfig) configuration; 295 296 Database foreignDb = secondaryConfig.getForeignKeyDatabase(); 297 if (foreignDb != null) { 298 foreignDb.foreignKeySecondaries.add(this); 299 } 300 301 /* Populate secondary if requested and secondary is empty. */ 302 if (!secondaryConfig.getAllowPopulate()) { 303 return; 304 } 305 Cursor secCursor = null; 306 Cursor priCursor = null; 307 try { 308 secCursor = new Cursor(this, locker, null); 309 DatabaseEntry key = new DatabaseEntry(); 310 DatabaseEntry data = new DatabaseEntry(); 311 OperationStatus status = secCursor.position(key, data, 312 LockMode.DEFAULT, 313 true); 314 if (status != OperationStatus.NOTFOUND) { 315 return; 316 } 317 /* Is empty, so populate */ 318 priCursor = new Cursor(primaryDatabase, locker, null); 319 status = priCursor.position(key, data, LockMode.DEFAULT, true); 320 while (status == OperationStatus.SUCCESS) { 321 updateSecondary(locker, secCursor, key, null, data); 322 status = priCursor.retrieveNext(key, data, 323 LockMode.DEFAULT, 324 GetMode.NEXT); 325 } 326 } finally { 327 if (secCursor != null) { 328 secCursor.close(); 329 } 330 if (priCursor != null) { 331 priCursor.close(); 332 } 333 } 334 } 335 336 @Override makeSecondaryAssociation()337 SecondaryAssociation makeSecondaryAssociation() { 338 /* Only one is non-null: primaryDatabase, SecondaryAssociation. */ 339 if (primaryDatabase != null) { 340 primaryDatabase.simpleAssocSecondaries.add(this); 341 return primaryDatabase.secAssoc; 342 } 343 return configuration.getSecondaryAssociation(); 344 } 345 346 /** 347 * Closes a secondary database and dis-associates it from its primary 348 * database. A secondary database should be closed before closing its 349 * associated primary database. 350 * 351 * {@inheritDoc} 352 * 353 * <!-- inherit other javadoc from overridden method --> 354 */ 355 @Override close()356 public synchronized void close() 357 throws DatabaseException { 358 359 /* removeReferringAssociations will be called during close. */ 360 super.close(); 361 } 362 363 @Override removeReferringAssociations()364 void removeReferringAssociations() { 365 super.removeReferringAssociations(); 366 if (primaryDatabase != null) { 367 primaryDatabase.simpleAssocSecondaries.remove(this); 368 } 369 if (secondaryConfig != null) { 370 final Database foreignDb = secondaryConfig.getForeignKeyDatabase(); 371 if (foreignDb != null) { 372 foreignDb.foreignKeySecondaries.remove(this); 373 } 374 } 375 } 376 377 /** 378 * @hidden 379 * For internal use only. 380 * 381 * Enables incremental population of this secondary database, so that index 382 * population can occur incrementally, and concurrently with primary 383 * database writes. 384 * <p> 385 * After calling this method (and before calling {@link 386 * #endIncrementalPopulation}), it is expected that the application will 387 * populate the secondary explicitly by calling {@link 388 * Database#populateSecondaries} to process all records for the primary 389 * database(s) associated with this secondary. 390 * <p> 391 * The concurrent population mode supports concurrent indexing by ordinary 392 * writes to the primary database(s) and calls to {@link 393 * Database#populateSecondaries}. To provide this capability, some 394 * primary-secondary integrity checking is disabled. The integrity 395 * checking (that is disabled) is meant only to detect application bugs, 396 * and is not necessary for normal operations. Specifically, the checks 397 * that are disabled are: 398 * <ul> 399 * <li>When a new secondary key is inserted, because a primary record is 400 * inserted or updated, we normally check that a key mapped to the 401 * primary record does not already exist in the secondary database.</li> 402 * <li>When an existing secondary key is deleted, because a primary 403 * record is updated or deleted, we normally check that a key mapped to 404 * the primary record already does exist in the secondary database.</li> 405 * </ul> 406 * Without these checks, one can think of the secondary indexing operations 407 * as being idempotent. Via the idempotent indexing operations, explicit 408 * population (via {@link Database#populateSecondaries}) and normal 409 * secondary population (via primary writes) collaborate to add and delete 410 * index records as needed. 411 */ startIncrementalPopulation()412 public void startIncrementalPopulation() { 413 isFullyPopulated = false; 414 } 415 416 /** 417 * @hidden 418 * For internal use only. 419 * 420 * Disables incremental population of this secondary database, after this 421 * index has been fully populated. 422 * <p> 423 * After calling this method, this database may not be populated by calling 424 * {@link Database#populateSecondaries}, and all primary-secondary 425 * integrity checking for this secondary is enabled. 426 */ endIncrementalPopulation()427 public void endIncrementalPopulation() { 428 isFullyPopulated = true; 429 } 430 431 /** 432 * @hidden 433 * For internal use only. 434 * 435 * @return true if {@link #startIncrementalPopulation} was called, and 436 * {@link #endIncrementalPopulation} was not subsequently called. 437 */ isIncrementalPopulationEnabled()438 public boolean isIncrementalPopulationEnabled() { 439 return !isFullyPopulated; 440 } 441 442 /** 443 * @hidden 444 * For internal use only. 445 * 446 * Reads {@code batchSize} records starting at the given {@code key} and 447 * {@code data}, and deletes any secondary records having a primary key 448 * (the data of the secondary record) for which {@link 449 * SecondaryAssociation#getPrimary} returns null. The next key/data pair 450 * to be processed is returned in the {@code key} and {@code data} 451 * parameters so these can be passed in to process the next batch. 452 * <p> 453 * It is the application's responsibility to save the key/data pair 454 * returned by this method, and then pass the saved key/data when the 455 * method is called again to process the next batch of records. The 456 * application may wish to save the key/data persistently in order to avoid 457 * restarting the processing from the beginning of the database after a 458 * crash. 459 * 460 * @param key contains the starting key for the batch of records to be 461 * processed when this method is called, and contains the next key to be 462 * processed when this method returns. If {@code key.getData() == null} 463 * when this method is called, the batch will begin with the first record 464 * in the database. 465 * 466 * @param data contains the starting data element (primary key) for the 467 * batch of records to be processed when this method is called, and 468 * contains the next data element to be processed when this method returns. 469 * If {@code key.getData() == null} when this method is called, the batch 470 * will begin with the first record in the database. 471 * 472 * @param batchSize is the maximum number of records to be read, and also 473 * the maximum number of deletions that will be included in a single 474 * transaction. 475 * 476 * @return true if more records may need to be processed, or false if 477 * processing is complete. 478 */ deleteObsoletePrimaryKeys(final DatabaseEntry key, final DatabaseEntry data, final int batchSize)479 public boolean deleteObsoletePrimaryKeys(final DatabaseEntry key, 480 final DatabaseEntry data, 481 final int batchSize) { 482 try { 483 checkEnv(); 484 DatabaseUtil.checkForNullDbt(key, "key", false); 485 if (batchSize <= 0) { 486 throw new IllegalArgumentException( 487 "batchSize must be positive"); 488 } 489 checkOpen("Can't call deleteObsoletePrimaryKeys:"); 490 trace(Level.FINEST, "deleteObsoletePrimaryKeys", null, key, 491 null, null); 492 493 final Locker locker = LockerFactory.getWritableLocker( 494 envHandle, null, getDatabaseImpl().isInternalDb(), 495 isTransactional(), 496 getDatabaseImpl().isReplicated() /*autoTxnIsReplicated*/); 497 try { 498 final Cursor cursor = new Cursor(this, locker, null); 499 try { 500 return deleteObsoletePrimaryKeysInternal( 501 cursor, locker, key, data, batchSize); 502 } finally { 503 cursor.close(); 504 } 505 } finally { 506 locker.operationEnd(true); 507 } 508 } catch (Error E) { 509 DbInternal.getEnvironmentImpl(envHandle).invalidate(E); 510 throw E; 511 } 512 } 513 514 /** 515 * Use a scan to walk through the primary keys. If the primary key is 516 * obsolete (SecondaryAssociation.getPrimary returns null), delete the 517 * record. 518 */ deleteObsoletePrimaryKeysInternal(final Cursor cursor, final Locker locker, final DatabaseEntry key, final DatabaseEntry data, final int batchSize)519 private boolean deleteObsoletePrimaryKeysInternal(final Cursor cursor, 520 final Locker locker, 521 final DatabaseEntry key, 522 final DatabaseEntry data, 523 final int batchSize) { 524 /* TODO: use dirty-read scan with mode to return deleted records. */ 525 final LockMode scanMode = LockMode.RMW; 526 OperationStatus searchStatus; 527 if (key.getData() == null) { 528 /* Start at first key. */ 529 searchStatus = cursor.position(key, data, scanMode, true); 530 } else { 531 /* Resume at key/data pair last processed. */ 532 searchStatus = cursor.search(key, data, scanMode, 533 SearchMode.BOTH_RANGE); 534 if (searchStatus != OperationStatus.SUCCESS) { 535 searchStatus = cursor.search(key, data, scanMode, 536 SearchMode.SET_RANGE); 537 } 538 } 539 int nProcessed = 0; 540 while (searchStatus == OperationStatus.SUCCESS) { 541 if (nProcessed >= batchSize) { 542 return true; 543 } 544 nProcessed += 1; 545 if (secAssoc.getPrimary(data) == null) { 546 cursor.deleteNoNotify(getDatabaseImpl().getRepContext()); 547 } 548 searchStatus = cursor.retrieveNext(key, data, scanMode, 549 GetMode.NEXT); 550 } 551 return false; 552 } 553 554 /** 555 * @hidden 556 * For internal use only. 557 */ 558 @Override populateSecondaries(DatabaseEntry key, int batchSize)559 public boolean populateSecondaries(DatabaseEntry key, int batchSize) { 560 throw new UnsupportedOperationException("Not allowed on a secondary"); 561 } 562 563 /** 564 * Returns the primary database associated with this secondary database. 565 * 566 * @return the primary database associated with this secondary database. 567 */ 568 569 /* 570 * To be added when SecondaryAssociation is published: 571 * If a {@link SecondaryAssociation} is {@link 572 * SecondaryCursor#setSecondaryAssociation configured}, this method returns 573 * null. 574 */ getPrimaryDatabase()575 public Database getPrimaryDatabase() { 576 return primaryDatabase; 577 } 578 579 /** 580 * Returns an empty list, since this database is itself a secondary 581 * database. 582 */ 583 @Override getSecondaryDatabases()584 public List<SecondaryDatabase> getSecondaryDatabases() { 585 return Collections.emptyList(); 586 } 587 588 /** 589 * Returns a copy of the secondary configuration of this database. 590 * 591 * @return a copy of the secondary configuration of this database. 592 * 593 * @throws EnvironmentFailureException if an unexpected, internal or 594 * environment-wide failure occurs. 595 * 596 * @deprecated As of JE 4.0.13, replaced by {@link 597 * SecondaryDatabase#getConfig()}.</p> 598 */ getSecondaryConfig()599 public SecondaryConfig getSecondaryConfig() 600 throws DatabaseException { 601 602 return getConfig(); 603 } 604 605 /** 606 * Returns a copy of the secondary configuration of this database. 607 * 608 * @return a copy of the secondary configuration of this database. 609 * 610 * @throws EnvironmentFailureException if an unexpected, internal or 611 * environment-wide failure occurs. 612 */ 613 @Override getConfig()614 public SecondaryConfig getConfig() 615 throws DatabaseException { 616 617 return (SecondaryConfig) super.getConfig(); 618 } 619 620 /** 621 * @hidden 622 * Returns the secondary config without cloning, for internal use. 623 */ getPrivateSecondaryConfig()624 public SecondaryConfig getPrivateSecondaryConfig() { 625 return secondaryConfig; 626 } 627 628 /** 629 * Obtain a cursor on a database, returning a 630 * <code>SecondaryCursor</code>. Calling this method is the equivalent of 631 * calling {@link #openCursor} and casting the result to {@link 632 * SecondaryCursor}. 633 * 634 * @param txn the transaction used to protect all operations performed with 635 * the cursor, or null if the operations should not be transaction 636 * protected. If the database is non-transactional, null must be 637 * specified. For a transactional database, the transaction is optional 638 * for read-only access and required for read-write access. 639 * 640 * @param cursorConfig The cursor attributes. If null, default attributes 641 * are used. 642 * 643 * @return A secondary database cursor. 644 * 645 * @throws EnvironmentFailureException if an unexpected, internal or 646 * environment-wide failure occurs. 647 * 648 * @deprecated As of JE 4.0.13, replaced by {@link 649 * SecondaryDatabase#openCursor}.</p> 650 */ openSecondaryCursor(final Transaction txn, final CursorConfig cursorConfig)651 public SecondaryCursor openSecondaryCursor(final Transaction txn, 652 final CursorConfig cursorConfig) 653 throws DatabaseException { 654 655 return openCursor(txn, cursorConfig); 656 } 657 658 /** 659 * Obtain a cursor on a database, returning a <code>SecondaryCursor</code>. 660 */ 661 @Override openCursor(final Transaction txn, final CursorConfig cursorConfig)662 public SecondaryCursor openCursor(final Transaction txn, 663 final CursorConfig cursorConfig) 664 throws DatabaseException { 665 666 checkReadable("Can't call SecondaryDatabase.openCursor:"); 667 return (SecondaryCursor) super.openCursor(txn, cursorConfig); 668 } 669 670 /** 671 * Overrides Database method. 672 */ 673 @Override newDbcInstance(final Transaction txn, final CursorConfig cursorConfig)674 Cursor newDbcInstance(final Transaction txn, 675 final CursorConfig cursorConfig) 676 throws DatabaseException { 677 678 return new SecondaryCursor(this, txn, cursorConfig); 679 } 680 681 /** 682 * Deletes the primary key/data pair associated with the specified 683 * secondary key. In the presence of duplicate key values, all primary 684 * records associated with the designated secondary key will be deleted. 685 * 686 * When the primary records are deleted, their associated secondary records 687 * are deleted as if {@link Database#delete} were called. This includes, 688 * but is not limited to, the secondary record referenced by the given key. 689 * 690 * @param key the secondary key used as input. It must be initialized with 691 * a non-null byte array by the caller. 692 * 693 * <!-- inherit other javadoc from overridden method --> 694 */ 695 @Override delete(final Transaction txn, final DatabaseEntry key)696 public OperationStatus delete(final Transaction txn, 697 final DatabaseEntry key) 698 throws DeleteConstraintException, 699 LockConflictException, 700 DatabaseException, 701 UnsupportedOperationException, 702 IllegalArgumentException { 703 704 checkEnv(); 705 DatabaseUtil.checkForNullDbt(key, "key", true); 706 checkReadable("Can't call SecondaryDatabase.delete:"); 707 trace(Level.FINEST, "SecondaryDatabase.delete", txn, 708 key, null, null); 709 if (deleteStat != null) { 710 deleteStat.increment(); 711 } 712 713 Locker locker = null; 714 Cursor cursor = null; 715 716 OperationStatus commitStatus = OperationStatus.NOTFOUND; 717 try { 718 locker = LockerFactory.getWritableLocker( 719 envHandle, 720 txn, 721 getDatabaseImpl().isInternalDb(), 722 isTransactional(), 723 getDatabaseImpl().isReplicated()); // autoTxnIsReplicated 724 725 final LockMode lockMode = locker.isSerializableIsolation() ? 726 LockMode.RMW : 727 LockMode.READ_UNCOMMITTED_ALL; 728 729 /* Read the primary key (the data of a secondary). */ 730 cursor = new Cursor(this, locker, null); 731 DatabaseEntry pKey = new DatabaseEntry(); 732 733 OperationStatus searchStatus = cursor.search( 734 key, pKey, lockMode, SearchMode.SET); 735 736 /* 737 * For each duplicate secondary key, delete the primary record and 738 * all its associated secondary records, including the one 739 * referenced by this secondary cursor. 740 */ 741 while (searchStatus == OperationStatus.SUCCESS) { 742 final Database primaryDb = getPrimary(pKey); 743 if (primaryDb == null) { 744 /* Primary was removed from the association. */ 745 cursor.deleteNoNotify(getDatabaseImpl().getRepContext()); 746 } else { 747 commitStatus = primaryDb.deleteInternal(locker, pKey); 748 if (commitStatus != OperationStatus.SUCCESS) { 749 if (lockMode != LockMode.RMW) { 750 751 /* 752 * The primary record was not found. The index 753 * may be either corrupt or the record was 754 * deleted between finding it in the secondary 755 * without locking and trying to delete it. 756 * If it was deleted then just skip it. 757 */ 758 if (cursor.checkCurrent(LockMode.RMW) == 759 OperationStatus.SUCCESS) { 760 /* there is a secondary index entry */ 761 throw secondaryRefersToMissingPrimaryKey( 762 locker, key, pKey); 763 } 764 } else { 765 /* there is a secondary index entry */ 766 throw secondaryRefersToMissingPrimaryKey( 767 locker, key, pKey); 768 } 769 } 770 } 771 searchStatus = cursor.retrieveNext( 772 key, pKey, lockMode, GetMode.NEXT_DUP); 773 } 774 return commitStatus; 775 } catch (Error E) { 776 DbInternal.getEnvironmentImpl(envHandle).invalidate(E); 777 throw E; 778 } finally { 779 if (cursor != null) { 780 cursor.close(); 781 } 782 if (locker != null) { 783 locker.operationEnd(commitStatus); 784 } 785 } 786 } 787 788 /** 789 * @param key the secondary key used as input. It must be initialized with 790 * a non-null byte array by the caller. 791 * 792 * @param data the primary data returned as output. Its byte array does 793 * not need to be initialized by the caller. 794 * 795 * <!-- inherit other javadoc from overridden method --> 796 */ 797 @Override get(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data, final LockMode lockMode)798 public OperationStatus get(final Transaction txn, 799 final DatabaseEntry key, 800 final DatabaseEntry data, 801 final LockMode lockMode) 802 throws DatabaseException { 803 804 return get(txn, key, new DatabaseEntry(), data, lockMode); 805 } 806 807 /** 808 * Retrieves the key/data pair with the given key. If the matching key has 809 * duplicate values, the first data item in the set of duplicates is 810 * returned. Retrieval of duplicates requires the use of {@link Cursor} 811 * operations. 812 * 813 * @param txn For a transactional database, an explicit transaction may be 814 * specified to transaction-protect the operation, or null may be specified 815 * to perform the operation without transaction protection. For a 816 * non-transactional database, null must be specified. 817 * 818 * @param key the secondary key used as input. It must be initialized with 819 * a non-null byte array by the caller. 820 * 821 * @param pKey the primary key returned as output. Its byte array does not 822 * need to be initialized by the caller. 823 * 824 * @param data the primary data returned as output. Its byte array does 825 * not need to be initialized by the caller. 826 * 827 * @param lockMode the locking attributes; if null, default attributes are 828 * used. 829 * 830 * @return {@link com.sleepycat.je.OperationStatus#NOTFOUND 831 * OperationStatus.NOTFOUND} if no matching key/data pair is found; 832 * otherwise, {@link com.sleepycat.je.OperationStatus#SUCCESS 833 * OperationStatus.SUCCESS}. 834 * 835 * @throws OperationFailureException if one of the <a 836 * href="OperationFailureException.html#readFailures">Read Operation 837 * Failures</a> occurs. 838 * 839 * @throws EnvironmentFailureException if an unexpected, internal or 840 * environment-wide failure occurs. 841 * 842 * @throws IllegalStateException if the database has been closed. 843 * 844 * @throws IllegalArgumentException if an invalid parameter is specified. 845 */ get(final Transaction txn, final DatabaseEntry key, final DatabaseEntry pKey, final DatabaseEntry data, LockMode lockMode)846 public OperationStatus get(final Transaction txn, 847 final DatabaseEntry key, 848 final DatabaseEntry pKey, 849 final DatabaseEntry data, 850 LockMode lockMode) 851 throws DatabaseException { 852 853 checkEnv(); 854 DatabaseUtil.checkForNullDbt(key, "key", true); 855 DatabaseUtil.checkForNullDbt(pKey, "pKey", false); 856 DatabaseUtil.checkForNullDbt(data, "data", false); 857 checkReadable("Can't call SecondaryDatabase.get:"); 858 trace(Level.FINEST, "SecondaryDatabase.get", txn, key, null, lockMode); 859 if (getStat != null) { 860 getStat.increment(); 861 } 862 863 CursorConfig cursorConfig = CursorConfig.DEFAULT; 864 if (lockMode == LockMode.READ_COMMITTED) { 865 cursorConfig = CursorConfig.READ_COMMITTED; 866 lockMode = null; 867 } 868 checkLockModeWithoutTxn(txn, lockMode); 869 870 Locker locker = null; 871 SecondaryCursor cursor = null; 872 OperationStatus commitStatus = null; 873 try { 874 locker = LockerFactory.getReadableLocker( 875 this, txn, cursorConfig.getReadCommitted()); 876 cursor = new SecondaryCursor(this, locker, cursorConfig); 877 commitStatus = 878 cursor.search(key, pKey, data, lockMode, SearchMode.SET); 879 return commitStatus; 880 } catch (Error E) { 881 DbInternal.getEnvironmentImpl(envHandle).invalidate(E); 882 throw E; 883 } finally { 884 if (cursor != null) { 885 cursor.close(); 886 } 887 888 if (locker != null) { 889 locker.operationEnd(commitStatus); 890 } 891 } 892 } 893 894 /** 895 * This operation is not allowed with this method signature. {@link 896 * UnsupportedOperationException} will always be thrown by this method. 897 * The corresponding method with the <code>pKey</code> parameter should be 898 * used instead. 899 */ 900 @Override getSearchBoth(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data, final LockMode lockMode)901 public OperationStatus getSearchBoth(final Transaction txn, 902 final DatabaseEntry key, 903 final DatabaseEntry data, 904 final LockMode lockMode) 905 throws UnsupportedOperationException { 906 907 throw notAllowedException(); 908 } 909 910 /** 911 * Retrieves the key/data pair with the specified secondary and primary 912 * key, that is, both the primary and secondary key items must match. 913 * 914 * @param txn For a transactional database, an explicit transaction may be 915 * specified to transaction-protect the operation, or null may be specified 916 * to perform the operation without transaction protection. For a 917 * non-transactional database, null must be specified. 918 * 919 * @param key the secondary key used as input. It must be initialized with 920 * a non-null byte array by the caller. 921 * 922 * @param pKey the primary key used as input. It must be initialized with a 923 * non-null byte array by the caller. 924 * 925 * @param data the primary data returned as output. Its byte array does not 926 * need to be initialized by the caller. 927 * 928 * @param lockMode the locking attributes; if null, default attributes are 929 * used. 930 * 931 * @return {@link com.sleepycat.je.OperationStatus#NOTFOUND 932 * OperationStatus.NOTFOUND} if no matching key/data pair is found; 933 * otherwise, {@link com.sleepycat.je.OperationStatus#SUCCESS 934 * OperationStatus.SUCCESS}. 935 * 936 * @throws OperationFailureException if one of the <a 937 * href="OperationFailureException.html#readFailures">Read Operation 938 * Failures</a> occurs. 939 * 940 * @throws EnvironmentFailureException if an unexpected, internal or 941 * environment-wide failure occurs. 942 * 943 * @throws IllegalStateException if the database has been closed. 944 * 945 * @throws IllegalArgumentException if an invalid parameter is specified. 946 */ getSearchBoth(final Transaction txn, final DatabaseEntry key, final DatabaseEntry pKey, final DatabaseEntry data, LockMode lockMode)947 public OperationStatus getSearchBoth(final Transaction txn, 948 final DatabaseEntry key, 949 final DatabaseEntry pKey, 950 final DatabaseEntry data, 951 LockMode lockMode) 952 throws DatabaseException { 953 954 checkEnv(); 955 DatabaseUtil.checkForNullDbt(key, "key", true); 956 DatabaseUtil.checkForNullDbt(pKey, "pKey", true); 957 DatabaseUtil.checkForNullDbt(data, "data", false); 958 checkReadable("Can't call SecondaryDatabase.getSearchBoth:"); 959 trace(Level.FINEST, "SecondaryDatabase.getSearchBoth", txn, key, data, 960 lockMode); 961 if (getSearchBothStat != null) { 962 getSearchBothStat.increment(); 963 } 964 965 CursorConfig cursorConfig = CursorConfig.DEFAULT; 966 if (lockMode == LockMode.READ_COMMITTED) { 967 cursorConfig = CursorConfig.READ_COMMITTED; 968 lockMode = null; 969 } 970 checkLockModeWithoutTxn(txn, lockMode); 971 972 Locker locker = null; 973 SecondaryCursor cursor = null; 974 OperationStatus commitStatus = null; 975 try { 976 locker = LockerFactory.getReadableLocker( 977 this, txn, cursorConfig.getReadCommitted()); 978 cursor = new SecondaryCursor(this, locker, cursorConfig); 979 commitStatus = 980 cursor.search(key, pKey, data, lockMode, SearchMode.BOTH); 981 return commitStatus; 982 } catch (Error E) { 983 DbInternal.getEnvironmentImpl(envHandle).invalidate(E); 984 throw E; 985 } finally { 986 if (cursor != null) { 987 cursor.close(); 988 } 989 990 if (locker != null) { 991 locker.operationEnd(commitStatus); 992 } 993 } 994 } 995 996 /** 997 * This operation is not allowed on a secondary database. {@link 998 * UnsupportedOperationException} will always be thrown by this method. 999 * The corresponding method on the primary database should be used instead. 1000 */ 1001 @Override put(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data)1002 public OperationStatus put(final Transaction txn, 1003 final DatabaseEntry key, 1004 final DatabaseEntry data) 1005 throws UnsupportedOperationException { 1006 1007 throw notAllowedException(); 1008 } 1009 1010 /** 1011 * This operation is not allowed on a secondary database. {@link 1012 * UnsupportedOperationException} will always be thrown by this method. 1013 * The corresponding method on the primary database should be used instead. 1014 */ 1015 @Override putNoOverwrite(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data)1016 public OperationStatus putNoOverwrite(final Transaction txn, 1017 final DatabaseEntry key, 1018 final DatabaseEntry data) 1019 throws UnsupportedOperationException { 1020 1021 throw notAllowedException(); 1022 } 1023 1024 /** 1025 * This operation is not allowed on a secondary database. {@link 1026 * UnsupportedOperationException} will always be thrown by this method. 1027 * The corresponding method on the primary database should be used instead. 1028 */ 1029 @Override putNoDupData(final Transaction txn, final DatabaseEntry key, final DatabaseEntry data)1030 public OperationStatus putNoDupData(final Transaction txn, 1031 final DatabaseEntry key, 1032 final DatabaseEntry data) 1033 throws UnsupportedOperationException { 1034 1035 throw notAllowedException(); 1036 } 1037 1038 /** 1039 * This operation is not allowed on a secondary database. {@link 1040 * UnsupportedOperationException} will always be thrown by this method. 1041 * The corresponding method on the primary database should be used instead. 1042 */ 1043 @Override join(final Cursor[] cursors, final JoinConfig config)1044 public JoinCursor join(final Cursor[] cursors, final JoinConfig config) 1045 throws UnsupportedOperationException { 1046 1047 throw notAllowedException(); 1048 } 1049 1050 /** 1051 * Updates a single secondary when a put() or delete() is performed on the 1052 * primary. 1053 * <p> 1054 * For an insert, newData will be non-null and oldData will be null. 1055 * <p> 1056 * For an update, newData will be non-null and oldData will be non-null. 1057 * <p> 1058 * For a delete, newData will be null and oldData may be null or non-null 1059 * depending on whether its need by the key creator/extractor. 1060 * 1061 * @param locker the internal locker. 1062 * 1063 * @param cursor secondary cursor to use, or null if this method should 1064 * open and close a cursor if one is needed. 1065 * 1066 * @param priKey the primary key. 1067 * 1068 * @param oldData the primary data before the change, or null if the record 1069 * did not previously exist. 1070 * 1071 * @param newData the primary data after the change, or null if the record 1072 * has been deleted. 1073 */ updateSecondary(final Locker locker, Cursor cursor, final DatabaseEntry priKey, final DatabaseEntry oldData, final DatabaseEntry newData)1074 void updateSecondary(final Locker locker, 1075 Cursor cursor, 1076 final DatabaseEntry priKey, 1077 final DatabaseEntry oldData, 1078 final DatabaseEntry newData) 1079 throws DatabaseException { 1080 1081 SecondaryKeyCreator keyCreator = secondaryConfig.getKeyCreator(); 1082 if (keyCreator != null) { 1083 /* Each primary record may have a single secondary key. */ 1084 assert secondaryConfig.getMultiKeyCreator() == null; 1085 1086 /* Get old and new secondary keys. */ 1087 DatabaseEntry oldSecKey = null; 1088 if (oldData != null || newData == null) { 1089 oldSecKey = new DatabaseEntry(); 1090 if (!keyCreator.createSecondaryKey(this, priKey, oldData, 1091 oldSecKey)) { 1092 oldSecKey = null; 1093 } 1094 } 1095 DatabaseEntry newSecKey = null; 1096 if (newData != null) { 1097 newSecKey = new DatabaseEntry(); 1098 if (!keyCreator.createSecondaryKey(this, priKey, newData, 1099 newSecKey)) { 1100 newSecKey = null; 1101 } 1102 } 1103 1104 /* Update secondary if old and new keys are unequal. */ 1105 if ((oldSecKey != null && !oldSecKey.equals(newSecKey)) || 1106 (newSecKey != null && !newSecKey.equals(oldSecKey))) { 1107 1108 boolean localCursor = (cursor == null); 1109 if (localCursor) { 1110 cursor = new Cursor(this, locker, null); 1111 } 1112 try { 1113 /* Delete the old key. */ 1114 if (oldSecKey != null) { 1115 deleteKey(cursor, priKey, oldSecKey); 1116 } 1117 /* Insert the new key. */ 1118 if (newSecKey != null) { 1119 insertKey(locker, cursor, priKey, newSecKey); 1120 } 1121 } finally { 1122 if (localCursor && cursor != null) { 1123 cursor.close(); 1124 } 1125 } 1126 } 1127 } else { 1128 /* Each primary record may have multiple secondary keys. */ 1129 SecondaryMultiKeyCreator multiKeyCreator = 1130 secondaryConfig.getMultiKeyCreator(); 1131 if (multiKeyCreator == null) { 1132 throw new IllegalArgumentException( 1133 "SecondaryConfig.getKeyCreator()/getMultiKeyCreator()" + 1134 " may be null only if the primary database is read-only"); 1135 } 1136 1137 /* Get old and new secondary keys. */ 1138 Set<DatabaseEntry> oldKeys = EMPTY_SET; 1139 Set<DatabaseEntry> newKeys = EMPTY_SET; 1140 if (oldData != null || newData == null) { 1141 oldKeys = new HashSet<DatabaseEntry>(); 1142 multiKeyCreator.createSecondaryKeys(this, priKey, 1143 oldData, oldKeys); 1144 } 1145 if (newData != null) { 1146 newKeys = new HashSet<DatabaseEntry>(); 1147 multiKeyCreator.createSecondaryKeys(this, priKey, 1148 newData, newKeys); 1149 } 1150 1151 /* Update the secondary if there is a difference. */ 1152 if (!oldKeys.equals(newKeys)) { 1153 1154 boolean localCursor = (cursor == null); 1155 if (localCursor) { 1156 cursor = new Cursor(this, locker, null); 1157 } 1158 try { 1159 /* Delete old keys that are no longer present. */ 1160 Set<DatabaseEntry> oldKeysCopy = oldKeys; 1161 if (oldKeys != EMPTY_SET) { 1162 oldKeysCopy = new HashSet<DatabaseEntry>(oldKeys); 1163 oldKeys.removeAll(newKeys); 1164 for (Iterator<DatabaseEntry> i = oldKeys.iterator(); 1165 i.hasNext();) { 1166 DatabaseEntry oldKey = i.next(); 1167 deleteKey(cursor, priKey, oldKey); 1168 } 1169 } 1170 1171 /* Insert new keys that were not present before. */ 1172 if (newKeys != EMPTY_SET) { 1173 newKeys.removeAll(oldKeysCopy); 1174 for (Iterator<DatabaseEntry> i = newKeys.iterator(); 1175 i.hasNext();) { 1176 DatabaseEntry newKey = i.next(); 1177 insertKey(locker, cursor, priKey, newKey); 1178 } 1179 } 1180 } finally { 1181 if (localCursor && cursor != null) { 1182 cursor.close(); 1183 } 1184 } 1185 } 1186 } 1187 } 1188 1189 /** 1190 * Deletes an old secondary key. 1191 */ deleteKey(final Cursor cursor, final DatabaseEntry priKey, final DatabaseEntry oldSecKey)1192 private void deleteKey(final Cursor cursor, 1193 final DatabaseEntry priKey, 1194 final DatabaseEntry oldSecKey) 1195 throws DatabaseException { 1196 1197 OperationStatus status = 1198 cursor.search(oldSecKey, priKey, 1199 LockMode.RMW, 1200 SearchMode.BOTH); 1201 if (status == OperationStatus.SUCCESS) { 1202 cursor.deleteInternal(getDatabaseImpl().getRepContext()); 1203 return; 1204 } 1205 if (isFullyPopulated) { 1206 throw new SecondaryIntegrityException( 1207 cursor.getCursorImpl().getLocker(), 1208 "Secondary is corrupt: the primary record contains a key " + 1209 "that is not present in the secondary", 1210 getDebugName(), oldSecKey, priKey); 1211 } 1212 } 1213 1214 /** 1215 * Inserts a new secondary key. 1216 */ insertKey(final Locker locker, final Cursor cursor, final DatabaseEntry priKey, final DatabaseEntry newSecKey)1217 private void insertKey(final Locker locker, 1218 final Cursor cursor, 1219 final DatabaseEntry priKey, 1220 final DatabaseEntry newSecKey) 1221 throws DatabaseException { 1222 1223 /* Check for the existence of a foreign key. */ 1224 Database foreignDb = 1225 secondaryConfig.getForeignKeyDatabase(); 1226 if (foreignDb != null) { 1227 Cursor foreignCursor = null; 1228 try { 1229 foreignCursor = new Cursor(foreignDb, locker, 1230 null); 1231 DatabaseEntry tmpData = new DatabaseEntry(); 1232 OperationStatus status = 1233 foreignCursor.search(newSecKey, tmpData, 1234 LockMode.DEFAULT, 1235 SearchMode.SET); 1236 if (status != OperationStatus.SUCCESS) { 1237 throw new ForeignConstraintException( 1238 locker, 1239 "Secondary " + getDebugName() + 1240 " foreign key not allowed: it is not" + 1241 " present in the foreign database " + 1242 foreignDb.getDebugName(), getDebugName(), 1243 newSecKey, priKey); 1244 } 1245 } finally { 1246 if (foreignCursor != null) { 1247 foreignCursor.close(); 1248 } 1249 } 1250 } 1251 1252 /* Insert the new key. */ 1253 if (configuration.getSortedDuplicates()) { 1254 OperationStatus status = cursor.putInternal(newSecKey, priKey, 1255 PutMode.NO_DUP_DATA); 1256 if (status != OperationStatus.SUCCESS && isFullyPopulated) { 1257 throw new SecondaryIntegrityException( 1258 locker, "Secondary/primary record already present", 1259 getDebugName(), newSecKey, priKey); 1260 } 1261 } else { 1262 OperationStatus status = cursor.putInternal(newSecKey, priKey, 1263 PutMode.NO_OVERWRITE); 1264 if (status != OperationStatus.SUCCESS && isFullyPopulated) { 1265 throw new UniqueConstraintException( 1266 locker, "Unique secondary key is already present", 1267 getDebugName(), newSecKey, priKey); 1268 } 1269 } 1270 } 1271 1272 /** 1273 * Called when a record in the foreign database is deleted. 1274 * 1275 * @param secKey is the primary key of the foreign database, which is the 1276 * secondary key (ordinary key) of this secondary database. 1277 */ onForeignKeyDelete(final Locker locker, final DatabaseEntry secKey)1278 void onForeignKeyDelete(final Locker locker, final DatabaseEntry secKey) 1279 throws DatabaseException { 1280 1281 final ForeignKeyDeleteAction deleteAction = 1282 secondaryConfig.getForeignKeyDeleteAction(); 1283 1284 /* Use RMW if we're going to be deleting the secondary records. */ 1285 final LockMode lockMode = 1286 (deleteAction == ForeignKeyDeleteAction.ABORT) ? 1287 LockMode.DEFAULT : 1288 LockMode.RMW; 1289 1290 /* 1291 * Use the deleted foreign primary key to read the data of this 1292 * database, which is the associated primary's key. 1293 */ 1294 final Cursor cursor = new Cursor(this, locker, null); 1295 try { 1296 final DatabaseEntry priKey = new DatabaseEntry(); 1297 OperationStatus status = 1298 cursor.search(secKey, priKey, lockMode, SearchMode.SET); 1299 while (status == OperationStatus.SUCCESS) { 1300 1301 if (deleteAction == ForeignKeyDeleteAction.ABORT) { 1302 1303 /* 1304 * ABORT - throw an exception to cause the user to abort 1305 * the transaction. 1306 */ 1307 throw new DeleteConstraintException( 1308 locker, "Secondary refers to a deleted foreign key", 1309 getDebugName(), secKey, priKey); 1310 1311 } else if (deleteAction == ForeignKeyDeleteAction.CASCADE) { 1312 1313 /* 1314 * CASCADE - delete the associated primary record. 1315 */ 1316 final Database primaryDb = getPrimary(priKey); 1317 if (primaryDb != null) { 1318 status = primaryDb.deleteInternal(locker, priKey); 1319 if (status != OperationStatus.SUCCESS) { 1320 throw secondaryRefersToMissingPrimaryKey( 1321 locker, secKey, priKey); 1322 } 1323 } 1324 1325 } else if (deleteAction == ForeignKeyDeleteAction.NULLIFY) { 1326 1327 /* 1328 * NULLIFY - set the secondary key to null in the 1329 * associated primary record. 1330 */ 1331 final Database primaryDb = getPrimary(priKey); 1332 if (primaryDb != null) { 1333 final Cursor priCursor = 1334 new Cursor(primaryDb, locker, null); 1335 try { 1336 final DatabaseEntry data = new DatabaseEntry(); 1337 status = priCursor.search( 1338 priKey, data, LockMode.RMW, SearchMode.SET); 1339 if (status != OperationStatus.SUCCESS) { 1340 throw secondaryRefersToMissingPrimaryKey( 1341 locker, secKey, priKey); 1342 } 1343 final ForeignMultiKeyNullifier multiNullifier = 1344 secondaryConfig.getForeignMultiKeyNullifier(); 1345 if (multiNullifier != null) { 1346 if (multiNullifier.nullifyForeignKey( 1347 this, priKey, data, secKey)) { 1348 priCursor.putCurrent(data); 1349 } 1350 } else { 1351 final ForeignKeyNullifier nullifier = 1352 secondaryConfig.getForeignKeyNullifier(); 1353 if (nullifier.nullifyForeignKey(this, data)) { 1354 priCursor.putCurrent(data); 1355 } 1356 } 1357 } finally { 1358 priCursor.close(); 1359 } 1360 } 1361 } else { 1362 /* Should never occur. */ 1363 throw EnvironmentFailureException.unexpectedState(); 1364 } 1365 1366 status = cursor.retrieveNext(secKey, priKey, LockMode.DEFAULT, 1367 GetMode.NEXT_DUP); 1368 } 1369 } finally { 1370 cursor.close(); 1371 } 1372 } 1373 1374 /** 1375 * If either ImmutableSecondaryKey or ExtractFromPrimaryKeyOnly is 1376 * configured, an update cannot change a secondary key. 1377 * ImmutableSecondaryKey is a guarantee from the user meaning just that, 1378 * and ExtractFromPrimaryKeyOnly also implies the secondary key cannot 1379 * change because it is derived from the primary key which is immutable 1380 * (like any other key). 1381 */ updateMayChangeSecondary()1382 boolean updateMayChangeSecondary() { 1383 return !secondaryConfig.getImmutableSecondaryKey() && 1384 !secondaryConfig.getExtractFromPrimaryKeyOnly(); 1385 } 1386 1387 /** 1388 * When false is returned, this allows optimizing for the case where a 1389 * primary update operation can update secondaries without reading the 1390 * primary data. 1391 */ 1392 static boolean needOldDataForUpdate(final Collection<SecondaryDatabase> secondaries)1393 needOldDataForUpdate(final Collection<SecondaryDatabase> secondaries) { 1394 if (secondaries == null) { 1395 return false; 1396 } 1397 for (final SecondaryDatabase secDb : secondaries) { 1398 if (secDb.updateMayChangeSecondary()) { 1399 return true; 1400 } 1401 } 1402 return false; 1403 } 1404 1405 /** 1406 * When false is returned, this allows optimizing for the case where a 1407 * primary delete operation can update secondaries without reading the 1408 * primary data. 1409 */ 1410 static boolean needOldDataForDelete(final Collection<SecondaryDatabase> secondaries)1411 needOldDataForDelete(final Collection<SecondaryDatabase> secondaries) { 1412 if (secondaries == null) { 1413 return false; 1414 } 1415 for (final SecondaryDatabase secDb : secondaries) { 1416 if (!secDb.secondaryConfig.getExtractFromPrimaryKeyOnly()) { 1417 return true; 1418 } 1419 } 1420 return false; 1421 } 1422 1423 /* A secondary DB has no secondaries of its own, by definition. */ 1424 @Override hasSecondaryOrForeignKeyAssociations()1425 boolean hasSecondaryOrForeignKeyAssociations() { 1426 return false; 1427 } 1428 1429 /** 1430 * Utility to call SecondaryAssociation.getPrimary. 1431 * 1432 * Handles exceptions and does an important debugging check that can't be 1433 * done at database open time: ensures that the same SecondaryAssociation 1434 * instance is used for all associated DBs. 1435 * <p> 1436 * Returns null if getPrimary returns null, so the caller must handle this 1437 * possibility. Null normally means that a secondary read operation can 1438 * skip the record. 1439 */ getPrimary(DatabaseEntry priKey)1440 Database getPrimary(DatabaseEntry priKey) { 1441 final Database priDb; 1442 try { 1443 priDb = secAssoc.getPrimary(priKey); 1444 } catch (RuntimeException e) { 1445 throw EnvironmentFailureException.unexpectedException( 1446 "Exception from SecondaryAssociation.getPrimary", e); 1447 } 1448 if (priDb == null) { 1449 return null; 1450 } 1451 if (priDb.secAssoc != secAssoc) { 1452 throw new IllegalArgumentException( 1453 "Primary and secondary have different SecondaryAssociation " + 1454 "instances. Remember to configure the SecondaryAssociation " + 1455 "on the primary database."); 1456 } 1457 return priDb; 1458 } 1459 checkReadable(final String msg)1460 private void checkReadable(final String msg) { 1461 checkOpen(msg); 1462 if (!isFullyPopulated) { 1463 throw new IllegalStateException( 1464 msg + " Incremental population is currently enabled."); 1465 } 1466 } 1467 setupThroughputStats(EnvironmentImpl envImpl)1468 private void setupThroughputStats(EnvironmentImpl envImpl) { 1469 getStat = envImpl.getThroughputStat(THROUGHPUT_SECONDARYDB_GET); 1470 deleteStat = envImpl.getThroughputStat(THROUGHPUT_SECONDARYDB_DELETE); 1471 getSearchBothStat = 1472 envImpl.getThroughputStat(THROUGHPUT_SECONDARYDB_GETSEARCHBOTH); 1473 } 1474 notAllowedException()1475 static UnsupportedOperationException notAllowedException() { 1476 1477 return new UnsupportedOperationException( 1478 "Operation not allowed on a secondary"); 1479 } 1480 1481 /** 1482 * Send trace messages to the java.util.logger. Don't rely on the logger 1483 * alone to conditionalize whether we send this message, we don't even want 1484 * to construct the message if the level is not enabled. 1485 */ trace(final Level level, final String methodName)1486 void trace(final Level level, final String methodName) { 1487 if (logger.isLoggable(level)) { 1488 StringBuilder sb = new StringBuilder(); 1489 sb.append(methodName); 1490 sb.append(" name=").append(getDebugName()); 1491 sb.append(" primary=").append(primaryDatabase.getDebugName()); 1492 1493 LoggerUtils.logMsg( 1494 logger, envHandle.getEnvironmentImpl(), level, sb.toString()); 1495 } 1496 } 1497 } 1498