Gregg is Co-founder and CTO of Wild Bamboo Rocket, LLC. He lives in Wichita, KS with his wife, 3 kids, a cat and a dog. Gregg has posted 6 posts at DZone. View Full User Profile

[Grails] Persisting Indexed Properties and their Parent

03.15.2010
| 4501 views |
  • submit to reddit

Most of the time, GORM handles the persisting of domains and their associations transparently.  Specifically, for a one-to-many relationship, assuming it is bi-directional, all you need to do is have a persisted instance of the ‘one’ side and simply call its dynamic addTo[Domains] method.  But this isn’t always so clear-cut.  I recently ran into a situation where I needed to persist not only the ‘many’ side, but also the ‘one’ side in a single transaction.  Let me explain with some examples.

I’ve defined 2 domains; Event and Reminder.  An Event has many Reminders and an Reminder belongs to an Event.  This is the bidirectional relationship I spoke of earlier.  In the following snippets you can see how we define this relationship.

class Event {

String name
static hasMany = [reminders:Reminder]

}
class Reminder {

Integer duration = 10
static belongsTo = [event:Event]

}

As seen by the image below, using Grail’s scaffolding to generate our views we see that Grails assumes Reminders will be added to Events after we create an Event.

 

 

 

 

 

 

And then if we click the Add Reminder link we add a few reminders and we can see those as part of the Event now.

 

 

 

 

 

 

 

Most of the time this use case will work.  Sure, we might clean up the IU a bit but generally creating the ‘one’ side first, then adding the ‘many’ side after is appropriate and works great.  Unfortunately, my use case was different.  I needed to present a UI that allowed the user to fill out the Event form and add Reminders at the same time, without the need for Event to be persisted yet.  Then on save() persist the Event along with all its Reminders.

Initially, this didn’t seem too difficult.  Grails has great support for indexed properties.  The reminder part of my GSP form looked like the following:

<g:textField name="reminders[0].duration" value="${eventInstance.reminders?.duration}"/>

After submitting the form and relying on the default scaffold save() method it failed with the following exception:

not-null property references a null or transient value: com.wbr.Reminder.event; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value: com.wbr.Reminder.event

I then wrote a test (I know, should have done this first) to prove what was happening and to find a way to solve it.

void testSave() {

def controller = new EventController()
controller.params.name = 'Test event 1'
controller.params.reminders = []
controller.params.reminders[0] = new Reminder(duration: 10)
try {
controller.save()
fail()
}catch(org.springframework.dao.DataIntegrityViolationException dive) {
def message = dive.message
assertEquals message, "not-null property references a null or transient value: com.wbr.Reminder.event; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value: com.wbr.Reminder.event"
}
}
As you can see from the test and the exception, when reminders are set on event via the new Event(params) magic, Reminder is missing a key property; the Event.  This is because we technically don’t have an Event yet therefore, we don’t have a persisted Event with an ID to pass to Reminder.  As we know, however, this is solved by the addTo[Domains] method.  This seemed like an easy fix.  Simply loop over eventInstance.reminders and call Event’s addToReminders() method.

I wanted to keep the original test so that I could always prove my use case so I wrote a new test and added a new save method to the EventController with a few modifications.

def saveRight = {
def eventInstance = new Event(params)
def reminders = eventInstance.reminders
if (eventInstance.save(flush: true)) {
reminders.each {reminder ->
eventInstance.addToReminders(reminder)
}
flash.message = "${message(code: 'default.created.message', args: [message(code: 'event.label', default: 'Event'), eventInstance.id])}"
redirect(action: "show", id: eventInstance.id)
}
else {
render(view: "create", model: [eventInstance: eventInstance])
}
}
void testSaveRight() {

def controller = new EventController()
controller.params.name = 'Test event 2'
controller.params.reminders = []
controller.params.reminders[0] = new Reminder(duration: 10)
controller.saveRight()

def event = Event.findByName('Test event 2')
assertNotNull "Event is null", event

assertEquals 1, event.reminders.size()

}

The new test was failing.  Event is coming back null.  The reason for this is because eventInstance still has a collection of reminders from the new Event(params) magic.   One modification to the code and we should be good.

def saveRight = {
def eventInstance = new Event(params)
def reminders = eventInstance.reminders
eventInstance.reminders = null // null our reminders so the save works
if (eventInstance.save(flush: true)) {
reminders.each {reminder ->
eventInstance.addToReminders(reminder)
}
flash.message = "${message(code: 'default.created.message', args: [message(code: 'event.label', default: 'Event'), eventInstance.id])}"
redirect(action: "show", id: eventInstance.id)
}
else {
render(view: "create", model: [eventInstance: eventInstance])
}
}

And now our test is green.  Just move this method to a service so that it is transactional and we're all good.  I hope that this has been helpful.  I’ve yet to see this discussed and there is a good chance I’m making this more difficult than need be so if anyone has any tips to improve the code, please let me know.

Published at DZone with permission of its author, Gregg Bolinger.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

John Rellis replied on Tue, 2010/03/16 - 5:58am

Hey,

 I don't think there is any need to "eventInstance.save(flush: true)" before adding the reminders.  Although I have seen border cases where you need to do this.  You can create your new Event, call addToReminders and then "eventInstance.save(flush: true)".  This will save and validate the reminders as well as the event.  Also I think if you do not explicitly call save() on your reminders (or on event in the sequence I have mentioned) your reminders will not be validated as outlined here "http://www.grails.org/GORM+-+CRUD" under the"Update" section.  You may already be aware of all this :)

Thanks for sharing,

John

 


 

Gregg Bolinger replied on Tue, 2010/03/16 - 1:47pm in response to: John Rellis

Yep, thanks John. I realized that about an hour after I submitted the article. Thanks for pointing it out. I was going to add a comment regarding that today. I just hadn't got around to it.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.