Monday, December 1, 2008

Grails Many To Many

Grails doesn't yet handle many-to-many relationships in the scaffolding.

Most of the stuff available online recommends using relationship domain class to fudge the many-to-many relationship. If I have a many to many relationship of User to Role, I could have a UserRoleRelation that has a one-to-many relationship with both a User and Role, which Grails should handle cleanly.

It is also pretty easy to set up a simple direct ManyToMany relationship manually, as follows:

In my Role.java class I have

@ManyToMany(targetEntity=User.class, mappedBy="roleCollection")
private Set userCollection;

In my User.java class I have

@ManyToMany(targetEntity=Role.class, cascade = {CascadeType.MERGE})
@JoinTable(name="USER_ROLE_XREF",
joinColumns=@JoinColumn(name="User_Name", referencedColumnName = "User_Name"),
inverseJoinColumns=@JoinColumn(name="Role_Name", referencedColumnName = "Role_Name")
)
private Set roleCollection;

I cross-refrence by name instead of id to make the table more human readable. This may have the unfortunate side effect of breaking the generate-views command.

In the user/edit.gsp, I add a select box with the list of roles available

<tr class="prop">
<td valign="top" class="name">
<label for="roleCollection">Roles:
</td>
<td valign="top" class="value ${hasErrors(bean:user,field:'roleCollection','errors')}">
<g:select name="newRoleCollection" from="${Role.list()}"
value="${user?.roleCollection?.id}" optionKey="id" multiple="multiple" />
</td>
</tr>

If you only want to present a subset of the Roles (which happens to be the case in my prototype app), you can use Role.findAllByXXX instead of Role.list()

Finally, in my controllers/UserController.groovy, I add the code to put humpty dumpty back together again.

user.roleCollection.clear()

//probably a groovier way to do this, but it works...
//if newRoleCollection is a string instead of a string[], each iterates over each character
boolean newRoleCollectionIsStringArray = params.newRoleCollection.class.name != 'java.lang.String'

if (newRoleCollectionIsStringArray) {
params.newRoleCollection.each{
user.roleCollection.add( Role.findById( Integer.valueOf( it ) ) )
}
} else {
user.roleCollection.add( Role.findById( Integer.valueOf( params.newRoleCollection ) ) )
}

It isn't pretty but it does the trick.

No comments:

Post a Comment