Self Validating Entities with CF9

August 23, 2009

My style of writing code is heavily influenced by Paul Marcotte's Metro and Bob Silverberg's blog. Both of those guys use rich business objects which can validate themselves. Essential this is a technique where you can populate an object with invalid data (from a form for instance). You then ask the object to check if the data it holds is valid. If it isn't valid you don't save the changes, but you can still use the object to re-populate a form.

The default settings for ColdFusion 9's ORM capabilities mean that you can't write code this way. The problem is that ColdFusion will automatically save any changes made to a persisted object at the end of the request regardless of whether you call EntitySave or not.

Thankfully, you can override this behaviour by adding this line to Application.cfc (thanks to Bob Silverberg and Mark Mandel for pointing this out to me)


this.ormsettings.flushatrequestend = false;

By adding this line of code, ColdFusion won't save any changes you make to an entity at the end of the request. It also won't save any changes even if you call EntitySave( myObject ), unless you call ORMFlush() before the end of the request, typically this will probably be straight after the EntitySave.

I've created and updated version of the port of Mark Mandel's tBlog sample application I did to include some basic validation (via an Abstract class) which you can download here.

It should be noted that ColdFusion 9 is set up to only run the SQL statements at the end of the request to improve performance, so changing the behaviour could have an impact on your application's performance.

If you are interested in a much more sophisticated validation, then you should check out Bob silverberg's ValidateThis! framework.


