Last updated by masih 5 years ago

Many-to-Many Mapping without Hibernate XML

One pattern that shows up repeatedly is creating a Many-to-Many relationship using a mapping class. In Rails, the hasMany/through directive provides this functionality. Grails doesn't have an explicit hasMany/through statement like Rails. Rather, Many-to-Many relationships are created implicitly by GORM via your domain classes.

Although GORM now supports implicit M2M relationships (1.0.3), sometimes you cannot replace the need for properties on the relationship itself. This is when the need for a mapping domain class between the ends of the relationship is necessary.

A Typical Example: Employees and Teams

This tutorial will show how to create a Many-to-Many relationship between two classes, Employee and Team.

A customer wants you to develop a web page for representing their employees and the teams their employees are on. The customer tells you:

  • All employees are on one or more teams.
  • All teams have one or more employees on them.
  • All teams are managed by a single employee.
  • An employee can manager multiple teams.
We quickly create our two domain classes:
class Employee {
	String name
}

class Team { String name Employee manager }

This first pass doesn't represent our domain model completely. We're missing our relations (Manager-to-Teams and Employess-to-Teams). The Manager-to-Teams relationship is a archetypical One-to-Many relationship:

class Employee {
	String name
	static hasMany = [managedTeams:Team]
	static mappedBy = [managedTeams:"manager"]
}

class Team { String name Employee manager static belongsTo = Employee }

Adding the two static values to Employee completely defines our One-to-Many relationship between a Employee (aka manager) and the teams he manages. We can see that it automagically works by firing up the grails shell:

% grails shell
groovy> new Employee(name:"Alice").addToManagedTeams(name:'A Team').save()
groovy> go

groovy> println Employee.findByName("Alice").managedTeams.name groovy> go A Team

groovy> println Team.findByName("A Team").manager.name groovy> go Alice

The Many-to-Many Map

Now, we need to add the Many-to-Many relationship between Employees and Teams. To do this, we'll use another domain class to map the relationship: Membership.

class Membership {
	Employee employee
	Team team
}

Additionally, we need to update our other classes:

class Employee {
	String name
	static hasMany = [managedTeams:Team, memberships:Membership]
	static mappedBy = [managedTeams:"manager"]
}

class Team { String name Employee manager static belongsTo = Employee static hasMany = [memberships:Membership] }

We now have a functional Many-to-Many relationship between Employee and Team. Unfortunately, it's a bit raw for us to use. Take, for example, trying to determine what teams an employee is on - we have to muddle through his memberships to do so:

groovy> def alice = Employee.findByName("Alice")
groovy> alice.memberships.each { println it.team.name }
groovy> go
A Team

Also, while GORM gives us the ability to associate an Employee to a Team, we must be careful when adding or removing a relationship between an Employee and a Team. However, to help us, we can add a couple of static methods to Membership to encapsulate the complexity:

class Membership {
	Employee employee
	Team team

static Membership link(employee, team) { def m = Membership.findByEmployeeAndTeam(employee, team) if (!m) { m = new Membership() employee?.addToMemberships(m) team?.addToMemberships(m) m.save() } return m }

static void unlink(employee, team) { def m = Membership.findByEmployeeAndTeam(employee, team) if (m) { employee?.removeFromMemberships(m) team?.removeFromMemberships(m) m.delete() } } }

Now, we can quickly and reliably manage the association between an Employee and a Team:

groovy> def bob = Employee.findByName("Bob")
groovy> def ateam = Team.findByName("A Team")
groovy> Membership.link(bob, ateam)
groovy> go

===> Membership : 1

groovy> bob.memberships groovy> go

===> [Membership : 1]

groovy> ateam.memberships groovy> go

===> [Membership : 1]

Traversing the Membership

It's worth noting that you need to traverse the membership class to find "the other end". For example, you need to go from Employee to Membership to Team. You can do this quickly via collect():

groovy> bob.memberships.collect{it.team}
groovy> go

===> [Team : 1]

Adding a method for this on the domain classes could simplify your code:

class Employee {
	String name
	static hasMany = [managedTeams:Team, memberships:Membership]
	static mappedBy = [managedTeams:"manager"]

def teams() { return memberships.collect{it.team} } }

class Team { String name Employee manager static belongsTo = Employee static hasMany = [memberships:Membership]

def employees() { return memberships.collect{it.employee} } }

You can now use these methods to directly access the objects across the relation:

groovy> def bob = Employee.findByName("Bob")
groovy> println bob.teams().name
groovy> go
A Team

GORM-y Helpers

Even with the static link/unlink methods on Membership, there is still some "impedence mismatch" with normal GORM-generated methods addTo/removeFrom. So, we can add our own:

class Employee {
	String name
	static hasMany = [managedTeams:Team, memberships:Membership]
	static mappedBy = [managedTeams:"manager"]

List teams() { return memberships.collect{it.team} }

List addToTeams(Team team) { Membership.link(this, team) return teams() }

List removeFromTeams(Team team) { Membership.unlink(this, team) return teams() } }

class Team { String name Employee manager static belongsTo = Employee static hasMany = [memberships:Membership]

List employees() { return memberships.collect{it.employee} }

List addToEmployees(Employee employee) { Membership.link(employee, this) return employees() }

List removeFromEmployees(Employee employee) { Membership.unlink(employee, this) return employees() } }

Now, we can manage the relationship from either end and without worrying about the mapping domain:

groovy> def carl = new Employee(name:"Carl")
groovy> def ateam = Team.findByName("A Team")
groovy> ateam.addToEmployees(carl)
groovy> println "A Team: " + ateam.employees()
groovy> println "Carl: " + carl.teams()
groovy> go

A Team: ["Alice", "Carl"] Carl: ["A Team"]

Lazy Initialization Errors

You may encounter Hibernate Lazy Initialization errors when using this technique, especially if you attempt to access your domain collections from inside GSP templates (via the render template command in GSP pages). There is a work-around.

First, use the grails command to install the grails templates into your project:

grails install-templates

Next, locate the web.xml template, grails-app/src/templates/war/web.xml.

Add the following:

<!-- Hibernate -->
<filter>
  <filter-name>hibernateFilter</filter-name>
  <filter-class>
      org.codehaus.groovy.grails.orm.hibernate.support.GrailsOpenSessionInViewFilter
  </filter-class>
</filter>

<filter-mapping> <filter-name>hibernateFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- ~ Hibernate -->

Insert the above JUST BEFORE the first <filter-mapping> in the template (it should be the charEncodingFilter).