OOPhoto - Handling Database Transactions With Ease

Posted August 5, 2008 at 6:35 PM

Tags: ColdFusion

The latest OOPhoto application can be experienced here.

The latest OOPhoto code can be seen here.

A while back, you might remember me freaking out a bit about handling database transactions in OOPhoto, my latest attempt at learning object oriented programming in ColdFusion. One of the things that is nice about procedural code is that you know where all the code is being called from (that page) and wrapping a whole bunch of code in CFTransaction tags is never a problem. When you are using object oriented programming, on the other hand, it's a totally different story; you never know where the objects are being called from. That's part of the beauty and the initial frustration of object oriented programming - the objects don't have and shouldn't have to know about anything else outside of themselves (well, almost nothing else).

Because of the environmental-agnostic properties of method execution in object oriented programming, what I ended up doing was creating two functions for methods that might require a database transaction. One uses transactions, the other one does not. Take, for example, the Save() method on my service objects; for saving a transient object (a non-cached object), I created two save methods:

  • Save()
  • SaveWithTransaction()

I got the basis of this idea (using two methods) from a brief conversation that I had with Peter Bell a couple of weeks ago. At first, I thought I would end up having to duplicate the logic in both methods, which was disappointing to say the least. And, in fact, this is how I went about implementing the methods initially. But then, half way through my first SaveWithTransaction() method, it dawned on me - why not just create the CFTransaction tags and then turn around and call the existing Save() method. After all, the only added functionality I wanted in SaveWithTransaction() was the CFTransaction tag itself; every other piece of logic was already built into the existing Save() method.

And so it was that I created a SaveWithTransaction() method that looked like this:

 Launch code in new window » Download code as text file »

  • <cffunction
  • name="SaveWithTransaction"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I take a photo object and persist it (using a database transaction).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Photo"
  • type="any"
  • required="true"
  • hint="I am the photo object to be persisted."
  • />
  •  
  •  
  • <!--- Wrap the whole interaction in a transaction. --->
  • <cftransaction action="begin">
  •  
  • <cftry>
  •  
  •  
  • <!---
  • Because the functionality for saving already
  • exists, let's just turn around and call our
  • existing Save() method (that works in a non-
  • transaction way).
  •  
  • Return the object that is passed back from
  • our Save() method.
  • --->
  • <cfreturn THIS.Save( ARGUMENTS.Photo ) />
  •  
  •  
  • <!--- Catch any errors. --->
  • <cfcatch>
  •  
  • <!--- Roll back transaction. --->
  • <cftransaction action="rollback" />
  •  
  • <!--- Rethrow error. --->
  • <cfrethrow />
  •  
  • </cfcatch>
  • </cftry>
  •  
  • </cftransaction>
  • </cffunction>

As you can see, the SaveWithTransaction() method just adds the CFTransaction functionality including the transaction rollback if the save action errors out. Other than that, the method simply turns around and calls THIS.Save() which handles the object persistence implementation.

Because the method calls of an object are supposed to be environment-agnostic, I had to make it a rule that no "Save" method would turn around and invoke the transactional version of another object's save method. By enforcing this, I am ensuring that a transaction is only created at the primary object persistence level and not for any of the composed objects. What this ultimately means is that a transaction-involving method will only ever be invoked by the Controller, never by a member of the domain model itself.

I am very satisfied with this solution. It allows me to cleanly organize my transactions. But almost more importantly, it allows me to add parallel versions of a feature (ie. Save, Delete) without duplicating any of my logic. That just feels clean to me.

Download Code Snippet ZIP File

Comments (14)  |  Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page


Related Blog Entries




Reader Comments

@Ben,

That seems like a great idea. I too am trying to get a handle on the OOP approach, and a few weeks ago I encountered the Transaction issue.

I had a transaction in my method which worked fine when the method was the only action I was calling from my code. When I later called that method as part of a larger series of steps, it ended up nested within another transaction, which generated an error.

Having the ability to call two methods seems like a nice approach, and the ease of creating the SaveWithTransaction() method keeps it pretty painless!

Posted by Rick Hopper on Aug 5, 2008 at 9:59 PM


@Ben,

What I like about this approach, Ben, first is that it's a simple start (doesn't require you to have some sort of additional DataSet object or something like an ORM) and then second that doing it the way you have done here means that you don't have to have the transaction code directly in your Controller. Yes, the Controller has to 'know' about transactions, but that's OK, it's making all your decisions in any case. What the Controller doesn't have to do is decide what 'DB transaction' means ... so, going back to some of the comments very early in the OOPhoto project: this way someone could theoretically change how the data layer deals with transactions without having to alter the Controller at all.

Nice solution that should be simple enough to not obstruct the ongoing project.

-jfish

Posted by Jason Fisher on Aug 5, 2008 at 10:08 PM


Great stuff as usual, but this almost seems to cry out as another reason to have method overloading in CF.

Posted by Jim Collins on Aug 5, 2008 at 11:01 PM


Love the series Ben. I was just thinking, this method of having dual methods where one is transactional and one isn't might be a great candidate for a "missing method" handler.

Your onMissingMethod function could just check for the suffix "withTransaction" (or any pattern you decide) on a method call and if there's a pattern match, it could wrap a dynamic call to the non-transactional version of the function passing along all parameters using nearly all of the same code in your current "saveWithTransaction" function.

