Skip to main content

Transaction Control

Obtaining a DAO from DAOFactory and calling insert does register the record. Because that alone works, implementation can proceed without transactions ever being made explicit. The unit of commit is then left to the platform, which leaves room for only one of two consecutive table updates to survive.

In im_mirage, database operations run inside a SessionTemplate.

Boundaries with SessionTemplate​

Passing a callback to SessionTemplate.execute makes its contents the range of the transaction.

SessionTemplate.execute(new SessionCallback<Void, RuntimeException>() {
@Override
public Void execute(final Session session) {
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
dao.insert(order);
return null;
}
});

SessionTemplate takes on four things.

  • Starting the session (begin)
  • Committing when the callback finishes normally (commit)
  • Rolling back when the callback throws (rollback)
  • Releasing regardless of the outcome (release)

The release always runs in a finally. An exception raised inside the callback propagates to the caller as it is, after the rollback. There is no need to catch and rethrow on the calling side just to trigger a rollback.

The type parameters of SessionCallback<T, E extends Exception> are the return type and the checked exception the callback may throw. Specify Void when no return value is needed, and RuntimeException when no checked exception is thrown.

When a value is returned, return the result of execute directly.

public Order findByOrderId(final String orderId) {
return SessionTemplate.execute(new SessionCallback<Order, RuntimeException>() {
@Override
public Order execute(final Session session) {
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
final OrderEntity entity = dao.findByOrderId(orderId);
return entity != null ? convertToModel(entity) : null;
}
});
}

Enclosing read-only processing in a SessionTemplate is the standard as well. Obtaining and releasing the connection then reads the same way as it does for updates, and the DAO is obtained in the same place in every method.

SessionCallback can also be written as a lambda expression. The type parameters are inferred from the body of the callback, so it is shorter than an anonymous class.

public Order findByOrderId(final String orderId) {
return SessionTemplate.execute(s -> {
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
final OrderEntity entity = dao.findByOrderId(orderId);
return entity != null ? convertToModel(entity) : null;
});
}

The callback can throw only one kind of checked exception. When you call processing that throws different kinds of checked exceptions, replace them with a single exception inside the callback before throwing.

Nested Transactions​

A repository keeps its own boundary while being called from a service as well. Inside a service that calls several repositories, those execute calls then become nested.

SessionTemplate.execute(new SessionCallback<Void, RuntimeException>() { // outer
@Override
public Void execute(final Session session) {
orderRepository.save(order); // execute is called inside
historyRepository.save(history); // execute is called inside
return null;
}
});

The inner execute neither commits nor rolls back. SessionTemplate checks whether a transaction is already in progress, and defers control of the boundary to the outer one when it is. The commit happens only when the outer execute completes normally, so the two writes above become a single unit.

When an exception is raised inside, the decision to roll back also rests with the outer boundary. Catching the inner exception outside and carrying on leaves the inner update uncancelled and included in the outer commit. If a requirement calls for cancelling only the inner part, address it through the order of operations or the design of the updates, not by splitting the transaction.

To combine several updates into a single unit, enclose the range you want to combine in an outer execute. There is no need to remove the execute from the repositories and place the boundary only on the calling side.

Never Update Outside the Boundary​

// Avoid
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
dao.insert(order);

Without enclosing this in a SessionTemplate, the unit of commit does not end up at the granularity you intended. Which layer establishes the boundary is covered in Infrastructure Layer Implementation Rules.