The project I am working on has run into some undesirable Mapper behaviour. It seems you cannot add objects to many-to-many associations unless the objects are already saved in the database (more specifically, you cannot retrieve some not-saved objects from many-to-many associations).
As an example we have Employees and Departments in an M:N relationship. We instantiate 2 departments: Accounting and Security. Accounting gets employees before they are saved, Security gets employees after they are saved.
object Employee extends Employee with LongKeyedMetaMapper[Employee]
class Employee extends LongKeyedMapper[Employee] with IdPK {
def getSingleton = Employee
object name extends MappedString(this, 20)
override def toString = name.get
}
object Department extends Department with LongKeyedMetaMapper[Department]
class Department extends LongKeyedMapper[Department] with ManyToMany with IdPK {
def getSingleton = Department
object employees extends MappedManyToMany(
EmployeeDepartment,
EmployeeDepartment.department,
EmployeeDepartment.employee,
Employee)
}
/* Many-to-Many association record */
object EmployeeDepartment extends EmployeeDepartment with LongKeyedMetaMapper[EmployeeDepartment]
class EmployeeDepartment extends LongKeyedMapper[EmployeeDepartment] with IdPK {
def getSingleton = EmployeeDepartment
object employee extends MappedLongForeignKey(this, Employee)
object department extends MappedLongForeignKey(this, Department)
}
object Company extends App {
DB.defineConnectionManager(DefaultConnectionIdentifier, myDBVendor)
val accounting = Department.create
val security = Department.create
accounting.employees ++= Seq(
Employee.create.name("Fred"),
Employee.create.name("Steve"))
security.employees ++= Seq(
Employee.create.name("Dave"),
Employee.create.name("Sonia")) map
{_.saveMe()}
accounting.employees.toList map println; println
security.employees.toList map println
}
Output
Fred Fred Dave Sonia
!!!
The employees of accounting have all turned into Fred! The problem scales similarly: adding 10 not-yet-saved entities to accounting.employees yields 10 references to Fred on access.
This appears to either be a bug, or a serious limitation to the usefulness of Mapper. Is there a workaround for this behaviour that does not involve either:
- using some data structure external to the model to keep track of entities and associations until you are ready to save, or
- saving entities on create, issuing deletes if the user “changes their mind” about saving
Found out the concrete reason for this apparent misbehaviour. Adding elements to a many-to-many association invokes the following algorithm:
The rub is in the find criteria. Specified in ManyToMany.scala (Lift 2.4), method
isJoinForChildcompares theisof the association foreign key (MappedManyToManyconstructor parameter 3) with the primary key (alsois) of the “other” element.As every non-saved entity
with IdPKhas id=-1, the first element in the “this” join list satisfies the find criteria and is then added in duplicate.This answers my original question: it is possible to fix this behaviour by modifying
isJoinForChild. Rather than simply comparing keys, the method would do something akin to:… or something to that effect. Since
isJoinForChildisprotectedthis can be accomplished non-invasively by overridingisJoinForChildusing a trait.