Grails Tip # 2: Hibernate Session


A different object with the same identifier value was already associated with the session

The Domain

Say we are managing a store with shelves full of items. Whenever an
item on a shelf is empty, we need to create a restock order form.
Lets represent this as 3 domain classes: Shelf, Item, RestockOrder

The Code

A restock order is created when an existing item on a shelf runs out,
BUT, a order is also created for brand new items. The client has requested
that when creating new items, restock orders can also be created.

The Shelf domain object:

			package com.ex
			
			class Shelf {
				int number
				static hasMany = [items:Item, restockOrders:RestockOrder]
			}
		

The Item domain object:

			package com.ex
			
			class Item {
				String name
				String description
				double price
			}
		

The RestockOrder domain object:

			package com.ex
			
			Item item
			static belongsTo =[shelf:Shelf]
			static mapping = {
				item cascade:'save-update'
			}	
		

The editing of a Shelf takes place over several pages, similar
to a web-flow, thus the Shelf object is created and stored in
the HTTP session (therefore detached from the Hibernate session).
On subsequent HTTP requests, the user adds more data to this
object in the session. Finally, after filling out the last
page, we are able to persist the domain object to the session.

On the last page, if the user made important changes to Shelf,
the user is showing a confirmation page comparing the old values
with the current one. This was done by simply populating
the model with the current version in the session and also
re-loading the same model from the database. If the user did not
make any changes that require confirmation, we save right away.

<%= image_tag "articles/grailstip2/flow.png" %>
In ShelfController:
The index() method loads the required shelf and puts it into the
HTTP session, detaching it from Hibernate session.

		    def index = {	
				Shelf s = Shelf.findByNumber(1)
				session.putValue("shelf", s)
			}
		

The save controller

			def save = {
				// get from sesion
				def shelfUnderEdit = session.getAt("shelf")
				
				// bind update
				shelfUnderEdit.parameters = params
				
				Shelf originalShelf = Shelf.findByNumber(1)
				
				// user hasn't confirmed, might have to show confirmation page.
				if (showConfirmation(shelf, original) && params.noComfirmation){
					render("showUpdates", model:[shelfUnderEdit:shelfUnderEdit, originalShelf:originalShelf])
					return
				}
				
				// save
				shelfUnderEdit.save(failOnError:true)
			}
		

However, when we try to run the code above, we get the
“A different object with the same identifier value was already associated with the session”
error.

The Problem

Hibernate tries to keep one instance of a persisted object
in the session at one time. In this example, for any “Shelf”
in the database, Hibernate tries to make sure only 1 instance
of a Shelf object represents that Shelf.

The problem resides in the save(), there is the original Shelf we
detached and put into the HTTP session. When we try to re-attach
it, Hibernate finds that another one is found in the session.
This second object was created when we loaded the same Shelf from
the database to do our comparison. Two objects representing the
same Shelf now resides in the session and much to the dislike to
Hibernate.

The Solution

Though there are several ways to solve this problem, the method
we took was to discard the re-load Shelf object that was used
for the comparison. Therefore, when save() is called on the
Shelf that is under edit, it is reattached to the Hibernate
session and is the only object representing that particular
Shelf in the database. Thus Hibernate is able to persist the
changes without error.

			def save = {
				// get from sesion
				def shelfUnderEdit = session.getAt("shelf")
				
				// bind update
				shelfUnderEdit.parameters = params
				
				Shelf originalShelf = Shelf.findByNumber(1)
				
				// user hasn't confirmed, might have to show confirmation page.
				if (showConfirmation(shelf, original) && params.noComfirmation){
					render("showUpdates", model:[shelfUnderEdit:shelfUnderEdit, originalShelf:originalShelf])
					return
				}
				
				originalShelf.discard()
				
				// save
				shelfUnderEdit.save(failOnError:true)
			}
		

References

Leave a Reply

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