Chapter 9. Persistence

9.1. Standard environment configuration

This section describes how the environment can be configured to use hibernate in a standard Java environment.

01 | <environment>
02 | 
03 |   <application>
04 |     <hibernate-session-factory />
05 |     <hibernate-configuration>
06 |       <properties resource="hibernate.properties" />
07 |       <mappings resources="org/jbpm/pvm.hibernate.mappings.xml" />
08 |       <cache-configuration 
09 |            resource="org/jbpm/pvm.definition.cache.xml" 
10 |            usage="nonstrict-read-write" />
11 |     </hibernate-configuration>
12 |   </application>
13 |
14 |   <block>
15 |     <standard-transaction />
16 |     <hibernate-session />
17 |     <pvm-db-session />
18 |   </block>
19 |
20 | </environment>

line 04 specifies a hibernate session factory in the application context. This means that a hibernate session factory is lazy created when it is first needed and cached in the EnvironmentFactory.

A hibernate session factory is build calling the method buildSessionFactory() on a hibernate configuration. By default, the hibernate configuration will be looked up by type.

line 05 specifies a hibernate configuration.

line 06 specifies the that the resource file hibernate.properties should be loaded into the configuration.

line 07 (note the plural form of mappings) specifies that resources org/jbpm/pvm.hibernate.mappings.xml contain references to hibernate mapping files or resources that should be included into the configuration. Also note the plural form of resources. This means that not one, but all the resource files on the whole classpath will be found. This way new library components containing a org/jbpm/pvm.hibernate.mappings.xml resource can plug automatically into the same hibernate session by just being added to the classpath.

Alternatively, individual hibernate mapping files can be referenced with the singular mapping element.

line 08 - 10 provide a single place to specify the hibernate caching strategy for all the PVM classes and collections.

line 15 specifies a standard transaction. This is a very simple global transaction strategy without recovery that can be used in standard environments to get all-or-nothing semantics over multiple transactional resources.

line 16 specifies the hibernate session that will automatically register itself with the standard transaction.

line 17 specifies a PvmDbSession. That is a class that adds methods that bind to specific queries to be executed on the hibernate session.

9.2. Standard hibernate configuration

Here is a set of default properties to configure hibernate with hsqldb in a standard Java environment.

hibernate.dialect                      org.hibernate.dialect.HSQLDialect
hibernate.connection.driver_class      org.hsqldb.jdbcDriver
hibernate.connection.url               jdbc:hsqldb:mem:.
hibernate.connection.username          sa
hibernate.connection.password
hibernate.cache.use_second_level_cache true
hibernate.cache.provider_class         org.hibernate.cache.HashtableCacheProvider

Optionally in development the schema export can be used to create the schema when the session factory is created and drop the schema when the session factory is closed.

hibernate.hbm2ddl.auto                 create-drop

For more information about hibernate configurations, see the hibernate reference manual.

9.3. Standard transaction

By default, the <hibernate-session /> will start a hibernate transaction with session.beginTransaction(). Then the hibernate transaction is wrapped in a org.jbpm.hibernate.HibernateTransactionResource and that resource is enlisted with the <standard-transaction /> (org.jbpm.tx.StandardTransaction)

Inside of the environment block, the transaction is available through environment.getTransaction(). So inside an environment block, the transaction can be rolled back with environment.getTransaction().setRollbackOnly()

When created, the standard transaction will register itself to be notified on the close of the environment. So in side the close, the standard transaction will commit or rollback depending on whether setRollbackOnly() was called.

So in the configuration shown above, each environment block will be a separate transaction. At least, if the hibernate session is used.

9.4. Basics of process persistence

In the next example, we'll show how this hibernate persistence is used with a concrete example. The 'persistent process' is a simple three-step process:

The persistent process

Figure 9.1. The persistent process

The activities in the three nodes will be wait states just like in Section 2.4, “ExternalActivity example”

To make sure we can persist this class, we create the hibernate mapping for it and add it to the configuration like this:

