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.

9 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
  4. Thanks a million! Wasted quite a bit of time trying to solve this issue before finding this.

    ReplyDelete
  5. Sir i have a problem with OneToMany Mapping. I am using Set and jointable in my project.The problem is while update the old records new rows inserted in the jointable

    ReplyDelete
  6. Try making it a list. Or try making it in two different translations

    ReplyDelete
  7. Sorry Sir i didn't understand this my code i have three classes with one to many relationship

    HotelDetail.groovy
    enum StarRating {
    THREE_STAR, FOUR_STAR, FIVE_STAR, SEVEN_STAR
    }
    class HotelDetail implements Serializable, Comparable {
    UUID id
    Hotel hotel
    StarRating starRating
    Set roomTypes
    Set attachments
    boolean checkoutPeriodFixed
    LocalTime startTime
    LocalTime endTime
    Organization organization
    LocalDateTime dateCreated
    User createdBy
    LocalDateTime lastUpdated
    User updatedBy


    static hasMany = [roomTypes : RoomType,attachments : Attachment]

    static constraints = {
    attachments nullable : true
    lastUpdated nullable : true
    updatedBy nullable : true
    }

    static mapping = {
    id generator: 'uuid2', type: 'pg-uuid'
    version false
    hotel column : 'HOTEL_ID'
    createdBy column : 'CREATED_BY'
    updatedBy column : 'UPDATED_BY'
    roomTypes lazy:true, fetch:'select', joinTable: [name: 'HOTEL_DETAIL_ROOM_TYPE',
    key: 'HOTEL_DETAIL_ID',
    column: 'ROOM_TYPE_ID']
    attachments lazy:true, fetch:'select', joinTable: [name: 'HOTEL_DETAIL_ATTACHMENT',
    key: 'HOTEL_DETAIL_ID',
    column: 'ATTACHMENT_ID']
    }

    def beforeInsert = {
    dateCreated = new LocalDateTime()
    createdBy = AccountUtils.loggedInUser
    }

    def beforeUpdate = {
    lastUpdated = new LocalDateTime()
    updatedBy = AccountUtils.loggedInUser
    }
    }

    RoomType.groovyclass
    RoomType implements Serializable, Comparable {
    UUID id
    String name
    String description
    int maxOccupancy
    int sizeInMeter
    int size
    String unit
    boolean childAllowed
    PickList roomCategory
    Set attachments
    int numRooms
    LocalDateTime dateCreated
    User createdBy
    LocalDateTime lastUpdated
    User updatedBy
    Organization organization

    static hasMany = [ attachments : Attachment]

    static constraints = {
    unit nullable : true
    lastUpdated nullable : true
    updatedBy nullable : true
    description nullable : true
    maxOccupancy validator : {return it !=0}
    }

    static mapping = {
    id generator: 'uuid2', type: 'pg-uuid'
    version false
    attachments lazy:true, fetch:'select', joinTable: [name: 'ROOM_TYPE_ATTACHMENT',
    key: 'ROOM_TYPE_ID',
    column: 'ATTACHMENT_ID']
    createdBy column : 'CREATED_BY'
    updatedBy column : 'UPDATED_BY'
    }

    def beforeInsert = {
    dateCreated = new LocalDateTime()
    createdBy = AccountUtils.loggedInUser
    }

    def beforeUpdate = {
    lastUpdated = new LocalDateTime()
    updatedBy = AccountUtils.loggedInUser
    }
    }

    Attachment.groovy

    class Attachment implements Serializable, Comparable{
    UUID id
    String fileName
    String ext
    Organization organization

    static constraints = {

    }

    static mapping = {
    id generator: 'uuid2', type: 'pg-uuid'
    version false

    }
    }

    Save and Fetch Works Correctly.My problem is while i update any one of the sets, for ex in Set roomTypes .
    if i fecth and modify some changes in it.while i update in jointables one new row created whatever i modifyied that id is duplicate in join tables

    ReplyDelete