Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (Feb. 2009) with: Joakim Marner

Learning ColdFusion 9: Trying To Understand ORM Event Handling

Posted by Ben Nadel
Tags: ColdFusion

Just as we can listen to application-related events using Application.cfc event handlers, we can also listen to ORM-related events using entity-based event handlers. At least, that's what the ColdFusion 9 public beta documentation tells us. In reality, however, I have not been able to get these event handlers to fire as expected. According to the documentation, there are eight events surrounding the four ORM actions: load, insert, update, delete. Each of these actions has a Pre and Post event that can be intercepted with the following entity-based event handlers:

  • PreLoad()
  • PostLoad()
  • PreInsert()
  • PostInsert()
  • PreUpdate( Struct oldData )
  • PostUpdate()
  • PreDelete()
  • PostDelete()

In the documentation about these events, there are two caveats:

  1. If you return "true" from the PreUpdate() method, you can cancel the update action.
  2. In order to get the Pre/PostUpdate() events to fire properly, the entity being saved must have been loaded using EntityLoad()

Just having these event handlers in place is not enough to get them to work; event handling has to be explicitly turned on in your Application.cfc ORMSettings. Below is the Application.cfc that I use for all of my ORM testing. Notice that it is now using the property, this.ormSettings.eventHandling:

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
  •  
  • <!---
  • Store the datasource for this entire application. This
  • will be used with all the CFQuery tags as well as by
  • the ORM system.
  • --->
  • <cfset this.datasource = "cf9orm" />
  •  
  • <!---
  • This will turn on ORM capabilities for this application.
  • This tells ColdFusion to load all of the Hibernate code
  • and to communicate with the datasource above to prepare
  • the configuration files.
  • --->
  • <cfset this.ormEnabled = true />
  •  
  • <!---
  • This will define how the ORM system will work. Because
  • we are not starting out with any database, but using the
  • ORM system to build the database, we want to turn off
  • the use of the DB to get mapping data - we will be
  • defining all of the mapping information in our CFCs.
  •  
  • NOTE: "None" is the default for dbCreate. I have left it
  • in here only because I am overriding it afterwards.
  • --->
  • <cfset this.ormSettings = {
  • dbCreate = "none",
  • useDBForMapping = false,
  • eventHandling = true
  • } />
  •  
  • <!---
  • Check to see if we need to rebuild the database. Normally,
  • these ORM settings only take effect when the application
  • is starting up; however, if we change them here AND then
  • call ORMReload() later on in the page, these settings seem
  • to take effect without stopping the appliation first. The
  • call to ORMReload(), however CANNOT be inside the
  • Application.cfc pseudo constructor.
  • --->
  • <cfif !isNull( url.rebuild )>
  •  
  • <!---
  • Signal to the ORM that we want to drop and then re-
  • create our database.
  • --->
  • <cfset this.ormSettings.dbCreate = "dropcreate" />
  •  
  • </cfif>
  •  
  • <!---
  • Check to see if we need to update the database (adding
  • columns and mappings that might not have previously
  • existed. Like the other ORM Settings, this only takes
  • effect when the application is restarted... OR, if
  • ORMReload() is called on the same page.
  • --->
  • <cfif !isNull( url.refresh )>
  •  
  • <!---
  • Signal to the ORM that we want to update existing
  • tables or add new ones.
  • --->
  • <cfset this.ormSettings.dbCreate = "update" />
  •  
  • </cfif>
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I intialize the request.">
  •  
  • <!--- Define the request settings. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!---
  • Check to see if the refresh of rebuild flag is
  • present. If it is, then we need to reload the ORM
  • mappings and configuration.
  • --->
  • <cfif (
  • !isNull( url.rebuild ) ||
  • !isNull( url.refresh )
  • )>
  •  
  • <!--- Reload the ORM configuration and mappings. --->
  • <cfset ormReload() />
  •  
  • </cfif>
  •  
  • <!--- Return true so that the page can run. --->
  • <cfreturn true />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, the Application.cfc property, this.ormSettings.eventHandling, has been set to true. This will tell the ORM system to check the target entities for event handlers when performing persistence-layer-related actions.

Now that the ORM event handling has been turned on, let's take a look at our test ColdFusion component. The following CFC has a few simple persisted properties and a non-persisted event log that will help us track the ORM events:

Thought.cfc

  • <cfcomponent
  • output="false"
  • hint="I represent a thought."
  • persistent="true"
  • table="thought">
  •  
  • <!--- Define the CFC properties. --->
  •  
  • <cfproperty
  • name="id"
  • type="numeric"
  • setter="false"
  • hint="I am the unique ID of the thought at the persistence layer."
  •  
  • fieldtype="id"
  • ormtype="integer"
  • length="10"
  • generator="identity"
  • notnull="true"
  • />
  •  
  • <cfproperty
  • name="content"
  • type="string"
  • validate="string"
  • validateparams="{ minlength=1 }"
  • hint="I am the thought."
  •  
  • fieldtype="column"
  • ormtype="text"
  • notnull="true"
  • />
  •  
  • <cfproperty
  • name="dateCreated"
  • type="date"
  • validate="date"
  • hint="I am the date the thought was created."
  •  
  • fieldtype="column"
  • ormtype="timestamp"
  • notnull="true"
  • />
  •  
  • <cfproperty
  • name="dateUpdated"
  • type="date"
  • hint="I am the date the thought was updated."
  •  
  • fieldtype="column"
  • ormtype="timestamp"
  • notnull="true"
  • />
  •  
  • <!---
  • This property is not a persistent property but is
  • one that we are using simply to track the events
  • that take place.
  • --->
  • <cfproperty
  • name="eventLog"
  • type="array"
  • hint="I am an internal log used to track events."
  •  
  • persistent="false"
  • />
  •  
  •  
  • <!---
  • Set default values for complext objects. These cannot
  • be set by default using the CFProperty tags.
  • --->
  • <cfset this.setEventLog( [] ) />
  •  
  •  
  • <cffunction
  • name="init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I initialize this object.">
  •  
  • <!--- Return this object reference. --->
  • <cfreturn this />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="preLoad"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run before this entity is loaded.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "preLoad" ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="postLoad"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run after this entity is loaded.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "postLoad" ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="preInsert"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run before this entity is inserted.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "preInsert" ) />
  •  
  • <!---
  • Set the date/time properties. This way, we can
  • keep track of the time at which this object was
  • created / inserted.
  • --->
  • <cfset this.setDateCreated( Now() ) />
  • <cfset this.setDateUpdated( Now() ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="postInsert"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run before this entity is inserted.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "postInsert" ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="preUpdate"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run before this entity is updated.">
  •  
  • <!--- Define the arguments. --->
  • <cfargument
  • name="oldData"
  • type="struct"
  • required="true"
  • hint="I am the collection of data held over from the load time."
  • />
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "preUpdate", arguments.oldData ) />
  •  
  • <!---
  • Set the date/time properties. This way, we can
  • keep track of the most recent time at which this
  • object was updated.
  • --->
  • <cfset this.setDateUpdated( Now() ) />
  •  
  • <!---
  • Return out. NOTE: If this method return TRUE, then
  • the update is cancelled.
  • --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="postUpdate"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run after this entity is updated.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "postUpdate" ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="preDelete"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run before this entity is deleted.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "preDelete" ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="postDelete"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run after this entity is deleted.">
  •  
  • <!--- Track event. --->
  • <cfset this.trackEvent( "postDelete" ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="trackEvent"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I log an event to the internal event log.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="eventName"
  • type="string"
  • required="true"
  • hint="I am the name of the event being logged."
  • />
  •  
  • <cfargument
  • name="eventData"
  • type="any"
  • required="false"
  • default=""
  • hint="I am the data associated with the event."
  • />
  •  
  • <!--- Create a log item from this event. --->
  • <cfset local.logItem = {
  • eventName = arguments.eventName,
  • eventData = arguments.eventData,
  • dateExecuted = now()
  • } />
  •  
  • <!--- Add this event to the internal log. --->
  • <cfset arrayAppend(
  • variables.eventLog,
  • local.logItem
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

While this ColdFusion component is long, it is quite simple; all it does is provide listeners for the ORM events that log the events to the internal EventLog property. Ok, that's not entirely true; you'll notice that the PreInsert() and PreUpdate() events both update the date/time properties for date created and date updated. I did this so that I wouldn't have to manually update these tracking values every time I persisted the object. (NOTE: I realize that using a TimeStamp data type was not correct for this intention).

Now that we have ORM event handling enabled and we have our event handlers defined in our target entity, Thought.cfc, it's time to run some tests. In the first test, I'm going to create a new entity, save it, reload it, and update it:

  • <!--- Create a new thought. --->
  • <cfset thought = entityNew( "Thought" ) />
  •  
  • <!--- Define the thought content. --->
  • <cfset thought.setContent( "I wonder what kind of undies Tricia is wearing today?" ) />
  •  
  • <!--- Persist the thought - this will be an INSERT. --->
  • <cfset entitySave( thought ) />
  •  
  • <!--- Output the object (this will include the event log). --->
  • <cfdump
  • var="#thought#"
  • label="EntityNew() / EntitySave()"
  • />
  •  
  •  
  • <br />
  • <!--- ----------------------------------------------------- --->
  • <cfthread action="sleep" duration="#(2 * 1000)#" />
  • <!--- ----------------------------------------------------- --->
  • <br />
  •  
  •  
  • <!---
  • Load the thought based on the primary key (which we know
  • will be "1" since we just rebuild the database).
  • --->
  • <cfset thought = entityLoadByPK( "Thought", 1 ) />
  •  
  • <!---
  • Reload this object from the database just incase there
  • is any session-based caching going on.
  • --->
  • <cfset entityReload( thought ) />
  •  
  • <!--- Reset the thought content. --->
  • <cfset thought.setContent( "That Joanna really looks good with her hair up. Why don't more women wear there hair up?" ) />
  •  
  • <!--- Persist the thought - this *should be* an UPDATE. --->
  • <cfset entitySave( thought ) />
  •  
  • <!--- Output the object (this will include the event log). --->
  • <cfdump
  • var="#thought#"
  • label="EntityLoadByPK() / EntitySave()"
  • />

Notice above that I am getting the current thread to sleep for two seconds between actions; I did this so that the date/time stamp in the event log would have a noticeable difference between Insert and Load actions. When we run the above code, we get the following CFDump output:

 
 
 
 
 
 
ColdFusion 9 ORM Event Handling. 
 
 
 

In the first part of the demo, we created a new Thought.cfc instance and saved it. Looking at the EventLog property, we can see that this caused the PreInsert() and PostInsert() events to fire properly. So far so good.

In the second part of the demo, you'll notice that I am calling both the EntityLoadByPK() and the EntityReload() methods. The reason that I am calling EntityReload() is because entities are cached for the duration of the Hibernate session (which by default is the current page request). This means that the entity loaded in the second part of the demo will be the same physical object as the one created in the first part of demo. As such, no load event would fire. Since EntityReload() forces the ORM system to go back to the database (for persisted properties only), the second part of our demo successfully calls the PreLoad() and PostLoad() events. And, looking at the EventLog property, we can see that we have the Pre/PostInsert() from the first part plus the PreLoad() and PostLoad() from the EntityReload() call.

But wait, I'm also calling EntitySave() in the second part of the demo; where is my PreUpdate() and PostUpdate() event tracking? That didn't seem to get triggered. Perhaps this is what Adobe was referring to in regards to Pre/PostUpdate() events working only in conjunction with the EntityLoad() method? To test this caveat, I set up a second demo that simply loads the previous object and updates it. This demo was created on a completely different page with a completely different Hibernate session:

  • <!---
  • Load the thought based on the primary key (which we know
  • will be "1" since we just rebuild the database).
  • --->
  • <cfset thought = entityLoad( "Thought", 1, true ) />
  •  
  • <!--- Reset the thought content. --->
  • <cfset thought.setContent( "Watching Tricia do squats is far too distracting." ) />
  •  
  • <!--- Persist the thought - this *should be* an UPDATE. --->
  • <cfset entitySave( thought ) />
  •  
  • <!--- Output the object (this will include the event log). --->
  • <cfdump
  • var="#thought#"
  • label="EntityLoadByPK() / EntitySave()"
  • />

This time, we are using the EntityLoad() as specified in the documentation in order to get the update events to work. And, when we run this code, we get the following CFDump output:

 
 
 
 
 
 
ColdFusion 9 ORM Event Handling. 
 
 
 

Still no PreUpdate() and PostUpdate() event tracking! But, what's really odd is that is that DateUpdated property does seem to have updated correctly. Of course, I think this might be a side-effect of the data type, TimeStamp (which I'll have to look more into), rather than my event handling since the update events were not tracked in my EventLog.

The PreUpdate() and PostUpdate() event handlers seem a little bit suspect. But what about the delete events? In this final demo, I am loading the object and deleting it:

  • <!---
  • Load the thought based on the primary key (which we know
  • will be "1" since we just rebuild the database).
  • --->
  • <cfset thought = entityLoad( "Thought", 1, true ) />
  •  
  • <!--- Delete this entity. --->
  • <cfset entityDelete( thought ) />
  •  
  • <!--- Output the object (this will include the event log). --->
  • <cfdump
  • var="#thought#"
  • label="EntityLoad() / EntityDelete()"
  • />

When we run this code, we get the following CFDump output:

 
 
 
 
 
 
ColdFusion 9 ORM Event Handling. 
 
 
 

Again, we get the proper PreLoad() and PostLoad() events firing, but there is no sign of any PreDelete() or PostDelete() events.

So far, the event handling in ColdFusion 9's ORM system seems to be a bit of a mystery to me. I could only get the Insert / Load events to fire; the Update / Delete events were never successfully intercepted. I have gone over the code several times and even copy/pasted the event names to weed out bugs; but, I can't seem to figure these event handlers out. Does anyone see me doing anything wrong here? Any feedback would be greatly appreciated.

NOTE: You can also perform global ORM event handling using a designated ColdFusion component (defined in the Application.cfc ORM settings).




Reader Comments

Another very good read Ben, I'm a big fan of these kinds of event handlers when it comes to placing hooks around database calls, I deal with quite a lot of objects that persist themselves in several different ways, for instance, in the DB but also a sort of Serialized version of the object onto the FS.

This way I can build the hooks right into the object and know that simply saving the object is enough to enter the values into the DB and also build the serialized version onto the FS without me having to work it manually with two method calls.

That behaviour of the update hooks does seem a little suspicious, I wonder why that is the case? perhaps someone from the Adobe team will shed a little light on it.

Rob

Reply to this Comment

@Robert,

On one hand, I hope it's not just a typo in my code (as that would be embarrassing)... but, on the other hand, I'd rather it be my error or lack of understanding than a CF bug :)

