The Bad Programmer

Grails: Update One-to-Many Collection



One of the things you frequently have to do is update the many side of a one-to-many relationship. Since Grails of course uses Hibernate the many side is a collection (a Set unless you change it) so when this relationship is updated you need to add new elements to the collection and then remove any element that should no longer be in the collection. Here I am going to show you an easy way to accomplish this. Note, that I will be showing this with Grails domain objects but the same thing will work just fine if you are using Hibernate directly.

In our example we will use an imaginary use case where we want to keep track of clubs and members of the clubs. So we will have the domain objects Club, ClubMember, and Person. There is a one-to-many relationship between Club and ClubMember and a one-to-one relationship between ClubMember and a Person.

I have included the three domain objects below. Notice that I have also included a equals() and hashcode() method. The equals() method is very important to what we are trying to accomplish. Note, that I wrote the equals() method with bad style to save space, I wouldn’t recommend that someone writes code like that.

Club

class Club {
    String name

    static hasMany = [clubMembers: ClubMember]
    static constraints = {
    }

    boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false
        Club group = (Club) o
        if (name != group.name) return false
        return true
    }

    int hashCode() {
        return name.hashCode()
    }
}

ClubMember

class ClubMember {
    Club club
    Person person

    static belongsTo = [Club]
    static constraints = {
    }

    boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false
        ClubMember that = (ClubMember) o
        if (person.fullName != that.person.fullName) return false
        return true
    }

    int hashCode() {
        return person.hashCode()
    }
}

Person

class Person {
    class Person {
    String fullName

    static constraints = {
    }

    boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false
        Person person = (Person) o
        if (fullName != person.fullName) return false
        return true
    }

    int hashCode() {
        return fullName.hashCode()
    }
}

Let’s just assume a user is editing the members of a currently existing Club and that user has submitted a form that contains a multiple select box that contains a list of all the names of people that should now be in the Club. Some of the names might already be in the Club, some names might have been removed, and some of the names might be new members. What is the easiest way to handle this situation? The answer is to take advantage of Collection bulk operations and let the JDK handle it for you. Once you have the right objects in the collection Hibernate will take care of the rest. By default hibernate uses Sets to map collections which are well suited to this; however, these methods can be used on Lists as well (they are part of the Collection interface, not the Set interface). My example is shown as if it was a method in a Service and the id and submitted names have already been pulled from the form parameters by the controller and passed to this method:

Club update(int id, Set fullNames) {
        def club = Club.findById(id)
        def clubMembers = [] as Set
        //Construct ClubMembers by assigning them to the Club we are editing and
        //looking up an existing person from the DB
        fullNames.each {
            //Assumes the Person being added to the Club already exists in the DB
            def person = Person.findByFullName(it)
            clubMembers 
        }

        //Now we let the collection bulk operations from the JDK and Hibernate
        //take over. Add all the clubMembers to the Set. This will make this
        //set contain all previous members of the club and new
        //members. Right now the Set includes members that might need to be
        //removed
        club.clubMembers.addAll(clubMembers)

        //Remember that clubMembers contains only the names of people that
        //should be in the Club now. So retain just those members. Equality
        //of a ClubMember is determined by the Person's fullName
        club.clubMembers.retainAll(clubMembers)
        club.save()

        return club
}

If you had logSql set to true for the datasource in the Grails Datasource.groovy config file you would see that Hibernate does inserts for any new Person in the club and does deletes for any Person no longer in the Club. Remember that this functionality depends on having a proper implementation of equals() for all the domain objects (something that is a Hibernate best practice anyway).