<hibernate-configuration>
  <properties resource="hibernate.properties" />
  <mappings resources="org/jbpm/pvm.hibernate.mappings.xml" />
  <mapping resource="org/jbpm/examples/ch09/state.hbm.xml" />
  <cache-configuration 
        resource="org/jbpm/pvm.definition.cache.xml" 
        usage="nonstrict-read-write" />

The next code pieces show the contents of one unit test method. The method will first create the environment factory. Then, in a first transaction, a process definition will be created and saved into the database. Then the next transaction will create a new execution of that process. And the following two transactions will provide external triggers to the execution.

EnvironmentFactory environmentFactory = EnvironmentFactory.parse(new ResourceStreamSource(
    "org/jbpm/examples/ch09/environment.cfg.xml"
));

Then in a first transaction, a process is created and saved in the database. This is typically referred to as deploying a process and it only needs to be done once.

Environment environment = environmentFactory.openEnvironment();
try {
  PvmDbSession pvmDbSession = environment.get(PvmDbSession.class);
  
  ProcessDefinition processDefinition = ProcessFactory.build("persisted process")
    .node("one").initial().behaviour(new State())
      .transition().to("two")
    .node("two").behaviour(new State())
      .transition().to("three")
    .node("three").behaviour(new State())
  .done();
  
  pvmDbSession.save(processDefinition);
} finally {
  environment.close();
}

In the previous transaction, the process definition, the nodes and transitions will be inserted into the database tables.

Next we'll show how a new process execution can be started for this process definition. Note that in this case, we provide a business key called 'first'. This will make it easy for us to retrieve the same execution from the database in subsequent transactions. After starting the new process execution, it will wait in node 'one' cause the behaviour is a wait state.

environment = environmentFactory.openEnvironment();
try {
  PvmDbSession pvmDbSession = environment.get(PvmDbSession.class);
  
  ProcessDefinition processDefinition = pvmDbSession.findProcessDefinition("persisted process");
  assertNotNull(processDefinition);
  
  Execution execution = processDefinition.startExecution("first");
  assertEquals("one", execution.getNode().getName());
  pvmDbSession.save(execution);

} finally {
  environment.close();
}

In the previous transaction, a new execution record will be inserted into the database.

Next we feed in an external trigger into this existing process execution. We load the execution, provide a signal and just save it back into the database.

environment = environmentFactory.openEnvironment();
try {
  PvmDbSession pvmDbSession = environment.get(PvmDbSession.class);
  
  Execution execution = pvmDbSession.findExecution("persisted process", "first");
  assertNotNull(execution);
  assertEquals("one", execution.getNode().getName());
  
  // external trigger that will cause the execution to execute until 
  // it reaches the next wait state
  execution.signal();

  assertEquals("two", execution.getNode().getName());

  pvmDbSession.save(execution);

} finally {
  environment.close();
}

The previous transaction will result in an update of the existing execution, reassigning the foreign key to reference another record in the node table.

UPDATE JBPM_EXECUTION 
SET
  NODE_=?,
  DBVERSION_=?,
  ...
WHERE DBID_=? 
  AND DBVERSION_=?

The version in this SQL shows the automatic optimistic locking that is baked into the PVM persistence so that process persistence can easily scale to multiple JVM's or multiple machines.

In the example code, there is one more transaction that is completely similar to the previous which takes the execution from node 'two' to node 'three'.

All of this shows that the PVM can move from one wait state to another wait state transactionally. Each transaction correcponds to a state transition.

Note that in case of automatic activities, multiple activities will be executed before the execution reaches a wait state. Typically that is desired behaviour. In case the automatic activities take too long or you don't want to block the original transaction to wait for the completion of those automatic activities, check out Chapter 11, Asynchronous continuations to learn about how it's possible to demarcate transactions in the process definition, which can also be seen as safe-points during process execution.

9.5. Business key

TODO

TODO: General persistence architecture

TODO: Object references

TODO: Threads, concurrency with respect to forks and joins

TODO: Caching

TODO: Process instance migration