Reply to this Comment

This looks like a bug to me. You are clearly updating the entity after you load it so it "should" be calling both the pre and post update events, same goes for the pre/post delete events.

you could as an extended attempt try to implement the CFIDE.ORM.IEventHandler interface yourself and see if you get any better results.

Reply to this Comment

Interesting.

Any thoughts on what you'd use this for beyond what you could do with DB triggers?

(No, I'm not arguing against events on the theory that "real men use triggers". Nor am I trying to be inflammatory. Just trying to think of a case where you could go beyond triggers.)

Reply to this Comment

@Rick,

damn that is a good question!
a few things come to mind.
1) laziness
2) developer not being comfortable with triggers (eg. NOT a real man)
3) wanting to keep the code as database agnostic as possible (multiple db installs), which relates to 1.

Reply to this Comment

Rick, there are a whole truck load of things you can do with this that can't be handled by DB triggers, namely, non-db related stuff ;-)

Take my example for instance, we do work with a lot of multimedia files and content, lets take a vCard for instance.

I have an object called vCard.cfc and it has lots of properties for the contact like FirstName, LastName, TelephoneNo etc. Now, when I save this object I want to persist it into the database table, but also create the actual vCard (.vcs) onto the FS.

So, I create a function called 'saveToFS()' which contains the logic to build the serialized string from the properties and then write that to the FS, it might also use some cfimage stuff to create a small thumbnail example of what the content will look like on a phone screen or something like that. I can now trigger that saveToFS() method after saving the object to the DB using these new ORM hooks.

