0

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
  • Just out of interest, are you using Lift Mapper as a library to do your ORM, or are you using the whole Lift framework? If it's the latter, you may want to have a look at Slick. – Jack Nov 02 '12 at 04:13
  • We are using Mapper as part of the Lift framework. At this point in the project going to [Slick](http://slick.typesafe.com/doc/0.11.2/index.html) would be somewhat expensive as much of the application is written. Also, we leverage html generation and validation features of Mapper (which I don't think Slick has). – frig.neutron Nov 02 '12 at 12:21

1 Answers1

0

Found out the concrete reason for this apparent misbehaviour. Adding elements to a many-to-many association invokes the following algorithm:

Find "other" element in "this" join list.
if not found
  create join and add to join list
else
  add to join list

The rub is in the find criteria. Specified in ManyToMany.scala (Lift 2.4), method isJoinForChild compares the is of the association foreign key (MappedManyToMany constructor parameter 3) with the primary key (also is) of the "other" element.

As every non-saved entity with IdPK has 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:

if other.is > -1
  regular comparison
else
  instance comparison

... or something to that effect. Since isJoinForChild is protected this can be accomplished non-invasively by overriding isJoinForChild using a trait.