[ Team LiB ] |
7.3 TransactionsAccesses and updates to persistent instances are performed in the context of a transaction. The JDO Transaction interface provides the methods you use to begin and commit a transaction. It also has methods to manage the settings of transaction flags. It is similar in functionality to a javax.transaction.UserTransaction. Both interfaces have begin( ), commit( ), and rollback( ) methods with the same semantics and behavior. A one-to-one relationship exists between a PersistenceManager and its associated Transaction instance. A PersistenceManager instance represents a single view of persistent data, including persistent instances that have been cached across multiple serial transactions. If your application needs multiple concurrent transactions, each transaction will have its own Transaction instance and associated PersistenceManager instance. You call methods in the JDO Transaction interface to perform operations on a transaction. The underlying datastore has its own representation for a transaction, with its own operations and interfaces. JDO supports a type of transaction referred to as a datastore transaction. This is not the transaction in the underlying datastore. We refer to the transaction at the datastore level as the transaction in the datastore, to distinguish it from the JDO datastore transaction. 7.3.1 Properties of TransactionsTransactions have a set of common properties that are referred to as the ACID (Atomic, Consistent, Isolated, Durable) properties of a transaction. JDO transactions support these properties.
7.3.2 Transactions and Locking in the DatastoreInstead of attempting to redefine the semantics of datastore transactions, JDO defines operations on persistent instances that use the underlying datastore operations. In order to understand the differences between the JDO transaction modes, it is useful to understand how transaction guarantees are implemented in datastores. Durability is mainly a datastore-implementation detail, in which changes are guaranteed to be persistent in the face of various failure modes of hardware, software, and the computing environment. Atomicity means that the datastore manages the changes associated with each instance, such that at commit time all of the changes to each instance are applied, and a failure to apply any change invalidates the entire set of changes. Additionally, all changes are made to the instances, or none are made. Consistency is a responsibility shared between the application and the datastore. It applies to all of the instances that were accessed during a transaction, whether the access was for read or write. Consistency requires that if multiple instances are related in some way, then changes in one of the instances are made consistently with changes in other instances. 7.3.2.1 Transaction-isolation levelsIsolation is the most complex of the transaction guarantees, and datastore vendors adopt many strategies to achieve it. Isolation is so complex because there is a significant performance penalty associated with strict isolation, which requires that transactions execute as if they operated completely independent of each another. Therefore, datastores provide varying levels of isolation with different performance characteristics, allowing applications to choose a level of isolation that provides an appropriate balance between consistency and performance. The isolation levels can be characterized as follows:
It is significant to note here that JDO does not mandate any specific isolation level; decisions regarding which isolation level to use, whether to expose the isolation level to applications, and how to expose the level are made by the JDO implementation. 7.3.2.2 Locking in the datastoreTo implement level 1, level 2, and level 3 transaction isolation, datastores often implement isolation of transactions in the datastore using locking. Locking is typically implemented by associating a lock instance with each datastore operation. The lock instance contains the transaction identifier, the lock mode, and the datastore instance. Locks are stored in a lock table. When an operation is performed to read, write, insert, or delete a datastore instance, the datastore creates a lock instance for the current operation and tries to add the lock to the lock table. The lock addition fails if an incompatible lock already exists in the lock table. Depending on the datastore implementation, the incompatibility might result in the transaction waiting for some timeout period, or immediately failing. During the timeout period, the transaction with the conflicting lock might commit or roll back, thereby allowing the waiting transaction to proceed. Lock compatibilities are typically implemented using a lock-compatibility matrix, a simplified version of which is illustrated in Table 7-3. Most datastores implement a much more sophisticated version of this matrix.
Read requests use shared locks, while insert, update, and delete requests use exclusive locks. Thus, multiple transactions can read the same datastore instances without conflict, but if a transaction is reading an instance, that instance cannot be updated or deleted by another transaction until all transactions holding the shared lock complete. Similarly, if a transaction deletes an instance, no other transaction can access that instance until the transaction holding the exclusive lock on the deleted instance completes. The effect of locking with long transactions is significant. While the long transaction is active, all other transactions that attempt to access instances used in it are subject to the compatibility rules of the lock table. Even if the long transaction only holds read locks, other transactions that attempt to update the same instances will wait for completion of the long transaction. This is a simplified view of datastore locks; for a more detailed understanding of database locking, you should consult your JDO implementation's documentation. 7.3.3 Types of Transactions in JDOTransactions are a fundamental aspect of JDO. All changes to instances that should be reflected in the datastore are performed in the context of a transaction. JDO supports three transaction-management strategies:
If you anticipate that you will primarily have concurrent transactions attempting to access and modify the same instances, resulting in lock conflicts, then you should use datastore transactions. If you anticipate that lock conflicts will not occur, you should consider optimistic transactions. In these situations, optimistic transactions place fewer demands on the datastore, because locks are not maintained throughout the duration of the optimistic transaction. We continue to use datastore transactions until we cover nontransactional access in Chapter 14 and optimistic transactions in Chapter 15. 7.3.4 Acquiring a TransactionYou can access the Transaction instance associated with a PersistenceManager by calling the following PersistenceManager method: Transaction currentTransaction( ); All calls you make to currentTransaction( ) for a given PersistenceManager instance return the same Transaction instance until you have closed the PersistenceManager instance with a call to close( ). You can use the same Transaction instance to execute multiple serial transactions. If you want to execute multiple parallel transactions in a JVM, then you can use multiple PersistenceManager instances. You can call the following Transaction method to access its associated PersistenceManager instance: PersistenceManager getPersistenceManager( ); 7.3.5 Setting the Transaction TypePersistenceManagerFactory and Transaction instances each maintain a flag that indicates whether to use a datastore or optimistic transaction. If an implementation does not support optimistic transactions, these PersistenceManagerFactory and Transaction flags will always be false. If the application attempts to set the flag to true, a JDOUnsupportedOptionException is thrown. If the implementation supports optimistic transactions, whether the default value is true or false is the implementation's choice. You can initialize the Optimistic flag when the PersistenceManagerFactory instance is constructed. You can also get and set the Optimistic flag in the PersistenceManagerFactory and Transaction instances with the following methods: void setOptimistic(boolean flag); boolean getOptimistic( ); Calling setOptimistic( ) with a false parameter value indicates that datastore transactions should be used, and calling it with a true value indicates that optimistic transactions should be used. You cannot call these methods when a Transaction instance is active (i.e., after you call begin( ) and before you call commit( ) or rollback( )). 7.3.6 Transaction DemarcationYour application is responsible for transaction demarcation in a nonmanaged environment. In the managed environment of an application server, transaction demarcation is performed for you automatically. One exception is when you use bean-managed transactions. The following discussion applies only when you are running in a nonmanaged environment or using bean-managed transactions in an EJB environment. Managed environments are covered in Chapter 16 and Chapter 17. If you call these transaction-demarcation methods in a managed environment with container-managed transactions, a JDOUserException is thrown. You call the following Transaction method to begin a transaction: void begin( ); You then call commit( ) or rollback( ) to complete the transaction: void commit( ); void rollback( ); Calling commit( ) indicates that you want all the updates that were made in the transaction to be propagated to the datastore. Calling rollback( ) indicates that none of the changes should be made in the datastore. The following code illustrates the use of begin( ), commit( ), and rollback( ). It also shows that you can use the same Transaction instance to execute multiple transactions serially. In addition, it demonstrates that repeated calls to currentTransaction( ) for a PersistenceManager instance return the same Transaction instance. // assume pmf variable is initialized to a PersistenceManagerFactory
PersistenceManager pm = pmf.getPersistenceManager( );
Transaction tx = pm.currentTransaction( );
try {
tx.begin( );
// place application's access of database here
tx.commit( );
} catch (JDOException jdoException) {
tx.rollback( );
System.err.println("JDOException thrown:");
jdoException.printStackTrace( );
}
// ...
try {
tx.begin( );
// place application's access of database here
tx.commit( );
} catch (JDOException jdoException) {
tx.rollback( );
System.err.println("JDOException thrown:");
jdoException.printStackTrace( );
}
// ...
Transaction trans = pm.currentTransaction( ); // trans and tx reference same instance [1]
try {
trans.begin( );
// place application's access of database here
trans.commit( );
} catch (JDOException jdoException) {
trans.rollback( );
System.err.println("JDOException thrown:");
jdoException.printStackTrace( );
}
We call currentTransaction( ) on line [1] to get a Transaction instance. We do this here only to point out that the Transaction instance returned on line [1] is the same instance referenced by the tx variable. All calls you make to currentTransaction( ) for a given PersistenceManager return the same Transaction instance. 7.3.6.1 Notification of transaction completionThe javax.transaction package has an interface, called Synchronization, that is used to notify an application when a transaction-completion process is about to begin. And when the completion process has finished, it provides a status indicating whether the transaction committed successfully. The Synchronization interface has the following two methods: void beforeCompletion( ); void afterCompletion(int status); The beforeCompletion( ) method is called prior to the start of the transaction-commit process; it is not called during rollback. The afterCompletion( ) method is called after the transaction has been committed or rolled back. The status parameter passed to afterCompletion( ) indicates whether the transaction committed or rolled back successfully. Its value is either STATUS_COMMITTED or STATUS_ROLLEDBACK; these are defined in the javax.transaction.Status interface. These two methods provide an application with some control over the environment in which the transaction completion executes (for example, to validate the state of instances in the cache before transaction completion) and the ability to perform some functionality once the transaction completes. JDO supports the Synchronization interface. To use it, you must declare a class that implements it. You can register one instance of the class with the Transaction instance using the following method: void setSynchronization(javax.transaction.Synchronization sync); Calling this method replaces any Synchronization instance already registered. If you need more than one instance to receive notification, then your Synchronization class is responsible for managing this, forwarding callbacks as necessary. If you pass a null to the method, this indicates that no instance should be notified. If you call setSynchronization( ) during commit processing (within beforeCompletion( ) or afterCompletion( )), a JDOUserException is thrown. You can retrieve the currently registered Synchronization instance by calling the following Transaction method: javax.transaction.Synchronization getSynchronization( ); 7.3.6.2 Commit processingTransaction.commit( ) performs the following operations:
Additional steps are taken with optimistic transactions, which are covered in Chapter 15. 7.3.6.3 Rollback processingTransaction.rollback( ) performs the following operations:
7.3.7 Restoring Values on RollbackThe RestoreValues feature controls the behavior that occurs at transaction rollback. If it is true, persistent and transactional instances are restored to their state as of the beginning of the transaction; if it is false, the state of instances is not restored. If RestoreValues is true, the values of fields of instances made persistent during the transaction are restored to their state as of the call to makePersistent( ). If RestoreValues is false, they keep the values they had when rollback( ) was called. You call the following Transaction methods to get and set the RestoreValues flag: boolean getRestoreValues( ); void setRestoreValues(boolean flag); The value of the flag parameter replaces the currently active RestoreValues setting. You can call this method only when the transaction is not active; otherwise, a JDOUserException is thrown. 7.3.8 Determining Whether a Transaction Is ActiveCall the following Transaction method to determine whether a transaction is active: boolean isActive( ); It returns true after the transaction has been started and until Synchronization.afterCompletion( ) has been called. |
[ Team LiB ] |