Rob

Reply to this Comment

@Ben, Another great post! I'll have a guess that the update events are not fired until they are actually updated at the end of the request. You could try forcing it by adding an ORMFlush() after the entity save and before the cfdump.

@Rick, you can use this for logging to a file (audit trail) or for injecting into your entities, or maybe updating a search collection or the cache across a cluster.

Reply to this Comment

@John,

You magnificent bastard! You are absolutely correct! Those events don't happen until the session changes are flushed. Damn you :)

Reply to this Comment

@Rick, Similar to Robert's use cases, I've used event handlers quite a bit for image/photo uploading.

In the postInsert, you can execute the code neccessary to resize and convert photos, create thumbnail versions, etc.

In the postDelete you can execute code to delete the photo from the file system.

Reply to this Comment

Interesting points, all, thank you. While I'm not personally keen on "side effect" coding, especially given the lazy flush-driven architecture, I do have to admit to the elegant simplicity of such approaches.

So ... who wants to be the first to shoot themselves in the foot by recursively having events generate events?

Reply to this Comment

Thank you very much for this great post Ben! The 'event handling has to be explicitly turned on in your Application.cfc ORMSettings' saved my ass ;)

Reply to this Comment

@Marco,

Awesome - glad to be able to help out. I can't wait to really get back into ORM testing.

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.