Silly GORM tricks, part I: Lists

In GORM, when one class has a hasMany relationship with another, a java.util.Set is injected into the class. Sometimes, though, I want to use a List instead in order to maintain ordering. The Grails reference documents (see section 5.2.4 specifically) discuss how to do that, but there are other issues that I needed to solve in order to make this work.

Consider an application that demonstrates the issue involved. It has only two domain classes, Quest and Task. A Quest consists of many Tasks.


class Quest {
    String name

    static hasMany = [tasks:Task]

    String toString() { name }
}

class Task {
    String name

    static belongsTo = [quest:Quest]

    String toString() { name }
}

I’m using a bi-directional one-to-many association here, mostly because the dynamic scaffolding works well with it (not the best reason, of course, but it makes it easy to illustrate the point). The hasMany assignment means that the Quest class will have a Set injected into it called tasks, and the belongsTo relationship means that all the cascade relationships (save, update, and delete) will work, too.

Before I take advantage of that in my boostrap code, though, I used the dynamic scaffolding just to make sure I could add quests and tasks through the normal views.

class QuestController { def scaffold = Quest }

class TaskController { def scaffold = Task }

As it happens, everything does work as advertised. A simple integration test that demonstrates it is shown below, which works.


void testAddTasks() {
    Quest q = new Quest(name:'Seek the grail')
    q.addToTasks(name:'Join King Arthur')
        .addToTasks(name:'Defeat Knights Who Say Ni')
        .addToTasks(name:'Fight Killer Rabbit')
        .save()
    assertEquals 3, Task.count()
}

Everything so far is standard stuff. One of the defining characteristics of a Set, however, is that it does not support ordering. If I want ordering, there’s a chrysalis stage I can go through on the way to a List, which is to use a SortedSet (assuming Task implements the Comparable interface).


class Quest {
    String name
    SortedSet tasks

    static hasMany = [tasks:Task]

    String toString() { name }
}

class Task implements Comparable {
    String name

    static belongsTo = [quest:Quest]

    String toString() { name }

    int compareTo(Object o) { 
        return name.compareTo(o.name) 
    }
}

The dynamic scaffolding still works, too. I can add a task, as long as there is a quest available to add it to. The tasks are sorted by name, as they should be. I added the above quest and tasks to my bootstrap code, too, so they were available as soon as my server started.

Incidentally, there’s a down side to using a SortedSet that I hadn’t realized right away. When I first wrote my application, I added a degree of difficulty to my tasks and tried sorting by them.


class Task implements Comparable {
    String name
    Integer difficulty

    // ...

    int compareTo(Object o) { 
        return difficulty - o.difficulty 
    }
}

That sorts tasks by difficulty all right, but there’s another consequence. I can only add a single task of a given difficulty to a particular quest! I can’t have two tasks both with the same difficulty. A SortedSet may be sorted, but it’s still a set. 🙂

So now I move on to using a List. As the reference documentation says, to do that, just declare tasks to be a reference of type List.


class Quest {
    String name
    List tasks

    // ...

    static hasMany = [tasks:Task]
}

Now there’s trouble. The server starts, and the bootstrap code works, because it adds tasks to an existing quest before saving them. The dynamic scaffolding has a serious problem, though. When I go to the tasks list and try to add a new task, everything is fine until I try to save the new task.

If I try to add a new task through the “Create Task” page, I get an exception: “org.hibernate.PropertyValueException: not-null property references a null or transient value“.

The reason is addressed in the reference documentation. First, changing to a list means that the database table for tasks now has an index column. Second, as the documentation says, you can’t save a task by itself any more — you have to add it to a quest first. It’s okay to say:

def t = new Task('Answer the bridgekeeper')

but I can’t save it by itself, or that index column will be a problem. I have to add the task to a quest first before saving.

Quest.get(1).addToTasks(t).save()

That works. Otherwise I get a null in that index column, which throws an exception and down goes the server.

So, knowing that, how do I fix the system?

Well, I definitely have to abandon the dynamic scaffolding. The built-in save method isn’t going to work, because it saves the task independently of the quest. So, it’s time to generate the real controllers.

After generating the task controller and views, the save method looks like:


def save = {
    def task = new Task(params)
    if(!task.hasErrors() && task.save()) {
        flash.message = "Task ${task.id} created"
        redirect(action:show,id:task.id)
    }
    else {
        render(view:'create',model:[task:task])
    }
}

