7.3 Transactions

Accesses 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 Transactions

Transactions 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.

Atomic

Within a transaction, either all or none of the changes made to instances are propagated to the datastore.

Consistent

A change to a value in an instance is consistent with changes to any other values in the same instance and all other instances in the same transaction.

Isolated

Changes to instances are isolated from changes made in other transactions.

Durable

Changes to persistent instances survive the end of the Java Virtual Machine context in which they are made.

7.3.2 Transactions and Locking in the Datastore

Instead 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 levels

Isolation 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:

Level 0 (Dirty Read; Read Uncommitted)

Transactions might read data from transactions that have not yet committed; therefore, there is no guarantee of consistency, although concurrency is highest.

Level 1 (Cursor Stability; Read Committed)

Transactions will read data only from committed transactions. Updates in one transaction will not overwrite updates from another transaction. Reading the same data twice might result in different data the second time.

Level 2 (Repeatable Read)

Updates in one transaction will not overwrite updates from another transaction. Reading the same data twice is guaranteed to return the same results each time, but queries might return different results due to inserted data between the queries (sometimes called phantom reads).

Level 3 (Serializable; Isolated)

Updates in one transaction will not overwrite updates from another transaction. Reading the same data twice is guaranteed to return the same results each time. Reading data prevents other transactions from updating the data. Queries return the same results if they are executed twice.

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 datastore

To 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.

Table 7-3. Lock-compatibility matrix
 

Lock Requested

Lock Held

 

Exclusive

Shared

Exclusive

No

No

Shared

No

OK

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 JDO

Transactions 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:

Nontransactional access

The ability to access instances from the datastore without having a transaction in the datastore in progress is an optional feature in JDO. The NontransactionalRead and NontransactionalWrite features determine whether an application can read and modify instances in memory outside of a transaction. But any modifications you make to instances in memory outside of a transaction cannot be propagated directly to the datastore.

Datastore transaction

When you use a datastore transaction, all the operations you perform on persistent data are done within a single transaction in the datastore. This means that between the first data access in the transaction and the commit of that transaction, a single active transaction is used in the datastore. Datastore transactions are supported in all JDO implementations.

Optimistic transaction

When you use an optimistic transaction, operations on instances in memory outside a JDO transaction or before transaction commit are implemented by the JDO implementation with a series of short local transactions in the datastore. If an optimistic transaction has updates that need to be propagated to the datastore, when you commit the optimistic transaction the JDO implementation uses an underlying transaction in the datastore to verify that the proposed changes do not conflict with updates that may have been committed by other, concurrent transactions. Optimistic transactions are an optional feature in JDO.

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 Transaction

You 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 Type

PersistenceManagerFactory 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 Demarcation

Your 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 completion

The 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 processing

Transaction.commit( ) performs the following operations:

  • It makes a call to beforeCompletion( ) on the Synchronization instance registered with the Transaction (if there is one).

  • It flushes (propagates) modified persistent instances to the datastore.

  • It notifies the underlying datastore to commit the transaction.

  • It transitions the states of persistent instances according to the JDO instance lifecycle specification; this is covered in Chapter 11 and Appendix A.

  • It makes a call to afterCompletion( ) for the Synchronization instance registered with the Transaction (if there is one), passing the results of the datastore commit operation.

Additional steps are taken with optimistic transactions, which are covered in Chapter 15.

7.3.6.3 Rollback processing

Transaction.rollback( ) performs the following operations:

  • It rolls back changes made in this transaction in the datastore.

  • It transitions the states of persistent instances according to the JDO instance lifecycle specification.

  • It makes a call to afterCompletion( ) for the Synchronization instance registered with the Transaction (if there is one).

7.3.7 Restoring Values on Rollback

The 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 Active

Call 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.