All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.liftweb.mapper.ManyToMany.scala Maven / Gradle / Ivy

The newest version!
package net.liftweb
package mapper

import common.{Empty, Full}

import scala.annotation.tailrec

/**
 * Add this trait to a Mapper to add support for many-to-many relationships
 *
 * @author nafg
 */
trait ManyToMany extends BaseKeyedMapper {
  this: KeyedMapper[_, _] =>

  private[this] type K = TheKeyType
  private[this] type T = KeyedMapperType

  private var manyToManyFields: List[MappedManyToMany[_,_,_]] = Nil

  /**
   * An override for save to propagate the save to all children
   * of this parent.
   * Returns false as soon as the parent or a one-to-many field returns false.
   * If they are all successful returns true.
   */
  abstract override def save: Boolean = {
    super.save && manyToManyFields.forall(_.save)
  }

  /**
   * An override for delete_! to propogate the deletion to all children
   * of this parent.
   * Returns false as soon as the parent or a one-to-many field returns false.
   * If they are all successful returns true.
   */
  abstract override def delete_! : Boolean = {
    super.delete_! &&
      manyToManyFields.forall( _.delete_!)
  }


  /**
   * This is the base class to extend for fields that track many-to-many relationships.
   * @param joinMeta The singleton of the join table
   * @param thisField The foreign key in the join table that refers to this mapper's primaryKey.
   * @param otherField The foreign key in the join table that refers to the other mapper's primaryKey
   * @param otherMeta The singleton of the other mapper
   * @param qp Any QueryParams to limit entries in the join table (other than matching thisField to primaryKey)
   * To limit children based on fields in the other table (not the join table), it is currently necessary
   * to point the join mapper to a view which pulls the join table's fields as well as fields of the other table.
   */
  class MappedManyToMany[O<:Mapper[O], K2, T2 <: KeyedMapper[K2,T2]](
    val joinMeta: MetaMapper[O],
    thisField: MappedForeignKey[K,O,_ <: KeyedMapper[_,_]],
    val otherField: MappedForeignKey[K2, O, T2],
    val otherMeta: MetaMapper[T2],
    val qp: QueryParam[O]*) extends scala.collection.mutable.Buffer[T2] {

    def otherFK[A](join: O)(f: MappedForeignKey[K2,O,T2] => A): A =
      otherField.actualField(join) match { case mfk: MappedForeignKey[K2,O,T2] => f(mfk) }

    protected def children: List[T2] = joins.flatMap(otherFK(_)(_.obj))

    protected var _joins: List[O] = _

    /**
     * Get the list of instances of joinMeta
     */
    def joins: List[O] = _joins // read only to the public
    protected var removedJoins: List[O] = Nil

    refresh
    manyToManyFields ::= this

    protected def isJoinForChild(e: T2)(join: O): Boolean = otherField.actualField(join).get == e.primaryKeyField.get

    protected def joinForChild(e: T2): Option[O] = joins.find(isJoinForChild(e))

    protected def own(e: T2): O = {
      joinForChild(e).fold {
        removedJoins
          // first check if we can recycle a removed join
          .find(otherField.actualField(_).get == e.primaryKeyField)
          .fold{
            val newJoin = joinMeta.create
            thisField.actualField(newJoin) match {
              case mfk: MappedForeignKey[K, O, T] => mfk.set(primaryKeyField.get.asInstanceOf[K])
            }
            otherFK(newJoin)(_.apply(e))
            newJoin
          }{ removedJoin =>
            removedJoins = removedJoins filter removedJoin.ne
            removedJoin // well, noLongerRemovedJoin...
          }
      }(join => join)
    }

    protected def unown(e: T2): Option[O] =
      joinForChild(e).map{ join =>
        removedJoins = join :: removedJoins
        val o = otherField.actualField(join)
        o.set(o.defaultValue)
        thisField.actualField(join) match { case mfk => mfk set mfk.defaultValue }
        join
      }

    /**
     * Get the List backing this Buffer.
     */
    def all: List[T2] = children

    def length: Int = children.length

    def iterator: Iterator[T2] = children.iterator

    protected def childAt(n: Int): T2 = children(n)
    def apply(n: Int): T2 = childAt(n)
    def indexOf(e: T2): Int = children.indexWhere(e.eq)

    def insertAll(n: Int, traversable: Traversable[T2]) {
      val ownedJoins = traversable map own
      val n2 = joins.indexWhere(isJoinForChild(children(n)))
      val before = joins.take(n2)
      val after = joins.drop(n2)

      _joins = before ++ ownedJoins ++ after
    }

    def +=:(elem: T2): MappedManyToMany.this.type = {
      _joins ::= own(elem)
      this
    }

    def +=(elem: T2): MappedManyToMany.this.type = {
      _joins ++= List(own(elem))
      this
    }

    def update(n: Int, newelem: T2): Unit = {
      unown(childAt(n)) match {
        case Some(join) =>
          val n2 = joins.indexOf(join)
          val (before, after) = (joins.take(n2), joins.drop(n2+1))
          _joins = before ++ List(own(newelem)) ++ after
        case None =>
      }
    }

    def remove(n: Int): T2 = {
      val child = childAt(n)
      unown(child).foreach(join => _joins = joins filterNot join.eq)
      child
    }

    override def remove(idx: Int, count: Int): Unit = {
      if (count > 0) {
        @tailrec
        def loop(c: Int, a: List[T2]): List[T2] =
          if (c == 0) childAt(idx) :: a
          else loop(c - 1, childAt(idx + c) :: a)

        val joins0 = loop(count - 1, Nil) flatMap unown
        _joins = joins filterNot joins0.contains
      }
    }

    def clear(): Unit = {
      children foreach unown
      _joins = Nil
    }

    /**
     * Discard the cached state of this MappedManyToMany's children and reinitialize it from the database
     */
    def refresh: List[T2] = {
      val by = new Cmp[O, TheKeyType](thisField, OprEnum.Eql, Full(primaryKeyField.get.asInstanceOf[K]), Empty, Empty)

      _joins = joinMeta.findAll( (by :: qp.toList): _*)
      all
    }

    /**
     * Save the state of this MappedManyToMany to the database.
     * This will do the following:
     * 1) Prune join table instances whose "child" foreign key's value is its defaultValue, i.e., -1
     * 2) Set all join table instances' "parent" foreign key
     * 3) Delete all join table instances whose child instance was removed
     * 4) Save all child instances
     * 5) If step 3 succeeds save all join instances
     * 6) Return true if steps 2-4 all returned true; otherwise false
     */
    def save: Boolean = {
      _joins = joins.filter { join =>
        otherFK(join)(f => f.get != f.defaultValue)
      }
      _joins foreach {
        thisField.actualField(_).asInstanceOf[MappedForeignKey[K,O,X] forSome {type X <: KeyedMapper[K,X]}] set ManyToMany.this.primaryKeyField.get.asInstanceOf[K]
      }

      removedJoins.forall {_.delete_!} & ( // continue saving even if deleting fails
        children.forall(_.save) &&
          joins.forall(_.save)
      )
    }

    /**
     * Deletes all join rows, including those
     * marked for removal.
     * Returns true if both succeed, otherwise false
     */
    def delete_! : Boolean = {
      removedJoins.forall(_.delete_!) &
        joins.forall(_.delete_!)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy