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.
Brilliant explanation - well done.
ReplyDeleteThanks. I ran into a few more peculiar situations, I just have to find time to document them
ReplyDeletecan you please answer to this:
ReplyDeletehttp://stackoverflow.com/questions/8670861/grails-multiple-tables-in-single-domain-class/8672990#8672990