In my previous post on tracking database changes I described how you can log all database changes for a quick orm system. I also mentioned you can do a similar thing for cborm systems which are based on coldfusion ORM (hibernate). I have used cborm a lot in the past, and even did some presentations on orm, but I noticed there are quite some people who don’t like or even hate the coldfusion orm, for being slow, memory consuming and hard to understand. I am not sure if this reputation is well deserved. Yes, error messages are hard to understand, but so are the errors in quick (if you are not the author of the module 🙂 . Initially Adobe did a bad job on default settings for ORM, which made it harder to understand and Lucee ruined my ORM setup when upgrading from lucee 4 to 5, and it took them several years to fix three lines of code. But nowadays, there is an update for the hibernate engine in Lucee, and more important: the cborm library does a very nice job in hiding the hard parts of database transactions in ORM, and it opens up the Criteria API from hibernate. Once you know how to handle cborm it is a lot faster than quick and it has a different abstraction for your entities. I will leave the details on performance for another post. Just want to mention both quick and cborm have their place, and in this post I will show you which steps you should take to track your database changes with cborm .

Tracking your database changes in cborm is quite similar to those in a quick system: the orm library will announce some events, and you have to create and register an interceptor which listens to these events. There are some differences though, so let’s see what we need.

First of all: we have to enable ORM event handling in the coldfusion orm. We do this in Application.cfc :

//configure this mapping here, your ormSetting need it
this.mappings[ "/cborm" ] = COLDBOX_APP_ROOT_PATH & "/modules/cborm";
this.ormSettings = {
    // ...... (other orm settings)
    // Enable event handling
    eventhandling = true,
    // Set the event handler to use, which will be inside our application.
    eventhandler = "cborm.models.EventHandler"
};

As you can see in the highlighted lines, we have to specify an event handler, which acts as a bridge between coldfusion orm and the coldbox interceptions. Here we have the first challenge: As I mentioned in my last post, there are some issues with this. Since coldbox 6.5 this will fail in Lucee. There are several ways to fix it, in coldbox, in the cborm library or in Lucee itself. I don’t know who feels responsible for this error, but if you just wish to test the orm event handler you can use coldbox 6.4. If you need a more permanent solution you can create your own eventhandler as described in my last post. Once this if fixed we can start listening to the ormEvents. The events relevant for this use case are:

  • ORMPostNew
  • ORMPreload / ORMPostLoad
  • ORMPreDelete / ORMPostDelete
  • ORMPreUpdate / ORMPostUpdate
  • ORMPreInsert / ORMPostInsert
  • ORMPreSave / ORMPostSave

Just as with the quick application, the ORMPostInsert, ORMPostUpdate and ORMPostDelete are the most important events. But cborm does a slightly better job in the ORMPreUpdate event, because not only the current entity will be listed in the interception, but also the old data. So if you really want to register old and new data, you can use the ORMPreUpdate event here.

We have to create our audit logger first in the coldbox configuration, for example:

logbox: {
  //.. appenders  
  categories : {
     "audit" : {
        levelMax  : "INFO",
        appenders : "extendedDBAppender"
     },
// ..etc

and register our logging interceptor in the same config file:

interceptors = [
//....  
{
        class      : "interceptors.cbormInterceptor",
        properties : {}
 },

Our cbormInterceptor here looks like this now:

component {
	property name="auditLogger" inject="logBox:logger:audit";
	property name="orm" inject="provider:entityService";
	
    void function configure(){
	}

	function ORMPostInsert( event, interceptData, buffer, rc, prc ){
		logAction("ORMPostInsert", interceptData,rc,prc);
	}

	function ORMPreUpdate( event, interceptData, buffer, rc, prc ){
		logAction("ORMPreUpdate", interceptData,rc,prc);
	}

	function ORMPostUpdate( event, interceptData, buffer, rc, prc ){
		logAction("ORMPostUpdate", interceptData,rc,prc);
	}

	function ORMPostDelete( event, interceptData, buffer, rc, prc ){
		logAction("ORMPostDelete", interceptData,rc,prc);
	}

	private function logAction(required string Action, data, rc, prc ){
		try {
			var entityName = orm.getEntityGivenName(data.entity);
			var entityId = data.Entity.getId() ?: "- no Id -";
			var myData = { "entity": data.Entity.getMemento() };
			if ( arguments.action == "ORMPreUpdate" ) {
				myData["oldData"] = data.oldData;
			}
		} catch(any e){
		   // ignore error for getMemento
			var myData={ "error catched": e};
			var entityName = "unknown";
			var entityId = "unknown";
		}
		mydata["rc"] = rc;
		auditLogger.info( "#arguments.action#  #entityName# with id #entityId#.",  myData);
	}
}

This interceptor has two dependencies: an audit logger is injected, but also a reference to the BaseOrm service (property name="orm" inject="provider:entityService"). We can use this base ORM service so our logAction can detect the name of the entity type. You can also do this by querying the metadata of the component. Make sure you wrap your logAction in a try .. catch block, so it doesn’t break your application if your orm events serve incomplete data.

Your ORM entities will have a memento method if you configure this in your coldbox config

// module settings - stored in modules.name.settings
moduleSettings = {
	mementifier = {
		iso8601Format = false,
		dateMask      = "yyyy-MM-dd",
		timeMask      = "HH:mm: ss",
		// Enable orm auto default includes: If true and an object doesn't have any `memento` struct defined
		// this module will create it with all properties and relationships it can find for the target entity
		// leveraging the cborm module.
		ormAutoIncludes = true
	}
}

The Orm events do have some caveats:

  • in the ORMPreLoad event your entity has not fully loaded yet. Because of this the getMemento method is not available here.
  • the ORM events look quite powerful. So you might be tempted to change your created_at or modified_at date in these events. Please don’t do that, because it will trigger new orm events, which will lead to all kinds of unwanted and/or nested dependencies.

Using this system you can track any database change. Please read my previous post on the lucee and coldbox issue very carefully if you want to use these events. If you have any questions feel free to ask wil-shiftinsert in the cfml Slack.

If you don’t want to use quick or cborm you will still have possibilities to track your database changes using some extra creativity. I will address some of these cases in a future post.