Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

[Grails] Persisting Indexed Properties and their Parent

DZone's Guide to

[Grails] Persisting Indexed Properties and their Parent

· Java Zone
Free Resource

Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code! Brought to you in partnership with ZeroTurnaround.

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.

The Java Zone is brought to you in partnership with ZeroTurnaround. Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code!

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}