devloop icon
Metastores

transactions code flow

Transactions can be complex to understand, this page will show you which parts of the code will process objects and at which stage. This can be useful to extend the current behaviour, improve your transactions performance or simply better understand the code.


We will follow the code flow at the point where the IOWriteHandlerInterface is called (either via a utility class like IOHelper , DAO or via your own custom code (custom DAO, etc).


  1. The default behaviour is to use ObjectWriteHandler but it is possible to use other implementations which may or may not provide transaction support.
    This implentation will create an ObjectIOStatementImpl and pass it to the write method of the default ObjectWriter (by default TransactionObjectWriter which does support transactions).
  2. The TransactionObjectWriter will then use a GenericTransactionManager instance for ObjectStoreStatements (GenericTransactionManager is a generic type) The instance will be created if needed as defined by the application's preferences - see transactions page for more details on the annotations used to define the object' transaction mode).
  3. The ObjectIOStatement's execute method is then called within that transaction block (see GenericTransactionHandler for the transaction handling code).
  4. The execute method of the ObjectIOStatementImpl uses the TransactionHandlerHelper to call the cascading ObjectIOStatements defined for the object's application and for each column of this application during the mapping phase (typically on startup).
    These statements can be defined to run before and/or after the actual execute body code. This allows the nested calls to take all the required actions, including saving/assigning the nested objects (which may in turn trigger some more nested calls).
    It is important to note that this nesting process will call the nested object's ObjectWriteHandler - starting again at step 1. This means that if the nested object's transaction mode is incompatible with the current object being saved, a new transaction may be created and nested into the current one. If the transaction mode is compatible (which is the default) the transaction context is simply re-used, making all the objects part of the same transaction.
    The execute main body simply calls the doWrite method of its ObjectStorageWriter (which defaults to an instance of TransactionObjectStorageWriter)
  5. The doWrite method of the TransactionObjectStorageWriter simply adds a WriteStatement to the current GenericTransaction<ObjectStoreStatement> started by step 2.
  6. Once the outermost transaction started at step 2 has reached the commit point (ie: when it exits the nest() call or if the nested transaction is running at a different isolation level), all the WriteStatements that were added to it are executed.
  7. If one does not exist yet, each WriteStatement's execute method then starts a GenericTransactionManager for StorageStatements. (note: this is different from the manager which manages ObjectStoreStatements). It also creates a trigger on the current ObjectIOStatement Transaction so that at the end of a successful commit of that transaction, the newly created StorageStatement Transaction is itself committed.
    It then calls the doWriteTransaction method on the TransactionObjectStorageWriter. (see AbstractWriteImpl)
  8. The doWriteTransaction method will then delegates to the actual storage engine's to obtain a StorageStatement.
    ie: on an instance of AbstractDBTable the call to getWriter will return a DBUpdate
    This StorageStatement is then placed on the Transaction of StorageStatements.
  9. If all went well, after all the object and nested objects have add their storage engine create one or many StorageStatements, the commit end of the ObjectStoreStatement will call execute on each StorageStatement.
    This is where all the actual disk I/O gets done.



Now to give a more concrete example (using just the default options and a standard database backend), we will assume a very simple case of 2 objects A and B, where B is nested (and cascading) in A:


@Table("Table_A")
public class A {
	@Sequence	@PrimaryKey
	public	int key = 0;
	@Cascade
	public	B	b;
}
@Table("Table_B")
public class B {
	@Sequence	@PrimaryKey
	public	int key = 0;
	public	String name;
}
   

We now simply call IOHelper.save(new A()); (after mapping the object to a schema using DBDataMapper.map(A.class))

  1. ObjectWriteHandler creates an ObjectIOStatementImpl to represent this IO operation request
  2. TransactionObjectWriter starts a GenericTransaction for ObjectStoreStatements. to collect the store statements
  3. GenericTransactionHandler calls the ObjectIOStatementImpl's execute method in this new transaction context.
  4. ObjectIOStatementImpl's execute method calls:
    • The "pre" statements defined for object A which will trigger this application's sequence to allocate an id for object A's key field
    • The "pre" statements defined for object A's fields which will execute a new ObjectIOStatementImpl for object B (since it is cascading from A.b).
    • The doWrite method of the TransactionObjectStorageWriter
    • The "post" statements defined for object A's fields (none)
    • The "post" statements defined for object A (none)
    Note: because object B is nested in A and does not define its own transaction isolation, it will run in the same context as A, therefore the execute method on the nested ObjectIOStatementImpl for B will result in the same flow as is shown here for A except that there will be no nested object in B. (we will not repeat the steps here - the important aspect is that all the WriteStatements will end up on the same transaction, B being a normal cascade it runs as a "pre" statements and its WriteStatements will be first in the list)
  5. The doWrite() method of the TransactionObjectStorageWriter creates a WriteStatement and adds it to the ObjectStoreStatement transaction.
  6. The commit() method of the GenericTransaction executes each WriteStatement in turn, it contains:
    • WriteStatement(B)
    • WriteStatement(A)
  7. The WriteStament(B) creates a new GenericTransaction for StorageStatements. Then it calls doWriteTransaction on the TransactionObjectStorageWriter.
    By the time WriteStament(B) is executed, the StorageStatement transaction exists and it just calls doWriteTransaction.
  8. doWriteTransaction for B generates a DBInsert("Table_B").
    doWriteTransaction for A generates a DBInsert("Table_A").
  9. Once the commit() has successfully processed all the StorageStatements, the COMMIT_END trigger calls commit() on the StorageStatement transaction, which calls execute() on each DBInsert statement it contains (DBInsert("Table_B") and DBInsert("Table_A")).
    This in turn causes the DBInsert statements to connect to the database and execute the appropriate SQL statements.

Note: in this example, the database lock is only held during part of step 9!
Between the time the first database statement requests a connection and the sucessful commit end of the StorageStatement transaction.
The database connection is automatically commited (or rolled back if needed) using another COMMIT_END (or COMMIT_FAIL) trigger on the StorageStatement transaction.