Learning ColdFusion 9: Trying To Understand ORM Event Handling
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:
- If you return "true" from the PreUpdate() method, you can cancel the update action.
- 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:
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:
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:
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).
Want to use code from this post? Check out the license.
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
@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 :)
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.
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.)
@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.
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
@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.
@John,
You magnificent bastard! You are absolutely correct! Those events don't happen until the session changes are flushed. Damn you :)
@Ben, haha - well, I guess I had to get something right eventually! :)
@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.
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?
Just like db-triggers, oh I've done that before :) thats a mistake you typically only make once!
@ Ben,
I had the same problem. This information helped me to resolve things:
http://bit.ly/14ejFr
@John,
Just confirming that you are indeed correct:
www.bennadel.com/blog/1691-Learning-ColdFusion-9-Understand-ORM-Events-Thanks-John-Whish-.htm
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 ;)
@Marco,
Awesome - glad to be able to help out. I can't wait to really get back into ORM testing.