Cayenne has its own simple transaction API centered around org.apache.cayenne.access.Transaction class. Its goal is to ensure consistency of the DataContext database operations. It works either as a standalone mechanism, or in conjunction with another transaction framework, such as JTA or Spring. To switch between the two modes of operation, use "Container-Managed Transactions" checkbox in the DataDomain editing panel in CayenneModeler:

If this box is unchecked (default), standalone mode is used and Cayenne will take care of transactional resources management on its own. If it is checked, Cayenne won't commit or rollback transactional resources, relying on the external transaction manager to do that.

In both cases Transaction API works implicitly behind the scenes, so the application doesn't need to interact with it directly. In that Cayenne Transactions are fully declarative.

How Transactions Work

Similar to the Java EE approach, Cayenne transactions are bound to the current thread for the duration of the execution. For instance this is how Cayenne does an internal check of whether there is a transaction in progress:

import org.apache.cayenne.access.Transaction;
...
Transaction currentTx = Transaction.getThreadTransaction();
if(currentTx != null) {
  // transaction in process...
}

When a Transaction is created inside Cayenne, it is immediately bound to the thread:

Transaction tx = ...;
Transaction.bindThreadTransaction(tx);

Now let's revisit the flow of a typical operation that requires a transaction:

  1. A DataContext sends a query or a commit request to the underlying org.apache.cayenne.DataChannel.
  2. The request travels the chain of DataChannels until it reaches one that is a org.apache.cayenne.access.DataDomain.
  3. DataDomain analyzes context request and dispatches data queries to one or more org.apache.cayenne.access.DataNodes.
  4. Each DataNode opens a JDBC Connection and executes queries.

Transactions come into play in step 3. DataDomain checks whether there is an existing Transaction in process and if not - creates and starts a new one (standalone or container, depending on the preconfigured type). In that Cayenne transaction policy is similar to Java EE "REQUIRE" policy.

Later in step 4 DataNodes will attach any of the Connections they obtains to the ongoing transaction:

import java.sql.Connection;
...
Connection connection = ...
currentTx.addConnection("someKey", connection);

Transaction Lifecycle Callbacks: TransactionDelegate

If you want to execute some custom code, such as Cayenne queries or raw JDBC queries at certain points in transaction lifecycle, you need to implement a org.apache.cayenne.access.TransactionDelegate callback interface:

ublic class MyTxCallback implements TransactionDelegate {

    public boolean willCommit(Transaction transaction) {
        // run extra query before transaction is committed

        // The results of it will be committed or rolled back together with the current Transaction. 

        DataContext context = DataContext.getThreadDataContext();
        context.performGenericQuery(new SQLTemplate(X.class, "..."));

        // return true, letting Cayenne know it should continue with commit
        return true;
    }

    public boolean willMarkAsRollbackOnly(Transaction transaction) {
        return true;
    }

    public boolean willRollback(Transaction transaction) {
        return true;
    }

    public void didCommit(Transaction transaction) {
    }

    public void didRollback(Transaction transaction) {
    }

    public boolean willAddConnection(Transaction transaction, Connection connection) {
        return true;
    }
}

Then an instance can be registered with the DataDomain.

DataDomain domain = Configuration.getSharedConfiguration().getDomain();
domain.setTransactionDelegate(new MyTxCallback());

The delegate is shared by all DataContexts.

User-Defined Transaction Scope

If the application needs to define its own transactional scope (e.g. wrap more than one DataContext.commitChanges() in a single database transaction), an explict org.apache.cayenne.access.Transaction can be started. It will serve as a simple substitute for the JTA transactions (of course JTA UserTransaction can be used instead if desired).

If the user code starts a Transaction, it must explicitly invoke "commit/rollback" methods and unbind the Transaction from the current thread when it is finished. Failure to do that may result in connection leaks. Of course if Cayenne starts an implicit transaction, it does the cleanup internally on its own.

Below is an example of user-controlled Transaction code. First it obtains a new transaction from the DataDomain (alternatively users can create Transaction subclasses of their own):

DataDomain domain = Configuration.getSharedConfiguration().getDomain();
Transaction tx = domain.createTransaction();

As we must finish transaction regardless of the outcome, wrap the rest of the code in try/catch/finally. Don't foget to bind/unbind the transaction, so that Cayenne stack is aware of it:

Transaction.bindThreadTransaction(tx);

try {
    // do something...
    ....
    // if no failures, commit
    tx.commit();
}
catch (Exception ex) {
    tx.setRollbackOnly();
}
finally {
    Transaction.bindThreadTransaction(null);
 
    if (tx.getStatus() == Transaction.STATUS_MARKED_ROLLEDBACK) {
        try {
           tx.rollback();
        }
        catch (Exception rollbackEx) {
        }
    }
}