one-to-many relationships in Grails forms

October 1, 2009 | by Tim

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.

    import org.apache.commons.collections.list.LazyList;
    import org.apache.commons.collections.FactoryUtils;

    class Author {

        static constraints = {
        }

        String name
        List books = new ArrayList()
        static hasMany = [ books:Book ]

        static mapping = {
            books cascade:"all,delete-orphan"
        }

        def getExpandableBookList() {
            return LazyList.decorate(books,FactoryUtils.instantiateFactory(Book.class))
        }

    }

(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.

    class Book {

        static constraints = {
        }

        String title
        boolean _deleted

        static transients = [ '_deleted' ]

        static belongsTo = [ author:Author ]

        def String toString() {
            return title
        }

    }

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.

    <div class="dialog">
        <table>
            <tbody>
                <tr class="prop">
                    <td valign="top" class="name"><label for="name">Name:</label></td>
                    <td valign="top" class="value ${hasErrors(bean:authorInstance,field:'name','errors')}">
                        <input type="text" id="name" name="name" value="${fieldValue(bean:authorInstance,field:'name')}"/>
                    </td>
                </tr>
                <tr class="prop">
                    <td valign="top" class="name"><label for="books">Books:</label></td>
                    <td valign="top" class="value ${hasErrors(bean:authorInstance,field:'books','errors')}">
                        <g:render template="books" model="['authorInstance':authorInstance]" />
                    </td>
                </tr>
            </tbody>
        </table>
    </div>

That uses _books.gsp to render the editable list of books.

<script type="text/javascript">
    var childCount = ${authorInstance?.books.size()} + 0;

    function addChild() {
        var htmlId = "book" + childCount;
        var deleteIcon = "${resource(dir:'images/skin', file:'database_delete.png')}";
        var templateHtml = "<div id='" + htmlId + "' name='" + htmlId + "'>\n";
        templateHtml += "<input type='text' id='expandableBookList[" + childCount + "].title' name='expandableBookList[" + childCount + "].title' />\n";
        templateHtml += "<span onClick='$(\"#" + htmlId + "\").remove();'><img src='" + deleteIcon + "' /></span>\n";
        templateHtml += "</div>\n";
        $("#childList").append(templateHtml);
        childCount++;
    }
</script>

<div id="childList">
    <g:each var="book" in="${authorInstance.books}" status="i">
        <g:render template='book' model="['book':book,'i':i]"/>
    </g:each>
</div>
<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.

    <div id="book${i}">
        <g:hiddenField name='expandableBookList[${i}].id' value='${book.id}'/>
        <g:textField name='expandableBookList[${i}].title' value='${book.title}'/>
        <input type="hidden" name='expandableBookList[${i}]._deleted' id='expandableBookList[${i}]._deleted' value='false'/>
        <span onClick="$('#expandableBookList\\[${i}\\]\\._deleted').val('true'); $('#book${i}').hide()"><img src="${resource(dir:'images/skin', file:'database_delete.png')}" /></span>
    </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.

    def update = {
        def authorInstance = Author.get( params.id )
        if(authorInstance) {
            if(params.version) {
                // ... version locking stuff
            }
        authorInstance.properties = params
        def _toBeDeleted = authorInstance.books.findAll {it._deleted}
        if (_toBeDeleted) {
            authorInstance.books.removeAll(_toBeDeleted)
        }
        // ... 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.

Create Author

Hit “Create” and it creates the Author and Book objects.

Show Author

Edit the author we just created and see how we get a form that looks the same.

Edit Author

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.

Edit Author

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.

Show Author

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.

Edit Author

No problem!

Edit Author

So that’s one-to-many relationships in Grails forms. I hope it’s useful.

Bookmark and Share

30 Responses to “one-to-many relationships in Grails forms”

  1. ThisIsWhatIWasLookingFor Says:

    Your posting is great! Exactly what i’m looking for since there is not complete example like this in grails manual.

    Thanks!

  2. Tim Says:

    Thanks. I also wanted the Grails manual to cover this. It seems like it would come up pretty often.

  3. Eddy de Boer Says:

    I was glad to find this site, there isn’t many about this subject online yet!

    I also entered a small fix for your demo project.

    The _toBeDeleted list needs to be checked for null’s. Null’s appear in AuthorInstance.books when you create more books and delete one(not the last) before pressing the update or save button. For this reason you need the _toBeDeleted check in the save action of the controller also.

    Improve the check with Elvis checking for the null iterator in the findAll of the update action and the save action.

    def _toBeDeleted = authorInstance.books.findAll {it==null?:it._deleted}

  4. Brad Rhoads Says:

    Great start!

    How about adding a unique constraint on Book.title?

  5. Tim Says:

    Thanks, Eddy and Brad! Those are both good suggestions, although I could see cases where multiple books could have the same title. Grails automatically adds an “id” property to each domain class to guarantee a unique identifier, so you don’t have to worry about conflicts in the title. I’ll leave the project files as-is, but note here that I think both of those changes are worth considering for anyone else who reads this.

    Also, you could probably simplify the _toBeDeleted null checking to this.
    def _toBeDeleted = authorInstance.books.findAll {it?._deleted}

  6. Malte Hübner Says:

    Just a little note: I think “all,delete-orphan” must be “all-delete-orphan”. The grails doc was incorrect until 1.2. I think.
    http://grails.org/doc/latest/guide/5.%20Object%20Relational%20Mapping%20%28GORM%29.html#5.5.2.9%20Custom%20Cascade%20Behaviour

  7. vnug Says:

    Thanks for the article. I have got most of it working except for “adding a book”. My javascript error console gives out message that “$” is undefined in create functionality when I am trying to add the books to the childList. I am using Grails1.2 and jquery1.3.2. Appreciate any pointers.

  8. vnug Says:

    Nevermind …. the problem was the syntax was changed for 1.2 for adding jquery plugin.

    Instead of (in main.gsp under layouts) –

    It should be -

    Thanks.

  9. LA Says:

    will this “pattern” work if the Book has many “Pages” (dummy example)?

  10. Tim Says:

    LA, I’m not sure I follow your example, but at first glance it sounds like it should be fine. You’d just do the same thing for the book class so it can edit the pages inline.

  11. Federico Says:

    Tim, thanks for the great post. Really helpful – exactly what I was looking for.

    A couple of issues I found when I tried the example (might be because I used Grails 1.3.1):
    (1) After saving an author with books, I couldn’t delete books from the list (they’d remain in the database). After debugging I found that the “authorInstance.properties = params” was not working correctly (the “_deleted” request parameter would be set to true, but the corresponding “authorInstance._deleted” property was always set to false). I changed “_deleted” to “deleted” (i.e. removed the underscore in the domain class & corresponding GSPs) then the code worked fine. I’m not sure why the underscore was causing an issue but it could be something weird relating to Grails 1.3.1.
    (2) After I got (1) working, I ran into another issue. If I created, say, 3 NEW books (BookA, BookB and BookC in that order), then immediately remove BookB BEFORE hitting “save/update”, Grails actually persists BookA with an index field of 0 and BookC with an index field of 2 (instead of shifting BookC up to position 1 which would make more sense since B never really existed). I found the index “gap” was then causing issues when going to re-edit that particular author since Grails starts trying to retrieve book[2] from a collection of just two items.

    There are a couple of ways round the 2nd issue – what I did was treat new books the same as existing ones (i.e. instead of calling “.remove()” in the GSP like you’d done, I kept a hidden “deleted” field for both existing AND new books, then called “hide()” in both cases when a book needed to be marked for deletion). Only thing is this means you DO have to ensure you include the same “authorInstance.books.removeAll(_toBeDeleted)” logic in your controller “save()” method as well (otherwise you can potentially end up with index “gaps” again).

  12. Federico Says:

    Sorry, just to clarify, I meant “authorInstance.books[i]._deleted” in point (1)

  13. RCachATX Says:

    Worked in Grails 1.3.2 on first try. Awesome! Thanks so much.

  14. Karol Says:

    Hello! I’m using your solution to make the a little complicated system, but to this later. When I was working on my version of yours _book.gsp the Netbeans shows me a little syntax error on line ” var deleteIcon = ${resource(dir:’images/skin’, file:’database_delete.png’)};” – it says:
    “illegal string body character after dollar sign”.

    I was thinking it was some artifact from previous changes to files, but it’s persistent and won’t leave after reopenings the file. I use Netbeans 6.9 and Grails 1.3.3.

    I have also next question – I need to make adding two, connected object with as one List element added to “parent” object. I have main class ( Author counterpart ) Wniosek that has many Osobowosc ( Identity ) and TypOsobowosci ( Type of Identity ), both as sperate classes, but both needed, as one “record” in List. How to make the similar solution to yours for two models object as one “record”? Make two rendering files I guess, but what inside the class itself?

    End result should be like this:

    Create Wniosek:

    Typ:

    Osobowosci: –

    etc.

  15. ballistic_realm Says:

    Nice tutorial. Very helpful. Thanks

  16. omarello Says:

    Tim, thanks a lot for this great tutorial. You just saved my life, can’t wait to go back home and try this. I’ve been looking all over for that sort of explanation. I really think this should be included somewhere in the grails docs.

  17. Train of Thought | Grails one-to-many dynamic forms Says:

    [...] to find a good way to implement one-to-many dynamic forms in Grails, I have finally come across this post which does a very good job at explaining the [...]

  18. Arne Says:

    _books.gsp

    11. $(“#childList”).append(templateHtml);

    // ———————— insert ————————————–

    var str = “expandableRefList["+ childCount +"].title”;
    document.getElementById(str).focus();

    //—————— To set focus in newly created field. ———–

    12. childCount++;

  19. seeker66 Says:

    Great post – Extremely helpful. Thanks!

  20. Rafael Says:

    Very nice!!! Thanks man!!!

  21. Greg Says:

    thanks for this – just what i was looking for!

  22. isaid Says:

    how could be the solution to photo list with type file inputs, in the save() just have to get the file request , but what about update(), in the edit.gsp should to be type text inputs? read only?
    regards.

  23. Alex Says:

    hey guys.

    I was wondering if you could help me.
    I am doing the same thing with grails 2.0.0.RC1 and on IE works fine, but in Firefox or Chrome the values never reach the server. From what i saw in the source of the generated HTML , the new components aren’t added to the DOM in Firefox/Chrome.
    Any hints on how to resolve this ?
    Thanx.

  24. Sapan Says:

    Hey Tim
    I have downloaded and opened your project at well as the book->author project but in both of those I am getting the same error I am using grails 2.0, I will really appreciate any help that you can provide.

    null id in blog.omarello.Phone entry (don’t flush the Session after an exception occurs). Stacktrace follows:
    Message: null id in blog.omarello.Phone entry (don’t flush the Session after an exception occurs)
    Line | Method
    ->> 43 | doCall in blog.omarello.ContactController$_closure4$$ENLORkU6
    - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - –
    | 886 | runTask in java.util.concurrent.ThreadPoolExecutor$Worker
    | 908 | run . . in ”
    ^ 662 | run in java.lang.Thread

  25. Stephane Rainville Says:

    Have you done some nice error management?

    When I have errors in the child items I get duplicate entries in errors in a format I do not understand

    lineItems[0][1].description ???? (why the second [] )
    lineItems[1][2].description ????
    lineItems[0].description Great
    lineItems[1].description Great

    If I loop thru the linItems in my controller and call validate I get the errors on the lineItems (if not they are null)

    How do you manage your errors in a parent child form?

  26. Curt Says:

    Anyone had any luck running this with Grails 2.x?

  27. Curt Says:

    I was having the same $ undefined issue as vnug above. Looking into it with Grails 2.x. Had to put in the layouts\main.gsp to get it to recognize jquery even though jquery was spcefied in the Buildconfig.groovy in the plugin section.

  28. Curt Says:

    I was having the same $ undefined issue as vnug above. Looking into it with Grails 2.x. Had to put in in the header portion the layouts\main.gsp to get it to recognize jquery even though jquery was spcefied in the Buildconfig.groovy in the plugin section.

  29. Jeroen Says:

    Reaction on Federico post of June 15th.

    I had the same problem by deleting books by a new author, and I’m new in this stuf. The reaction of Federice was helpful but I misses an example yow the Java script must look like. It takes me a couple of hours to change the java script, so for newbies here’s the code.

    var childCount = ${authorInstance?.books.size()} + 0;

    function addChild() {
    var htmlId = “book” + childCount;
    var deleteIcon = “${resource(dir:’images/skin’, file:’database_delete.png’)}”;
    var templateHtml = “\n”;
    templateHtml += “\n”;
    templateHtml += “\n”;
    templateHtml += “\n”;
    templateHtml += “\n”;
    $(“#childList”).append(templateHtml);
    childCount++;
    }

  30. Jeroen Says:

    Tim the java script is not correct shown on the site. I don’t now how to get it right on the side. If you want ik I can mail it.

Leave a Reply