30 comments

  1. This is really the last piece I am working on for a new application I am writing. I wish there was an easier way to Inject an object into an entity. I create a Product Validator object in coldspring but I have no way to set it in my entity. I am using a similar approach to you but slightly different as I want to abstract the validation stuff. I will keep hacking away at it and let you know what I come up with. Keep up the great posts!

    Comment by Dan Vega – August 24, 2009
  2. This was very useful (AKA Educational). Thank you.

    Comment by david buhler – August 24, 2009
  3. Hey Dan. Yeah - I did simplify the validation stuff for the purposes of keeping the code simple. You can use the EventHandler to inject into an Entity.

    Comment by John Whish – August 24, 2009
  4. You would have to write the event handler in the entity though right? You would be creating the object there instead of passing in from coldspring. I will have to look at events and to see what is available because I would obviously only want 1 instance of the Validator object loaded.

    Comment by Dan Vega – August 24, 2009
  5. @Dan, you can use the site wide event handler to do it, it's a little bit messy but will work.

    Comment by John Whish – August 24, 2009
  6. Hey John, thanks for the post.

    I also take this approach. What are your thoughts about using the event model and preUpdate() method to call the validate() method?

    I guess you still have the bunk data in the object, and it might be a little janky/difficult to "revert" object to the previous state.

    Liking your blog and LOVING CF9... it is just TOOOOO good.

    Comment by John Allen – August 24, 2009
  7. John is correct that this can be accomplished using the application event handler. I had coded something to do this with the old ORMInterceptor and it worked quite well. I'm going to look into doing the same with the new EventHandler.

    Comment by Bob Silverberg – August 25, 2009
  8. Do you guys mean by using the events inside of the Entity itself? I could create the validator using the preLoad event but I was looking for a better solution using coldspring since everything else is managed there. I guess I will have to dig farther into the event stuff as I don't know a lot about it yet.

    Comment by Dan Vega – August 25, 2009
  9. Very cool stuff.

    Comment by Ben Nadel – August 25, 2009
  10. Thanks Ben :)

    Comment by John Whish – August 25, 2009
  11. @Dan, you can use the preLoad method of the site wide event handler to inject a bean from ColdSpring

    This code is from a few betas back but (hopefully!) should work

    /**
    * Event handler for ORM operations
    * @output false
    * @implements cfide.orm.IEventHandler
    */
    component
    {
      /**
      * This method is called before the data is loaded from the DB.
      */
      public void function preLoad( any entity )
      {
        
        var Validator = application.beanFactory.getBean( "Validator" );
        arguments.entity.setValidator( Validator );
      }
    ....
    }

    It does break encapsulation by accessing the application scope, and I'm sure this can be improved but I hope it helps.

    Comment by John Whish – August 25, 2009
  12. @Dan, I forget to mention that you need to enable the site wide handler in Application.cfc with this line:

    this.ormsettings.eventHandler = "model.interceptor.EventHandler";

    Comment by John Whish – August 25, 2009
  13. Hey John, the worst thing about CF9 is having to go back and write code for CF8 :)

    Hmm, interesting idea, I think there are a few things that could trip you up though. The preUpdate method may only fire at the end of the request unless you've set the flushatrequestend to false or called ORMFlush(). Also, the preUpdate method doesn't return anything so you'd still have to go through the steps of checking if it was valid. You might as well get the obj.hasErrors() method to call obj.validate internally.

    Let me know if you do try it out, as I'd be interested to hear how you get on.

    Comment by John Whish – August 25, 2009
  14. Great stuff John. I have a quick question about the events. If you create an application wide event handler like your example (this.ormsettings.eventHandler = "model.interceptor.EventHandler") can you still do event handling at the entity level or does it all now switch to your global event handler? Sorry, still learning the event stuff.

    Comment by Dan Vega – August 25, 2009
  15. Interesting stuff. I wonder if someone has looked into using Hibernate Validation framework.

    wheelersoftware.com/articles/hibernate-validator.html

    Comment by Qasim Rasheed – August 26, 2009
  16. @Dan, if you have a event handler in an entity and the site wide event handler enabled then, it will fire the method in the entity first, followed by the site wide method.

    Comment by John Whish – August 26, 2009
  17. @Qasim, I've not seen the hibernate validator before, looks interesting. Thanks for the link!

    Comment by John Whish – August 26, 2009
  18. even if you set this setting:

    this.ormsettings.flushatrequestend = false;

    If you make changes to an entity, and you do not use EntitySave(), but still call ORMFlush(), your data is still persisted to the DB persisted.

    My point is that while you might not explicitly call ORMFlush() in the current file where you're working with an entity, it might be called somewhere else further along in the request, thus unintentionally persisting your entity to the db.

    There is a way around this. If you call ORMCloseSession(), then change a property (i.e. setFirstName('Dutch'), the only way the object would be persisted is to use EntitySave() *and* ORMFlush().

    Now, it makes me wonder. Let's say I do validations as part of an isValid method or even as part of preInsert() or preUpdate(). Can I call ORMSessionClose() from within the entity itself? If so, what are the implications there?

    Regardless, it sounds like the only way to prevent an object from being automatically saved during a flush, regardless of what this.ormsettings.flushatrequestend is set to, is to call ORMCloseSession() after each EntityLoad().

    Comment by Dutch Rapley – October 23, 2009
  19. @Dutch: That is not totally accurate. It is true that if you make changes to a persistent object that those changes will be written to the database when the session flushes, even if you don't call EntitySave(), but there are other ways to deal with the issue than calling ORMCloseSession(). Personally I'd avoid using ORMCloseSession().

    You can evict an entity from the session by calling ormGetSession().evict(myObject). You can also get total control by wrapping all of your entity manipulation in a transaction. Then, if you decide that you don't want the changes committed to the database, you can call transactionRollback(). That latter approach is probably going to be my preferred method.

    Comment by Bob Silverberg – October 23, 2009
  20. @John

    Thanks for the clarification there. I'm a Hibernate noob. When it says "persistence framework," that's what it is, it persists, no matter what, unless you tell it not to.

    I've used Reactor, Transfer, and even ActiveRecord (to some extent in Rails). I've dug into the CF documentation. While it's good, it leaves some questions to be answered.

    I think there's a few gotchas in there that some ORM noobs won't understand, feel they are shortcomings, and will eventually abandon ORM support in CF. To really understand how ORM support works in CF, I feel it's important to understand Hibernate (which I'll be doing this weekend).

    I agree with you on using transactions to work around some shortcomings (nice to see script support there) and was going to mention it in my last comment.

    I knew the hibernate session object is available, but just don't know what's there yet. There is a CF equiv to evict with ORMEvictEntity(), which I guess can always be used in a manner similar to the following

    if (!user.isValid()) { ORMEvictEntity( "User", user.getId() ); }

    My one question there is - after you "evict" an object, can you change properties and pass it to EntitySave()?

    Comment by Dutch Rapley – October 23, 2009
  21. @Bob,

    sorry, I meant to prefix that last comment with your name and not @John, it's Friday afternoon - what can I say?

    Comment by Dutch Rapley – October 23, 2009
  22. A couple of things:

    1. ORMEvictEntity() will not evict the object from the Hibernate session - it is only used to evict objects from the secondary cache. Yes, this is yet another thing that is confusing about CF/Hibernate.

    2. I would assume that after evicting an object (using evict(), not ORMEvictEntity) you can change properties and then call EntitySave() to place it back into the session. There's one way to know for sure, right?

    Also, I totally agree that there is a lot that people are going to have to learn/understand about Hibernate sessions in order to work with CF ORM. I gave a presentation on it at CFinNC, and will be presenting it again to the Online ColdFusion Meetup group. I also hope to publish a number of blog posts on the subject.

    Comment by Bob Silverberg – October 23, 2009
  23. @Bob,

    As usual, nothing but pearls of wisdom from you!

    I'm looking forward to your preso on the Online Meetup, and wish I could have made it to CFinNC.

    And yes, I was wondering about the primary and secondary cache.

    Comment by Dutch Rapley – October 23, 2009
  24. Great advice as usual from Bob. I've tried using the evict() method and can tell you that it gets complicated quickly when you start building up complex relationships between objects, in addition to using transactions, you can also use the EntityReload method to re-populate the object from the database, but I think the transaction route is the way to go.

    Comment by John Whish – October 24, 2009
  25. I've found an area where we'll have to be careful with using transactions.

    I'm Application.cfc, I'm telling the application that I will handle flushing with
    <cfset this.ormsettings.flushatrequestend = false />

    Let's consider the following snippet:

    <cfset var user = entityLoad("User", 123, "true")/>
    <cfset user.setFirstName('John')/>
    <cftransaction>
    </cftransaction>

    We all know that a transaction closes previous sessions and starts a new one. If I have an entity that I've loaded and updated a property on, then begin a transaction, the session the user entity belongs to is flushed and the user entity is actually persisted to the database.

    If I take out the <cftransaction> tags, the user entity is never persisited (which behaves as expected).

    For the above example, I wouldd probably have UserService.cfc with create() and update() functions that would take a structure (i.e. form) as a parameter. Load the user entity and populate/modify it from within the transaction.

    I can see where EntityReload() may be a good choice on some occasions where you're only wanting to update a single property and not wanting to add yet another method to UserService.cfc if you don't have to.

    Comment by Dutch Rapley – October 26, 2009
  26. I guess you want to wrap a transaction around the calls to setters and then commit if required.

    Comment by John Whish – October 27, 2009
  27. The transaction has to wrap the EntityNew()/EntityLoad(), as well as the calls to all the setters. Any entities that are loaded before a transaction become detached when the transaction starts.

    Here's a preliminary, it should work. The insert() function is part of UserService.cfc

      public struct function insert(Struct user) {
        var results = {};
        results['success'] = false;
        
        transaction {
          try {
            // instantiate new user object
            var newUser = EntityNew("User");
            
            //set user properties here
            newUser.setFirstName(user.firstName);
            newUser.setLastName(user.lastName);
            
            // validate user
            if(!newUser.isValid()) {
              // user is not valid - do not save'
              transactionRollback();
              results['errorMsg'] = 'The data is invalid. Please review and correct errors below.';
              results['errors'] = newUser.errors();
            }
            else {
              // save user
              EntitySave(newUser);
              transactionCommit();
              results['success'] = true;
            }      
          
          } catch(Any excp) {
            transactionRollback();
            results['errorMsg'] = 'There was an error saving the data. Please try again';
          }
        } // end transaction

        return results;  
      }

    Comment by Dutch Rapley – October 27, 2009
  28. Hi Dutch, just doing some quick testing and not finding that I need to include the EntityLoad inside the transaction. As long as the setter calls are inside the transaction, I can roll back.

    Comment by John Whish – October 27, 2009
  29. @Dutch: You don't _have_ to call EntityLoad from within the txn, as long as you call EntitySave from within the txn. It's true that if you load an object outside of the txn and then start a txn that the object becomes detached, but you can "reattach" it (it's called Update in Hibernate) by calling EntitySave.

    Another comment, when you create a new entity, via EntityNew for example, no matter where or when you do it it will never be persisted to the database unless you call EntitySave on it. So you don't actually need the rollback in your example when the validation fails. Simply not calling EntitySave will ensure that the object is not persisted to the database.

    Comment by Bob Silverberg – October 27, 2009
  30. @John & @Bob

    Thanks for the tips, they're helpful and greatly appreciated as I work towards leveraging Hibernate in CF. This is some good stuff! You guys should really consider writing a series of articles on CFHibernate best practices. I've read through Chapter 8 on ORM in the dev guide a couple of times now. While it does a great job at introducing the power of Hibernate in CF and covering basic concepts, I felt there were a ton of gaps in understanding how to leverage it effectively.

    Thanks again!

    Comment by Dutch Rapley – October 27, 2009

Leave a comment

If you found this post useful, interesting or just plain wrong, let me know - I like feedback :)

Please note: If you haven't commented before, then your comments will be moderated before they are displayed.