[coldbox-5.3.0] Hibernate and transactions

We have been experiencing some unexpected (to us) ORM behavior and are trying to reason through it. We are running cf2016, cb 5.3, and cborm but not using the included aop.

An entity is loaded from the database, updated from user input, but then doesn’t pass validation so we do not call save. Later on in an interceptor listening for postProcess() we save an object related to database backed session information and the item that didn’t pass validation still persists its data to the database.

After doing some research we came across a relevant line in the cf transaction docs Transaction and concurrency.

Whenbegins, any existing ORM session is flushed and closed, and a new ORM session is created.

This explains the behavior we are seeing. We have an entity tracked by hibernate that has been updated, when a transaction is opened (in the BaseORMService.save() called from our postProcess() interceptor) it flushes everything previously and saves the entity.

A simple, stripped down example

`
var item = entityLoadByPK( ‘Your_Entity’, ‘id’ );

// update a property
item.setActive( !item.getActive() );

// the orm session is flushed because a new transaction is opened
transaction {
// correct me if I’m wrong but - nothing in here matters?
}
`

This leads me to a couple questions

  1. What is the point of the transaction{} code in the BaseORMService.$transactioned() method? This is the default behavior, but if the ORMSession is flushed when the transaction is opened doesn’t that mean it has nothing to do with the code that called save() in the first place?

  2. What is the appropriate pattern for working with our entities? Should we be wrapping everything in a transaction (aop?)? Should we be calling ORMClearSession() when we’ve decided not to save something? Should we turn off the default transaction in the BaseORMService and manually call ORMFlush()?

Thanks in advance for any help in understanding this.

Jacob,

You seem to be using native ORM methods, along with the ORM service methods. When using cbORM, you should avoid the native CF ORM methods and use the Base ORM service methods to load and work with entities. ( e.g. getInstance( “myEntity” ).get( myId ) rather than entityLoadByPK( 'myEntity’, myId ) ).

Any saves called by the BaseORMService are automatically transactioned, unless you specify otherwise, or they are already within a transaction. This ensures that procedural blocks with subsequent save() calls can depend on the database record existing and saves a whole bunch of manual flushing of the session.

If you don’t wish to use transactions, you can bypass that:

Active Entity : myEntity.save( transactional=false )
Service : myService.save( entity=myEntity, transactional=false)

An empty transaction block would not be the way to force a flush of the session. You can use the the save() or saveAll() methods and pass a flush=true argument or just rely on the transaction to flush.

If you have a bunch of items being saved within a loop, simply wrap all of those items in one transaction and pass the transactional=false argument to the save() call. Then commit the transaction at the end.

So, to give you an example, using your code ( which will automatically transaction your entity save ):

var itemService = new cborm.models.VirtualEntityService( entityName=“Item” );
var item = itemService.get( itemId );

// update and save
item.setActive( !item.getActive() ).save();

If you want to do other things before you make the big save:

transaction{
item.setActive( !item.getActive() ).save( transactional=false );

… do lots of things …

transaction action=”commit”;
}

HTH,

Jon

Thanks for your quick reply! Sorry, my example was not a very good representation of what we are actually doing in our codebase, that was just to illustrate the weird behavior we are seeing with transaction{}.

In our code we follow a pattern similar to what you are describing:

`
public any function updateItem( required Any event, required Struct rc, required Struct prc ) {

var item0 = itemService.get( ‘0’ );

item0.setActive( !item0.getActive() );

itemService.save( item0 );

return {
done: true
};
}
`

What I’m finding is that this can update entities not passed into the save as well:

`
public any function updateItem( required Any event, required Struct rc, required Struct prc ) {

var item0 = itemService.get( ‘0’ );
var item1 = itemService.get( ‘1’ );

item0.setActive( !item0.getActive() );
// this one is also updated, but not passed into the save function
// it WILL be updated in the database as well
item1.setActive( !item1.getActive() );

itemService.save( item0 );

return {
done: true
};
}
`

What is even stranger is that if in any example I comment out the code inside the BaseORMService.$save() method it will STILL update our entities in the database simply because a transaction{} is getting opened due to getUseTransactions() being defaulted to true in the BaseORMService.

Hi Jacob,

This is a pesty issue I have had with Adobe/Lucee in that cftransaction should have NEVER had flushed the session. They refused to listen to us back in the cf9 days, and not much I can do on that front, except go around them.

The cborm services leverage cftransactions for you, so it can provide the transaction demarcation without you constructing nasty demarcation code. It allows for nesting, savepoints and rollbacks even partial rollbacks.

Because it leverages cftransaction, that’s why you see that side-effect. However, as a rule of thumb, if you load an entity or entities into the hibernate session, populate them with data, and the data does not validate your rules, those entities SHOULD be cleared from session, they have no business being in the session. They do not belong there as they are in an invalid state. Therefore, as best practice, you should always clear or clean the session from entities you don’t want potentially to be flushed. As best practice.

We also provide an alternative in cborm, the Hibernate Transaction AOP aspect, which can wrap method calls with a native hibernate transaction so this stupid Adobe issue does not reflect in this scenario, but it still has several other issues. See below:

If we switch to hibernate native in the base services (believe it or not, version 1 was this way), you still have issues:

  1. Nested cftransactions will not work, nested method calls with our aspect will work though
  2. Transaction save points will not work, hibernate does not provide those
  3. Partial Rollbacks will not work, hibernate does not provide those
  4. Native cfquery calls will not be transactioned, that’s outside of hibernate

Again, this might be overwhelming and it is. Transaction demarcation is not easy for any language and in any eventuality, even if you are NOT using orm, you should be having some type of transaction demarcations with vanilla cfquery. Which can also be hard, since once that query runs, it runs and persists. Creating consistent boudaries for data persistence can get very complex in complex systems.

Through experience, what I have found, is that the easiest and most pragmatic approach, is to still leverage cftransactions and know (you are the programmer) that if there are entities in session that should not be there before the transaction demarcation border is applied, or your code finalizes in a method, they should be cleaned from session. The base orm services has tons of methods for clearing that traditional cf does not have:

clearSession() - clear entire session
evictEntity( entities ) - clear 1 or an array of entities
evict( entityName, collectionName, id ) - evict a specific collection or entity by id

I see. Thanks for taking the time to chime in on this. We will likely need to spend some time reviewing our code to determine what works best for us.