public
Description: The Lift web framework for Scala
Home | Edit | New

How To: Work with one-to-many relationships

OneToMany provides functionality to simplify working with one-to-many relationships among Mapper classes. It allows you to work with the children of a relationship as with any Scala collection, adding, removing, and iterating over children, not persisting your changes until you call save. This allows you to manipulate entitiy relationships in memory. This can greatly simplify coding applications that allow the user to alter relationships across multiple screens or on one screen with buttons that are not implemented with Javascript, submitting and loading the page multiple times although nothing has been changed in the database yet.

Model the relationship in your Mapper entity

Example (assuming Books have one Author each):

object Author extends Author with LongKeyedMetaMapper[Author]
class Author extends LongKeyedMapper[Author] with IdPK with OneToMany[Long, Author] {
  def getSingleton = Author
  object name extends MappedString(this, 100)
  object books
    extends MappedOneToMany(Book, Book.author, OrderBy(Book.sort, Ascending))
    with Owned[Book] with Cascade[Book]
}

object Book extends Book with LongKeyedMetaMapper[Book]
class Book extends LongKeyedMapper[Book] with IdPK with Ordered[Book] {
def getSingleton = Book
object author extends MappedLongForeignKey(this, Author)
with LongMappedForeignMapper[Book, Author]
object name extends MappedString(this, 100) {
override def validations = valMinLen(1, “Name cannot be empty”) _ :: Nil
}
object sort extends MappedInt(this)
def compare(that: Book) = this.sort.is – that.sort.is
}

Note the following points that differentiate these entities from ordinary ones.

  1. The “one” side of the relationship extends OneToMany in the mapper. It takes two type parameters: The type of the primary key, and the self type.
    class Author extends LongKeyedMapper[Author] with IdPK with OneToMany[Long, Author] {

    This trait, in addition to providing access to traits mixed in to one-to-many fields, overrides save and delete_! to customize their behavior.
  2. The “field” that represents the many items related to this one item.
    object books
        extends MappedOneToMany(Book, Book.author, OrderBy(Book.sort, Ascending))
        with Owned[Book] with Cascade[Book]
    

    It looks like another field. Despite the name, however, MappedOneToMany does not extend MappedField, and it does not correspond to a table field in the “one” side of the relationship. It takes as parameters the singleton of the “many” side, and the field on the “many” side that represents the foreign key that points back to this mapper. These are followed by any QueryParams you wish to sort or constrain the set of children (in addition to having foreign keys that match the primary key).
    There is an alternative constructor that takes two parameters, a function to get a Seq of children (not filtered to match keys), and a function to get the foreign key for a given child entity.
    Here we have mixed in two optional traits, Owned and Cascade. Owned implies that children must have a parent; therefore if the owning entity is saved and children that were removed have not been given another parent, those children will be deleted from the database. Cascade means that deleting the parent will delete the children.
  3. The foreign key field on the child
    object nature extends MappedLongForeignKey(this, Author)
        with LongMappedForeignMapper[Book, Author]
    

    Mainly an ordinary MappedLongForeignKey, mixing in the LongMappedForeignMapper trait taking type parameters of the child mapper and the parent mapper. This trait allows you to get and set the value of the foreign key via the actual parent object rather than the value of its primary key, even when the parent has not yet been saved and therefore has no primary key. For example,
    bookXXX.author(johnSmith)
    or
    bookXX.author.is == johnSmith
    rather than
    bookXXX.author(johnSmith.primaryKey.is)
    or
    boookXX.author.is == johnSmith.primaryKey.is
    (Although usually you would set it by adding it on the parent)

Use in the view (using a StatefulSnippet for simplicity)

Very straightforward
Listing:

def list(xhtml: NodeSeq) = Author.findAll.flatMap { a =>
    bind("author", xhtml,
         "id" ->  a.id,
         "name" -> a.name,
         "books" -> a.books.map(_.name.toString).mkString(", "),
         "edit" -> link("edit", ()=>author=a, Text(?("Edit"))),
         "remove" -> link("list", ()=>a.delete_!, Text(?("Remove")))
    )
  }

Editing:

...
    bind("author", xhtml,
         "id" -> author.id.toString,
         "name" -> author.name.toForm,
         "books" -> {(ns:NodeSeq)=>
             author.books.flatMap {book =>
                  bind("book", ns,
                      "name" -> book.name.toForm,
                      "remove" -> SHtml.submit(?("Remove"), ()=> author.books -= book),
                  )
             }
         },
         "insert-> SHtml.submit(?("New book"), ()=> author.books += new Book),
         "submit" -> SHtml.submit(?("Save"), ()=>save(author))
    )

In short, the MappedOneToMany “field” behaves like an ordinary collection, and holds your changes until you save.

Holding children from several Mapper classes

You can also use OneToMany with children from several tables/mapper classes. To accomplish this, you need a base class that declares an abstract def save: Unit method, and if you want to use Owned or Cascade, a def delete_!: Unit method as well. Your child Mapper classes should derive from this base class. Then, instead of using MappedOneToMany use MappedOneToManyBase, which takes the base class as its type parameter and two constructor arguments. The first is a ()=>Seq[O] function, where O is the type argument. It should return the list of children to be managed, and is invoked whenever refresh is called to populate the list of children. The second is a function from an O to a MappedForeignKey[K, _, T], where K is the primary key type of the parent (and the foreign key type of the child), and T is the parent mapper class. In other words, it should return the MappedForeignKey on the given child entity that links to the parent.

Last edited by nafg, Wed Aug 12 13:54:34 -0700 2009
Home | Edit | New
Versions: