Transaction handling

Developping a RIA that involves long-lived entities introduces a big challenge regarding transaction handling. In traditional webapps, the transaction is mapped to the ORM session (e.g. JPA or Hibernate) wich, in turn, is mapped to simple and well-defined application lifecycle phases (be it the HTTP request itself with the "open session in view" pattern, or a more extended scope like JBoss Seam or CDI "conversation").

Jspresso offers a more flexible application worlkflow where the user is not driven by pre-defined pages chainings. Unlike in traditional webapps, the transient model state can evolve independently of the persistent state in an arbitrary way until the user decides to save (or rollback) the changes. Therefore, there is a subtle coordination achieved by the framework to that the model in-memory state remains coherent with the persistent one. Jspresso cannot solely rely on the ORM session to handle the session model state since it is bad practice to keep the ORM session open indefinitely and span multiple requests/transactions. Moreover, any runtime exception can make the ORM session unusable, thus invalidating all the transient state.

In order to achieve this, Jspresso implements automatic tracking of session model changes, and seamlessly deals with the ORM to merge those changes back to the persistent store. All this work nicely integrates with transaction management through the concept of "Unit of Work" (referred to as UOW). A Jspresso UOW can be seen as an in-memory draft zone where model parts can be duplicated and modified without impacting the actual model session state. A UOW can be started, committed, rolled-back, very much like transactions indeed. But you will rarely need to manage the UOW yourself since Jspresso automatically maps its lifecycle to the actual underlying transaction platform, e.g. the Hibernate session, the JTA implementor, ... This means that whenever a transaction is actually started, committed or rolled-back, thhe corresponding operations are performed on the UOW.

Jspresso offers built-in standard actions as well as a comprehensive API to handle domain model persistence along with transactions. Once the user as updated parts of the model using the application views and services here is a synopsis of the operations involved in persisting the changes :

  1. A transaction is started through an action triggered by the user (either the built-in standard save action or a custom one).
  2. All or a subset of the session-modified model is imported (carbon-copied) into the unit of work.
  3. All sorts of operations are performed on UOW entity clones like further modifications, ORM updates and deletes, ... all this in complete isolation from the outer application session state.
  4. Depending on the transaction outcome, the UOW content is merged back to the application session or simply forgotten, in which case, the session state remains untouched.

 

 

Starting a transaction

There are 2 different ways to start a transaction in Jspresso. The first one is to annotate the backend action class with org.jspresso.framework.application.backend.action.Transactional (beware to use the Jspresso one, not the spring one). It forces the action to execute inside a transaction with a REQUIRED propagation scheme (use the existing transaction if any or start a new one).

@Transactional
public class MyCustomAction extends AbstractHibernateAction {
  ...
}

The previous method is just a shortcut to using the Spring transaction template that is configured in the application backend controller. This is the second, more versatile way of using transactions in Jspresso :

public class MyCustomAction extends AbstractHibernateAction {
  @Override
  public boolean execute(IActionHandler actionHandler, final Map<String, Object> context) {
    getBackendController(context).getTransactionTemplate().execute(new TransactionCallback() {       
      public Object doInTransaction(TransactionStatus txStatus) {
        ...
      }
    });
  }
}

The second form allows for more control over the transaction(s) demarcation, e.g. chain 2 or more different transactions in the same action.

 

Cloning working entities into the UOW

Before working on an entity transactionally you have to make a carbon copy of it into the UOW and do all the work on this copy. The backend controller provides the following methods to perform the carbon copy :

  IEntity cloneInUnitOfWork(IEntity entity);

  List<IEntity> cloneInUnitOfWork(List<IEntity> entities);

Several characteristics of the cloning process :

  • When an entity is cloned into the unit of work, it is deeply cloned (i.e. its loaded children are cloned).
  • The entity cloning process is idempotent, i.e. calling twice the cloning process on the same entity results in obtaining the same clone instance.
  • An entity clone has the same "dirty state" that its source entity

 

Note :
You should always make sure that you work on UOW entity clones inside a transaction. This is the only way that Jspresso can garantee the complete isolation of the transaction space. Obscure Hibernate exceptions like "entity with the same identifier was already registered in the session" are generally the consequence of working on entities that have not been cloned into the UOW.

 

Perform operations on UOW entity clones

Now that you are in an isolated space, you can do whatever you need without having the risk to polute the application session in case of a failure (either due to a functional violation or a technical error). Note that all operations made on the UOW entities are local to the unit of work, e.g. :

  • model state changes
  • persistence operations, e.g. registerForUpdate, registerForDeletion, performPendingOperations, cleanRelationshipsOnDeletion, ...

 

Merge UOW entity clones back into the normal application session

It is time to commit or rollback the transaction. This is automatically done by the Spring transaction template or indirectly at the end of the "execute" method of a Transactional annotated backend action.
Whenever the transaction is rolled back, the UOW is simply discarded and the session state remains untouched.
Whenever the transaction is committed, we have to merge back the UOW state that needs to be propagated to the application session current state.

This last step is automatically performed for created/updated entities whose state has been synchronized (flushed) to the store on transaction commit. During this phase, the entity state is merged back to the correponding session entity (if any) and its dirty state is reset to unmodified.
This is all perfect for save operations; however, there are situations when you need to apply another type of merge from the UOW entities. These other merge modes cannot be automatically inferred by the framework, and thus, must be explicitely coded through the use of the following backend controller methods :

  IEntity merge(IEntity entity, EMergeMode mergeMode);

  List<IEntity> merge(List<IEntity> entities, EMergeMode mergeMode);

Several characteristics of the merging process :

  • When an entity is merged back into the session, it is deeply merged (i.e. its loaded children are merged using the same mode).
  • The entity merging process is idempotent, i.e. calling twice the merging process on the same entity results in obtaining the same merged instance.
  • The merged entity dirty state depends on the used merge mode.

Jspresso implements 4 different merge modes :

  • MERGE_CLEAN_LAZY : This is the merge mode used for flushed entities after a successful commit. Merged entities are considered clean (unmodified).
  • MERGE_CLEAN_EAGER : This is the merge mode used for entities reloaded from the persistent store. The merged entities (and their loaded children) have their state completely reset and are considered clean (unmodified).
  • MERGE_EAGER : This is the merge mode used for implementing "in-memory" transactions. Merged entities have their state updated but they are NOT considered clean, i.e. they keep their previous dirty state completed with the new one coming from the merge.
  • MERGE_KEEP : This merge mode will only register entities that do not exist yet in the session. Existing entities are simply returned (even if their version is obsolete) and their dirty state is kept.