You could place the onMissingMethod handler in a base class that you extend so that you don't have to actually have 2 versions of each function in each of your CFCs. It would just become an understood convention that if you need to use a transaction, you call the function you want with the suffix "withTransaction"

Keep up the brain dumping, love it.

- Kurt

Posted by Kurt Bonnet on Aug 5, 2008 at 11:22 PM


Just as a side note, this could be written just as:

<cftransaction>
<cfreturn THIS.Save( ARGUMENTS.Photo ) />
</cftransaction>

And you would be done.

If an error happens inside the <transaction> tags, then the db is automatically rolled back.

I've seen a few people use try/catch blocks, just like this, and it's not necessary.

Now, what you need is some clever state storage, so that you can next saveinTransaction() calls ;o)

Keep up the good work!

Posted by Mark Mandel on Aug 6, 2008 at 4:06 AM


@Jason,

Ahhh, so that's what the "J" stands for :)

I think one thing we could do to overcome having the Controller to know about the transactions is to make the generic "Save" method the one with Transactions. Then, once inside the model, we could have the Service objects call SaveWithoutTransaction().

It doesn't seem to roll off the tongue like the other one, but at least this way, to the outside world, you are always calling "Save" or "Delete" and it is actually the model that is then 100% worrying about how to handle transactions internally.

@Kurt,

It's funny you mention that because on walk home last night, after posting this, I thought the same thing. Why not just let the OnMissingMethod() handle it. After all, if all the method is going to do is use the CFTransaction tag and then call around and call another method and pass along ALL of the same arguments, its can be easily factored out. And, especially with the ArgumentCollection feature, it's almost a no-brainer.

@Mark,

I actually used to do transactions that way. It seems really easy and straight forward. But then, one day, someone told me that that was not how it was meant to be used; that to rely on the error to automatically roll-back was not actually the intent.

Of course, I never bothered to check. I did, however, just now, look up the documentation, and you are 100% correct:

If a cfquery tag generates an error within a cftransaction block, all cfquery operations in the transaction roll back.

That's what happens when you blindly follow people's advice :( Shame on me.

Although looking at that documentation again, it does mention specifically CFQuery tag errors. I can't imagine that the CFTransaction tag would selectively roll back for cfquery errors but not other errors thrown in the code.

Hey, what is the SaveInTransaction() idea? I am not sure what the intent would be?

Posted by Ben Nadel on Aug 6, 2008 at 8:36 AM


@Ben,

Love the idea of pushing the call type out of the Controller and letting the Model decide what Save and Delete have to mean in any given context. Guess that also makes sense when thinking about a few fat Services that are in turn implementing combinations of many, more table-based DAO objects or whatever: the top tier of the model is the only one that really knows how it's assembling DAO calls at any rate, so the Transaction decision almost has to be there.

As for the rollback use of cftransaction, yes, you can manually flag it, but you don't need to. In good coding on the DB side, such as using T-SQL to build stored procedures in SQL Server, you want to manually trap for errors and flag the rollbacks when appropriate, but CF has (once again) encapsulated that for us, which is awesome.

Also, you are right that any which occurs before the closing /cftransaction tag will cause the rollback, even if the error is unrelated to the database interactions.

Posted by Jason Fisher on Aug 6, 2008 at 9:49 AM


Sorry, that last sentence was supposed to be:

"... you are right that any error which occurs ..."

-jfish

Posted by Jason Fisher on Aug 6, 2008 at 9:51 AM


@Jason, Mark,

Thanks for the CFTransaction insights. I will put my transaction code on a diet immediately (except where, of course, I do need to catch the error and actually do something with it, such as with an API call).

Posted by Ben Nadel on Aug 6, 2008 at 10:00 AM


I have updated my service objects, moving the transaction control into the BaseService.cfc and out of the individual services:

http://www.bennadel.com/index.cfm?dax=blog:1311.view

This uses OnMissingMethod() for generic transaction control.

Posted by Ben Nadel on Aug 6, 2008 at 3:38 PM


Its true about the transactions in OO. Ive had serious problems when calling DAOs with coldfusion with session and database swapping. In the end i had to use CFLOCK inside the DAO but it slows the whole show down alot.

Posted by GPS dude on Aug 9, 2008 at 10:59 AM


@Ben, FYI - I was testing cftransaction with and without the cftransaction action="rollback" inside of the cfcatch block and it did not work without it. In other words it did not rollback the records when the insert query bombed out. Maybe just wrapping the query blocks with cftransaction works in some cases, but in my case it did not. In case your curious, the sequence of tags in my test was cftransaction,cftry,cfloop,cfif,cfquery(insert),cfcatch. Hope that makes sense and hope this helps.

Posted by Joey Krabacher on Sep 23, 2008 at 5:54 PM


@Joey,

I think the rollback happens automatically if you do not include a CFTry / CFCatch. But, sine the CFTry / CFCatch block traps the error without letting it bubble up, the parent CFTransaction block has no idea that an error occurred.

Posted by Ben Nadel on Sep 23, 2008 at 6:02 PM


@Ben,

Good point. I will have to agree with you on that one.

Posted by Joey Krabacher on Sep 23, 2008 at 6:34 PM


Post Comment  |  Ask Ben


Home   |   Web Log   |   ColdFusion   |   Projects   |   Resume   |   Job Form   |   Search   |   Contact
Epicenter Consulting - Custom Software Solutions for Business Evolution HostMySite.com - The Leader In ColdFusion Hosting