Grails Tip # 3: refresh()


refresh() is not an undo()

In your application, you may come across a time when you need to revert or undo
changes done on a domain object. discard() comes to mind, but there may be instances where
that is not an option. refresh() also looks like an tantalizing option, but there is one critical
feature of refresh() you need to be aware:

If you have a trasient object associated to the domain object you are calling
refresh() on, you will get a
org.hibernate.AssertionFailure: null identifier
error.

Consider the following example to see why you might need to use refresh
and what you have to watch out for.

The Domain

Consider the following domain, a Car has several models
(for each year) and most of the details of a “car” is
in the model domain. There are a few description fields on
the Car domain object however.

The Code

The Car domain object:

		package com.ex
			
		class Car {
			String name
			String description
			static hasMany = [models:Model]
		}
	

The Model domain object:

		package com.ex
			
		class Model {
			String name
			int year
			Engine engine
			. (other properties)
			.
			.
			static belongsTo = [car:Car]
		}
	

Users are able to modify car details and model details at the same time.
They can also add new models to the car as well. All changes to the
Car (and it’s models) must be atomic. That is, all changes
must be committed as one transaction. Lets take a
look of the UI:

<%= image_tag "articles/grailstip3/flow.png" %>

When the user decides to change their changes,
they exectue the saveChanges action on CarController.

Because of the belongsTo relationship on Models to Cars, we
can save any user changes to Car model and it’s models by
calling save() on the Car object.

		package com.ex
			
		class CarController {
			def saveChanges = {
				.
				.
				.
				// save car under edit
				car.save(failOnError:true)
				.
				.
				.
			}
		}
	

The Problem

Due to a requirement, the users have the ability
to revert changes on a particular Model, but
still save their changes on the Car.

		package com.ex
			
		class CarController {
			def saveChanges = {
				.
				.
				.
				// model to revert
				if (some condition)
					// rever model
				.
				.
				.
				// save car under edit
				car.save(failOnError:true)
				.
				.
				.
			}
		}
	

One might think we can call discard(), but this will
not work in this situation because further down
the code, we end up calling save on Model’s parent,
the Car. Even if we discard a particular Model of a Car,
it will still be saved when Car is saved due to the
cascade behaviour brought on by belongsTo relationship.

We also are unable to change the cascade settings between
Car and Model. In this particular instance, we could
prevent cascade on save, but in other cases, we do need it.

The other other option is to call refresh() on the Model
being reverted. And we do this.

		package com.ex
			
		class CarController {
			def saveChanges = {
				.
				.
				.
				// model to revert
				if (some condition)
					model.refresh()
				.
				.
				.
				// save car under edit
				car.save(failOnError:true)
				.
				.
				.
			}
		}
	

Done right? well, not quite. Under some use cases, we
discover this exception being thrown:
org.hibernate.AssertionFailure: null identifier

The error only occurs when new objects are associated to
a Model. For example, if the user adds a new Engine to
the reverted Model. What’s going on?

When you call refresh on the Model, Hibernate will traverse
though the object’s object graph and overwrite its data
with what is in the database.

If there is a transient object associated to the Model,
Hibernate will not find an id associated with this entity
and thus will throw a null identifier assertion failure.

The Solution

To be able to use the refresh() method, you must break all
associations with transient objects in the graph in the
current Hibernate session.

This can be done by calling clear() on any hasMany associations
and re-assigning any variables assigned to transient objects to null.

		package com.ex
			
		class CarController {
			def saveChanges = {
				.
				.
				.
				// model to revert
				if (some condition){
					model.engine = null
					model.tries.clear()
					model.refresh()
				}
				.
				.
				.
				// save car under edit
				car.save(failOnError:true)
				.
				.
				.
			}
		}
	

Many people believe this to be hackish — a workaround
and, well, though this might be true, Hibernate’s API is being
used correctly and instances such as this, transient objects
are known and can be properly be removed from the session.

Though Grails itself does not offer an “undo” method, I
would argue the solution above deals with the problem at hand
using the API as it was designed. However, one of the reasons
I’ve posted this is to see if there are any others who have faced
similar problems have found a different way of solving it.

Leave a Reply

Your email address will not be published. Required fields are marked *