I need to add the task to a quest and then save the quest. Fortunately, one of the parameters in the request is the id of the quest, under params.quest.id. That means my first try is to change the above code to this:


def save = {
    def task = new Task(params)
    def q = Quest.get(params.quest.id)
    q.addToTasks(task)
    if (!task.hasErrors() && q.save()) {
        // ... etc ...
}

Unfortunately, this doesn’t work either. When I fill in the page to make a new task and try to save it, I get a NullPointerException due to the fact that the task still has a null id.

This, I believe, turns out to be a Hibernate problem. Hibernate doesn’t do the save when I ask it to, but rather is waiting until the “right moment” to do the commit. Unfortunately, I need that commit right away.

Fortunately, there’s an answer to that, too. The save method takes a flush parameter which can be set to true.

Therefore, I changed the above code to:


def save = {
    def task = new Task(params)
    def q = Quest.get(params.quest.id)
    q.addToTasks(task)
    if (!task.hasErrors() && q.save(flush:true)) {
        // ... etc ...
}

Now, at long last, it all works. The key was to add the task to the quest and save the quest with flush set to true.

It’s possible that there are alternative solutions, but this one worked for me. If you know of better alternatives, please let me know.

13 responses to “Silly GORM tricks, part I: Lists”

  1. is there some way to use a linked hash set or fix the compare to include the name as part of the sort key?

  2. You could keep a SortedSet and change the implementation of the compareTo method to:
    return (difficulty – o.difficulty)*10 + name.compareTo(o.name)

  3. That’s certainly true. I can make the comparison use whatever fields I have, and that would take care of the duplicates problem. In my real application, though — the one that got me to look at this in the first place, I needed to sort my tasks and access them in a sorted order. That one needed a list.

    I appreciate the comment, though.

  4. Also, as Ray said, it’s easy enough to use a LinkedHashSet. All I have to do is declare the reference in the Quest class as:

    class Quest {

    String name

    Set tasks = new LinkedHashSet()

    static hasMany = [tasks:Task]
    // …

    }

    That way I can go back to the original scaffolding, too. The only down side is that the linked hash set maintains its entries in insertion order only. If that’s the order I want (which would be true for, say, comments or blog entries), then no problem. I don’t see a constructor for LinkedHashSet that takes a Comparator, though, so if I want a different ordering, I think I have to either use a SortedSet or use a List and order the entries myself.

  5. very insightful post my friend.

    If you have more of them coming to your attention, please doPost!

    😉

  6. Really interesting post, had me rivetted like a suspense thriller till the end (am I a geek or what 🙂 ) Every now and then I had some thoughts and you had addressed exactly those in the very next para. Thanks and I’ve bookmarked your blog.

  7. […] I started with a simple two-class domain model that I discussed in my last GORM post. […]

  8. but if you modify your first test to:
    void testAddTasks() {
    Quest q = new Quest(name:’Seek the grail’)
    q.addToTasks(name:’Join King Arthur’)
    .addToTasks(name:’Defeat Knights Who Say Ni’)
    .addToTasks(name:’Fight Killer Rabbit’)
    .save()
    assertEquals 3, q.tasks.count()
    }

    it won’t successfully run.
    Do you have an answer for that?

  9. Useful post as always. Keep up the good work. I have become your regular reader.

  10. I have a a one to many relationship where a Quest has like thousands of Tasks, for which I have to do:

    new Quest(name: “name1”).addToTasks(name: “task1”).
    ………
    ………
    ………
    addToTasks(name, “task2000”).save()

    Wouldnt this be inefficient? Is there a better way to do adding than this? Any advices?

  11. I believe the issue of requiring a flush may be outdated. I am learning Grails using grails-1.3.7 and I am able to save Task using

    def save = {
    def q = Quest.get(params.quest.id)
    def taskInstance = new Task(params)
    taskInstance.quest = q
    q.addToTasks(taskInstance)
    if (taskInstance.save(flush: false)) {
    flash.message = “${message(code: ‘default.created.message’, args: [message(code: ‘task.label’, default: ‘Task’), taskInstance.id])}”
    redirect(action: “show”, id: taskInstance.id)
    }
    else {
    render(view: “create”, model: [taskInstance: taskInstance])
    }
    }

  12. When I specified a ‘Sorted Set’, like you did with, “SortedSet tasks” and
    implemented Comparable on my domain objects, I got the sort on the hasMany that I was after.

    Thanks for posting this, Ken.

  13. Good
    Is there any way to stop the existing criteria execution?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.