In all my posts so far on Transfer, this is the one I was most looking forward to. I had known, in general, what Transfer did and how it works... kind of. But the event model was something I was completely in the dark with so I couldn't wait to start playing with it. Now that I have, I can say I'm both impressed, and a bit disappointed.
I've been pretty happy with Transfer so far, but this one feature bugged me in regards to how poorly the documentation covers the topic. I encourage you to read for yourself (Using the Transfer Event Model) and tell me if it is clear to you how event model is used? Yes, we get an API list, but nowhere do we see an actual example of using the API! I shouldn't complain so much. I actually have access to edit the wiki and, well, let's not talk about the documentation on my projects, but such an important feature really should have a bit more detail added.
Ok, so enough ranting. Mark knows I mean well (grin), I hope this blog entry can help clear things up for folks. Let's get started.
At a basic level, the Transfer Event Model is a way to automatically do things based on certain events. So for example, we know that when we make something new in Transfer, at some point that object is persisted to the database. We also know that if we retrieve an existing object, modify it, and then save it, an update is being performed. These are two of the various events that Transfer gives you access to.
A good example of where this comes in handy is with auditing. If you want to mark when an object was created and when it was last updated, you can obviously do this by hand, but the Transfer event model lets you do this automatically.
Transfer makes the actual code simple enough. You write a function - it gets passed event data - and you do what you want. What didn't maker sense to me is how you hooked up this function to Transfer.
The number one thing you must do is create a component. Even though you may just want to handle one event (and therefore use one function) it has to be packaged in the CFC.
The second thing you must do is ensure that the method name in the CFC matches with the required name per the docs (again, see the event model doc). So - if you want to handle the 'before I save the information to the database' event, or "Before Create", then you must name your method: actionBeforeCreateTransferEvent.
The final step then is to register your CFC with Transfer. This confused me as well. So if you build one CFC with handlers for multiple events, you must register each and every event with Transfer. You can't just make one call to Transfer with the one CFC.
Here is a simple example. I want to handle the 'before save' and 'before update' operations in Transfer. These are called BeforeCreate and BeforeUpdate. I create a CFC named observer.cfc and placed two methods within it:
<cfcomponent>
<cffunction name="actionBeforeCreateTransferEvent" hint="Actions an event before a create happens" access="public" returntype="void" output="false">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="Yes">
<cflog file="transferdemo" text="create called">
</cffunction>
<cffunction name="actionBeforeUpdateTransferEvent" hint="Actions an event before a update happens" access="public" returntype="void" output="false">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="Yes">
<cflog file="transferdemo" text="update called"></cffunction>
</cfcomponent>
For the two events, I named them per the spec and I ensure both had the event argument. I'll talk a bit more about that argument in a second. I used a simple cflog for both.
Now I need to register this component with Transfer. Going to the Application.cfc/onApplicationStart method I've been using for my sample application, remember that I made an instance of my Transfer like so:
<cfset application.transfer = application.transferFactory.getTransfer()>
I then made an instace of the CFC I just wrote:
<cfset application.observer = createObject("component", "empdir.model.observer")>
I now tell Transfer that this CFC is where it can find the event handler for BeforeCreate:
<cfset application.transfer.addBeforeCreateObserver(application.observer)>
And I also tell it to use this CFC for BeforeUpdate:
<cfset application.transfer.addBeforeUpdateObserver(application.observer)>
Altogether, the entire onApplicationStart now has:
<cfset application.transferFactory =
createObject("component", "transfer.TransferFactory").init(
"/empdir/configs/datasource.xml",
"/empdir/configs/transfer.xml",
"/empdir/configs/definitions")>
<cfset application.transfer = application.transferFactory.getTransfer()>
<cfset application.observer = createObject("component", "empdir.model.observer")>
<cfset application.transfer.addBeforeCreateObserver(application.observer)>
<cfset application.transfer.addBeforeUpdateObserver(application.observer)>
So I don't know exactly why - but this felt really awkward to me. The docs don't actually show a simple CFC nor do they show setting it up like so. I'm repeating myself, but I also didn't quite get why I couldn't pass the CFC one time to Transfer.
So that aside, once I re-inited my application, I went to the admin, created a new Employee, save it, then edited it again and saved it. When I checked the logs I saw that Transfer correctly noticed both events! Pretty cool when you get past the confusion. Now let's make it a bit real. I went into my Employee table added two new fields, created and updated. I reflected this in the XML:
<property name="created" type="date" />
<property name="updated" type="date" />
I then borrowed some code from Bob Silverberg's excellent blog post on Transfer events (My Take on Transfer ORM Event Model - BeforeCreate Example). Using the event object passed to Transfer, we can get access to the actual Transfer object being worked with:
<cfset var to = arguments.event.getTransferObject()>
Once we have the TransferObject, we can check to see if it is an Employee, or, follow Bob's example, and just see if it has a created field:
<cfif structKeyExists(to, "setCreated")>
<cfset to.setCreated(now())>
</cfif>
The complete method now looks like this:
<cffunction name="actionBeforeCreateTransferEvent" hint="Actions an event before a create happens" access="public" returntype="void" output="false">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="Yes">
<cfset var to = arguments.event.getTransferObject()>
<cfif structKeyExists(to, "setCreated")>
<cfset to.setCreated(now())>
</cfif>
</cffunction>
The handler for updates is very similar:
<cffunction name="actionBeforeUpdateTransferEvent" hint="Actions an event before a update happens" access="public" returntype="void" output="false">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="Yes">
<cfset var to = arguments.event.getTransferObject()>
<cfif structKeyExists(to, "setUpdated")>
<cfset to.setUpdated(now())>
</cfif>
</cffunction>
Once again I re-inited my application and played with some new data. Transfer noticed the presence of the created and updated properties and automatically took care of setting the values. What's cool is that I can just go to some other object, add the properties, and not worry about it. The auditing will be handled automatically.
As with my earlier entries, I've included a zip of my most recent code base so you can see this in action. I hope this helps clear things up a bit. If you are curious, here is a complete list of the events Transfer can handle:
- AfterNew: When the object is first created, but before it is persisted.
- BeforeCreate: Called before the object is persisted.
- AfterCreate: Called after the object is persisted.
- BeforeUpdate: Called before a persisted object is updated.
- AfterUpdate: Called after the already persisted object is updated.
- BeforeDelete: Called before an object is about to be deleted.
- AfterDelete: Called after the object is deleted.
Archived Comments
That's pretty awkward. So according to the article, if I have 50 data objects, and each one needed some sort of "aftercreate" handling, I'd have to write 50 checks to see which object is being passed in?
If true, Transfer would do better to make the object "decorator" a full-fledged delegate, so it can then call the single and specific "aftercreate" method for that object type (if it exists in the delegate). Which also tends to conveniently eliminates the duplicate object registration process.
At the very least, it should let you register an event handler for a specific type (employee). Coding a single function that switches on object type is simply bad design.
And needlessly passing every object created by the system to a single function so it can be tested is a poor performance choice.
* takes it on the chin, keeps on running *
@Marc, if you already have the event mechanism in place, and if you can get to the decorator from the transfer object (TO), then adding the decorator/delegate functionality I just described should be trivial.
Then you'd have the best of both worlds, easily customization of events for a given TO with minimal extra code via the delegate, plus the ability of external components to tie into the global system using the current mechanism.
I also think you should add the ability to register a handler bound to a specific type. That would fix the performance issue of broadcasting events for each and every object insert and update.
@ML: I'm not 100% sure I read your first paragraph right. If you have 50 _types_, and you want event handling for all for BeforeCreate, you only do ONE function call. Ie, one addBeforeCreateObserver.
Or do you mean that if you had 50 different types of things to do you would have a huge ass CFIF? Well, in that case, you are right. If you had 50 unique things to do for BeforeCreate than you would need a rather large CFSWITCH. However, to be fair to Mark, I'd say that is a VERY extreme case. I see the Event Model is being a bit more generic. Using my autoset create/update example, I think you can imagine that it would probably apply to ALL content in my employee db.
That's my 2 cents.
Ray, just a note that if you are using ColdSpring, you can simplify this a fair bit by letting your Observer configure itself with the Transfer Factory:
<bean id="myTransferObserver" class="com.MyObserver">
<constructor-arg name="transfer">
<ref bean="transfer" />
</constructor-arg>
</bean>
And then in the constructor for the Observer:
<cffunction name="init" access="public" returntype="any" hint="Constructor.">
<cfargument name="transfer" type="transfer.com.Transfer" required="true" />
<cfset arguments.transfer.addBeforeUpdateObserver(this) />
<cfset arguments.transfer.addBeforeCreateObserver(this) />
<cfreturn this />
</cffunction>
So CS passes Transfer into the Observer, and it registers itself for whatever events it is interested in.
Michael's idea seems to be going on the assumption that every object needs it's own custom event handling code. In reality, this is not the case.
Observers are meant to handle cross-cutting concerns in a manner similar to AOP, things like populating a createdDate or lastModififierID, which tend to apply to many if not all Transfer objects in the system. If Transfer did that, you'd end up with duplicated handling code in every Transfer object, which would be absolutely awful.
Further, if you have code that you want to run before update or save that is specific to each Transfer object, you already have the means to do this! Just override any of the save or update methods in the Transfer object and run whatever code you want to. What would be the point of designating some "special" method within a TO that did nothing but execute some code that you could just have run in your save() method? Again, Transfer Observers are meant to handle things that apply across many objects, they are not meant for code specific to individual objects.
Thanks for sharing this Brian. Your comments on cross cutting is exactly what I took from the docs as well, but you explained it much better. :)
Thanks Ray and Brian. Very timely. I was trying to figure out a one-place way to default a date to null. I had to do this on lots of tables and didn't really want to do it in a configure method.
The AfterNew event worked great for me. I did have to add lazy-init="false" to the observer bean definition in CS for it to do the registering automatically.