Wednesday, December 3, 2008

Stealth little gotcha in grails (with a happy ending of course)

So i noticed a little Gotcha in Grails the other day. This is all down to the way hibernate works and and how grails uses it.

The way hibernate works:

When you load objects using hibernate this is done using an hibernate session. Once an object is loaded by the hibernate session it automatically becomes a managed object as far as hibernate is concerned. By managed, amongst other things i mean to automatically do dirty checking on any changes to the object.

Now when hibernate is setup for auto flushing (as is the default case with grails) it will flush any changes to the object to the database on the following events:

  • Whenever a query is run
  • Directly before a transaction is committed
  • Directly after a grails action completes if no exception is thrown

So this leads to some interesting Gotchas if you are not careful especially in the update action:

So here is an example:

Say i have the following domain object:

class EBook{
String name;
String summary;
String filename;

static constraints = {
name(blank:false)
summary(nullable:true)
}

}


Now in my controller i have this:

def update = {
def ebook = Ebook.get(params.id)
if (ebook) {
ebook.properties = params;

if(!ebook.hasErrors() && ebook.validate()){
if(!ebook.fileName){
ebook.generateFileName() //generates a default filename based on the name
}
if(ebook.save()){
flash.message = "Your changes have been saved"
render(view: 'show', model: [ebook: ebook])
}else{
render(view: 'edit', model: [ebook: ebook])
}

}else{
render(view: 'edit', model: [ebook: ebook])
}

}else{
render(view: 'edit', model: [ebook: ebook])
}

}


Now the problem with this code is. If validation fails then the ebook object still gets saved to the database. This is because of the rules discussed above. The ebook object has been changed via ebook.properties = params so hibernate knows it is dirty. So when the action finishes hibernate will flush the dirty changes to the database. This means i get bad data in my database.

So what to do ? Well like everything in Grails it turns out to be VERY easy to solve. The discard() method to the rescue.

def update = {
def ebook = Ebook.get(params.id)
if (ebook) {
ebook.properties = params;

if(!ebook.hasErrors() && ebook.validate()){
if(!ebook.fileName){
ebook.generateFileName() //generates a default filename based on the name
}
if(ebook.save()){
flash.message = "Your changes have been saved"
render(view: 'show', model: [ebook: ebook])
}else{
render(view: 'edit', model: [ebook: ebook])
}

}else{
ebook.discard()
render(view: 'edit', model: [ebook: ebook])
}

}else{
render(view: 'edit', model: [ebook: ebook])
}

}


The discard() method will detatch the ebook object from the session. This should then solve the problem. The only possible issue then would be LazyInitializationException depending on your object. Anyway for now that is a solution for my problem but may not a solution for all problems like this.

Hope this helps :)

8 comments:

Roshan Shrestha said...

If the object were to get saved automatically, then why even bother with "ebook.save()"?

Peter Delahunty said...

Good question.

These are my answers as to why i call save explicitly:

1) grails validation is a called again. Just to further check the object after i change the filename.

2) if save fails i am able to do something about it. In this case namely show the edit page. If i don't call save and have it execute at the end of the action. I don't get a chance to do anything about it and prob end up going to a grails error page.

3) maintainability and readability. Calling the save() method explicitly show the reader of the code the full intention.

peace

Brian M. Franklin said...

I believe this has been fixed in Grails 1.1 (beta1). I tested it out, and the behavior no longer appeared as it did in 1.0.4.

Reference to the JIRA issue:
http://jira.codehaus.org/browse/GRAILS-3163

Brock Heinz said...

Peter Ledbrook also detailed this pretty deeply back in July.

Roshan Shrestha said...

if "ebook.save()" fails, then don't you have to call "ebook.discard()"?

Roshan Shrestha said...

Never mind my earlier comment, if save() fails, then the database is not updated, so there is no need to call discard().

Peter Delahunty said...

Thanks Brock.. I see that update for 1.1 that would be most welcome.

Roshan. If you see the code you see i don't call discard if save() fails only if validate() fails. Anyway like i said in the post it is not a good solution because you may still end up with session closed exceptions with lazy loading.

Anonymous said...

This was supposed to be fixed in version 1.1, but from Graeme's comment on http://jira.codehaus.org/browse/GRAILS-3163, the issue still remains:

"ok it was fixed in one of the earlier betas but there seems to have been an unfortunate regression. We are working on a functional test to prevent it happening in the future, but as it stands 1.1 has the same behavior as 1.0.x at the moment. Meaning you have to call discard() on the entity or set it to read-only upon a validation error as a workaround"