Analytics

Thursday, October 28, 2010

Grails One to Many Mapping with Foreign Key

Grails is a fabulous light weight framework that operates by convention over configuration. This mode of operation results in significant developer productivity and a decrease in configuration headaches.  However, Grails conventions sometimes yield subtle unwanted results.  One example is the default one to many unidirectional mapping configuration, in a very specific scenario.  Grails maps the one to many relationship with a join table. This results in a problem when deleting the child when calling delete() on a child object.

One to Many Mapping Unidirectional Default

When mapping two objects with a one to many relationship and making that relationship unidirectional, grails provides a simple default way to map this to the database.  Take an example of Company and Employee. A Company can have many Employees. Here is a code snippet of how to map these objects.


class Company {
  String name
  String industry
  static hasMany = [employees: Employee]
} 

class Employee {
  String name
  Long yearsEmployed
  static belongsTo = Company
}

Notice a few things about the GORM mappings.  Company has many employess, as noted by the hasMany attribute.  An Employee is dependent on the life of a Company, as expressed by the belongsTo attribute.  In other words, belongsTo enables cascade delete so that when a Company is deleted, the Employees for the company are deleted as well. GORM generates a database ddl where the COMPANY table is associated with a EMPLOYEE table through a third association table (also known as a join table).

Delete Fails Due to Join Table

In GORM, if you have access to a company instance, then the call company.delete() works fine and the whole object graph is deleted. 

However, with this domain it seems like you should be able to delete an Employee from a Company by calling delete() on an employee instance object.  For example, in UI scaffolding screens, say you access a Company, then click on an Employee, and use the Delete button to zap an employee.  A referential integrity SQL exception occurs! 

This is because scaffolding code calls something like this on the Employee UI page, and the code fails.

def employee = Employee.get(params.id)
employee.delete()

This failure is due to the join table having a foreign reference to the EMPLOYEE table. You cannot delete a row from a table that is being reference by another table, due to referencial integrity. Thus, we have a mapping problem.

One to Many Mapping with Foreign Key


There are a few ways top get around this issue.  First if you are a stickler to keep a unidirectional relationship, you can map the relationship with a foreign key instead of with a join table. Use the hibernate DSL shown below. The EMPLOYEE table can have a foreign key back to the COMPANY table.



class Company {

  String name
  String industry

  static hasMany = [employees: Employee]

   static mapping = {
    employees column:'COMPANY_EMPLOYEE_ID',joinTable: false
  }
}

class Employee {

  String name
  Long yearsEmployed

  static belongsTo = Company
}

Another option is the allow a birectional one to many relationship. This means the employee will have a reference to its parent company. This also creates a foreign key in the EMPLOYEE table which references back to COMPANY.

class Company {
  String name
  String industry
  static hasMany = [employees: Employee]
} 

class Employee {
  String name
  Long yearsEmployed
  static belongsTo = [company: Company]
} 
Both these mappings allow the following code to work:

def employee = Employee.get(params.id)
employee.delete() 

Design Considerations

The reason hibernate creates a join table by default is so that it can enforce the "not-null" constraint of the child object.  In other words, it makes the assumption that a Company must have at least one Employee. Cascading is still enabled, but to delete a Employee you have to go through the Company object and remove a particular Employee from the collection.

If you don't agree with this assumption, then you can map the one to many relationship with a foreign key.

3 comments:

  1. Brilliant explanation - well done.

    ReplyDelete
  2. Thanks. I ran into a few more peculiar situations, I just have to find time to document them

    ReplyDelete
  3. can you please answer to this:

    http://stackoverflow.com/questions/8670861/grails-multiple-tables-in-single-domain-class/8672990#8672990

    ReplyDelete