Here’s a scenario we see fairly often in our Grails applications.
- Parent object has a collection of Child objects
- We want the Parent’s create and edit GSPs to allow us to add/remove/update associated Child objects
- The controller should correctly persist changes to the collection of Child objects, including maintaining Child object ids so any other objects referencing them don’t get screwed up
I found a really nice solution that avoids adding a lot of code to the controller to sift out added/changed/deleted collection members. The original page seems to have disappeared, so here are copies from archive.org (easier to read) and Google cache (PDF).
I was disappointed that the original page is gone, and I found some small errors in the sample code, so I thought it would be nice to document here.
Here’s a sample project I created to go through this. Source code: one-many.tar.gz
The original example used Quest objects that can hold many Task objects. I’ll follow the Grails docs and use Author objects that can hold many Book objects.
First, create the Author class.
01.
import
org.apache.commons.collections.list.LazyList;
02.
import
org.apache.commons.collections.FactoryUtils;
03.
04.
class
Author {
05.
06.
static
constraints = {
07.
}
08.
09.
String name
10.
List books =
new
ArrayList()
11.
static
hasMany = [ books:Book ]
12.
13.
static
mapping = {
14.
books cascade:
"all,delete-orphan"
15.
}
16.
17.
def getExpandableBookList() {
18.
return
LazyList.decorate(books,FactoryUtils.instantiateFactory(Book.
class
))
19.
}
20.
21.
}
(Here’s a minor correction I had to make to the original document’s code. They declared getExpandableBookList as returning a List, but that gave unknown property errors. Using a plain def fixed that.)
This adds a bunch of useful behaviour right away. The mapping block declares that books will be deleted when they’re removed from the Author.books collection, so we don’t need to clean up anything manually. By initializing books to an empty ArrayList when an Author object is created, and by using the getExpandableBookList() method, we can easily add and remove Book objects to the Author.books collection.
Next, the Book class is pretty simple.
01.
class
Book {
02.
03.
static
constraints = {
04.
}
05.
06.
String title
07.
boolean
_deleted
08.
09.
static
transients = [
'_deleted'
]
10.
11.
static
belongsTo = [ author:Author ]
12.
13.
def String toString() {
14.
return
title
15.
}
16.
17.
}
Nothing too fancy here, but pay attention to the _deleted property. That’s what we’ll be using to filter out Book objects that need to be removed from the Author.book collection on updates.
For the views, I like to combine the guts of the create and edit GSPs into a template that they can both render.
01.
<
div
class
=
"dialog"
>
02.
<
table
>
03.
<
tbody
>
04.
<
tr
class
=
"prop"
>
05.
<
td
valign
=
"top"
class
=
"name"
><
label
for
=
"name"
>Name:</
label
></
td
>
06.
<
td
valign
=
"top"
class
=
"value ${hasErrors(bean:authorInstance,field:'name','errors')}"
>
07.
<
input
type
=
"text"
id
=
"name"
name
=
"name"
value
=
"${fieldValue(bean:authorInstance,field:'name')}"
/>
08.
</
td
>
09.
</
tr
>
10.
<
tr
class
=
"prop"
>
11.
<
td
valign
=
"top"
class
=
"name"
><
label
for
=
"books"
>Books:</
label
></
td
>
12.
<
td
valign
=
"top"
class
=
"value ${hasErrors(bean:authorInstance,field:'books','errors')}"
>
13.
<
g:render
template
=
"books"
model
=
"['authorInstance':authorInstance]"
/>
14.
</
td
>
15.
</
tr
>
16.
</
tbody
>
17.
</
table
>
18.
</
div
>
That uses _books.gsp to render the editable list of books.
01.
<
script
type
=
"text/javascript"
>
02.
var childCount = ${authorInstance?.books.size()} + 0;
03.
04.
function addChild() {
05.
var htmlId = "book" + childCount;
06.
var deleteIcon = "${resource(dir:'images/skin', file:'database_delete.png')}";
07.
var templateHtml = "<
div
id
=
'" + htmlId + "'
name
=
'" + htmlId + "'
>\n";
08.
templateHtml += "<
input
type
=
'text'
id
=
'expandableBookList[" + childCount + "].title'
name
=
'expandableBookList[" + childCount + "].title'
/>\n";
09.
templateHtml += "<
span
onClick
=
'$(\"#" + htmlId + "\").remove();'
><
img
src
=
'" + deleteIcon + "'
/></
span
>\n";
10.
templateHtml += "</
div
>\n";
11.
$("#childList").append(templateHtml);
12.
childCount++;
13.
}
14.
</
script
>
15.
16.
<
div
id
=
"childList"
>
17.
<
g:each
var
=
"book"
in
=
"${authorInstance.books}"
status
=
"i"
>
18.
<
g:render
template
=
'book'
model
=
"['book':book,'i':i]"
/>
19.
</
g:each
>
20.
</
div
>
21.
<
input
type
=
"button"
value
=
"Add Book"
onclick
=
"addChild();"
/>
And that uses _book.gsp to render the individual records. It’s a bit overkill to call out to another template for only a few lines of HTML, but that’s how the original example did it and I’ll do the same for consistency.
1.
<
div
id
=
"book${i}"
>
2.
<
g:hiddenField
name
=
'expandableBookList[${i}].id'
value
=
'${book.id}'
/>
3.
<
g:textField
name
=
'expandableBookList[${i}].title'
value
=
'${book.title}'
/>
4.
<
input
type
=
"hidden"
name
=
'expandableBookList[${i}]._deleted'
id
=
'expandableBookList[${i}]._deleted'
value
=
'false'
/>
5.
<
span
onClick
=
"$('#expandableBookList\\[${i}\\]\\._deleted').val('true'); $('#book${i}').hide()"
><
img
src
=
"${resource(dir:'images/skin', file:'database_delete.png')}"
/></
span
>
6.
</
div
>
Here’s where I changed a bit more from the original example. I used jQuery because the selectors make things easy. Basically we render the books from the already-persisted author object, and keep track (using the _deleted field) of any that the user wants to remove. We also keep track of new objects to add.
One of the reasons I really liked this technique was how little impact there is on the controller. We just need to add this to the update method in AuthorController.
01.
def update = {
02.
def authorInstance = Author.get( params.id )
03.
if
(authorInstance) {
04.
if
(params.version) {
05.
// ... version locking stuff
06.
}
07.
authorInstance.properties = params
08.
def _toBeDeleted = authorInstance.books.findAll {it._deleted}
09.
if
(_toBeDeleted) {
10.
authorInstance.books.removeAll(_toBeDeleted)
11.
}
12.
// ... etc.
The original example added similar code to the save method, but I don’t think it’s required for new objects (since they don’t have any already-persisted books to delete, only new books to create) so I only put it in the update method. I also changed it from find{} to findAll{} to guarantee that we get a list, and checked that we have objects to remove before calling the removeAll().
And it works great! Let’s look at some screenshots of the application in action.
First, we can create a new author and add some books right here instead of creating them separately and then matching them up.
Hit “Create” and it creates the Author and Book objects.
Edit the author we just created and see how we get a form that looks the same.
However, it’s worth noting that the books displayed here are the already-persisted ones, so the form is keeping track of their ids and whether we should keep them or delete them on update. Let’s delete the first one and add two more new books.
Now when we hit “Update” the controller has to be smart enough to remove that first book from the Author.books collection, then create two new Book objects and add them to the collection. And naturally, it is.
In addition to creating and destroying Book objects, we can update them. For example, let’s change the title of that first book to be the long version.
No problem!
So that’s one-to-many relationships in Grails forms. I hope it’s useful.