play-slick problem: one-to-one relationship required MappedTypeMapper?

2,439 views
Skip to first unread message

Tiger

unread,
Sep 13, 2013, 12:29:03 PM9/13/13
to scala...@googlegroups.com
Hi, I use the play2.1.4 and play-slick 0.4.0, the below is my code:
package models

import play.api.db.slick.Config.driver.simple._
import play.api.db.slick._
import play.api.Play.current


case class User(id: Option[Long] = None, name: String, email: String, age: Int, female: Boolean, address: Address/*, friends: Option[Seq[User]], spouse: Option[User] */)

case class Address(id: Option[Long] = None, fullStreet: String, county: String, country: String)

object Users extends Table[User]("USER") {
  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def name = column[String]("NAME", O.NotNull)
  def email = column[String]("EMAIL", O.NotNull)
  def age = column[Int]("AGE", O.NotNull)
  def female = column[Boolean]("FEMALE", O.NotNull)
  def address = column[Address]("ADDRESS_ID", O.NotNull)

  def * = id.? ~ name ~ email ~ age ~ female ~ address <> (User, User.unapply _)
  
 
}

object Addresses extends Table[Address]("ADDRESS")  {
  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def fullStreet = column[String]("FULL_STREET", O.NotNull)
  def county = column[String]("COUNTY", O.NotNull)
  def country = column[String]("COUNTRY", O.NotNull)

  def * = id.? ~ fullStreet ~ county ~ country <> (Address, Address.unapply _)

}

but the scala-idea report an error at line " def address = column[Address]("ADDRESS_ID", O.NotNull) " in object Users, 
the error message is: 
Multiple markers at this line
- not enough arguments for method column: (implicit tm: scala.slick.lifted.TypeMapper[models.Address])scala.slick.lifted.Column[models.Address]. 
Unspecified value parameter tm.
- could not find implicit value for parameter tm: scala.slick.lifted.TypeMapper[models.Address]
- not enough arguments for method column: (implicit tm: scala.slick.lifted.TypeMapper[models.Address])scala.slick.lifted.Column[models.Address]. 
Unspecified value parameter tm.
- could not find implicit value for parameter tm: scala.slick.lifted.TypeMapper[models.Address]
I don't know how to write the TypeMapper, Who can help me ? Thanks!

Best Regards!
Tiger

Pedro Furlanetto

unread,
Sep 13, 2013, 1:52:24 PM9/13/13
to scala...@googlegroups.com
The way you are modeling it seems you actually want a foreign

In Users:

def addressId = column[Address]("ADDRESS_ID", O.NotNull)
def addressFk = foreignKey("fk_name", authorId, Addresses)(_.id)
> --
>
> ---
> You received this message because you are subscribed to the Google Groups
> "Slick / ScalaQuery" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to scalaquery+...@googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.

Christopher Vogt

unread,
Sep 13, 2013, 2:25:24 PM9/13/13
to scala...@googlegroups.com
Hi Tiger,

the proper way to do relationships with Slick is NOT to do nesting (user
having an address member) but using a foreign key instead (user having
an addressId member and a corresponding address object). Among other
things this makes the loading strategy (lazy, eager, ...) explicit in
user code and does not complicate Slick's api and implementation with
the need to have configuration options for loading strategies of related
objects. This is a fundamental design choice that makes Slick different
from ORMs. I will publish a proper documentation section about
relationships in Slick in the near future. The message regarding type
mappers refers to a feature of Slick that allows you to integrate your
own column types into Slick, i.e. values that map to exactly one column.

Instead of user having an address member, you either get the address
using the foreign key:

val user = ... // <- your user object
val address = Query(Addresses).filter(_.id === user.addressId).first

Or you write a more universal function, that even works for users which
you have not been materialized in memory (in other words queries of
users). It takes a querys of users and returns a query of addresses or
(user,address) tuples. E.g. something like this

def joinUserAddress( users:Query[Users,User] = Query(Users),
address=Query[Addresses,Address] = Query(Addresses) ) = for( u <- users;
a <- addresses; if u.address_id === a.id ) yield (u,a)

And then you can fetch the address of a users in the place where you
need it like this:

val userQuery = ... // <- some query finding one or more user objects
val (user,address) = joinUserAddress( users=userQuery ).first

Also see our ScalaDays 2013 talk http://slick.typesafe.com/docs/ .

For a one-to-one relationship you can alternatively consider to put all
columns into a single table. In this case you can actually map them to
nested case class structure as I described here:
http://stackoverflow.com/questions/18572084/how-to-combine-multiple-columns-in-one-case-class-field-when-using-lifted-embedd/18591234

Chris
--
Slick Team

Tiger

unread,
Sep 14, 2013, 2:55:25 AM9/14/13
to scala...@googlegroups.com
Thanks Pedro & Chris!
Iif use the foreign key, how to write the userForm code and the play2 html template

// in controller, can't compile
lazy val userMapping: Mapping[User] = mapping (
      "name" -> nonEmptyText,
      "email" -> email,
      "age"  -> number(min = 0, max = 150),
      "female" -> boolean,
      "address" -> addressMapping
      
    )(User.apply)(User.unapply)

// html template, address in the user form
 @helper.form(action = routes.Data.post) {
            @helper.inputText(userForm("name"))
            @helper.inputText(userForm("email"))
            @helper.input(userForm("age")) { (id, name, value, args) =>
                    <input type="number" name="@name" id="@id" @toHtmlArgs(args) />
            }

            @helper.checkbox(userForm("female"))

            <fieldset>
                <legend>Address:</legend>

                @helper.inputText(userForm("address.fullStreet"), '_label -> "Full Street")

                @helper.inputText(
                    userForm("address.county"), '_label -> "County"
                )
                @helper.select(userForm("address.country"), Seq(
                    ""      -> "----",
                    "AR"    -> "Arda",
                    "BE"    -> "Belgium",
                    "SL"    -> "Smurfs Land"
                ), '_lable -> "Country")
            </fieldset>

            <input type="submit" name="send" value="Feed" />
        }
Should I defined an UserAndAddressView VO for the html and controller?

Best Regards
Tiger

Christopher Vogt

unread,
Sep 14, 2013, 3:07:23 AM9/14/13
to scala...@googlegroups.com
Not sure you will get an answer to this in time. I don't know Play well
enough to know how to write the addressMapping you reference there. I
can look at it at some point, but Slick 2.0 release has priority. You
may get an answer on the Play mailing list.

Two obvious approaches that come to my head are either to put all
columns into the same table (which also saves you the join as already
said). Or if possible, write a Mapping[(User,Address)] and provide it
two functions that map from and two such a tuples instead of providing
it User.apply and User.unapply.

Chris

Tiger

unread,
Sep 14, 2013, 3:49:52 AM9/14/13
to scala...@googlegroups.com
Thanks Chirs

Best Regards
Tiger

在 2013年9月14日星期六UTC+8下午3时07分23秒,Christopher Vogt写道:

virtualeyes

unread,
Sep 14, 2013, 11:24:14 AM9/14/13
to scala...@googlegroups.com
Tiger, check out Play nested forms.

Tiger

unread,
Sep 14, 2013, 9:55:03 PM9/14/13
to scala...@googlegroups.com
Thank virtualeyes!

the play2.1 document sample:
case class User(name: String, address: Address)
case class Address(street: String, city: String)

val userForm = Form(
  mapping(
    "name" -> text,
    "address" -> mapping(
        "street" -> text,
        "city" -> text
    )(Address.apply)(Address.unapply)
  )(User.apply, User.unapply)
)
but in slick the User model can't have a Address member, but a addressId(Long) member. This is my problem.
another play2.1 sample don't use slick.
 
Best regards
Tiger

在 2013年9月14日星期六UTC+8下午11时24分14秒,virtualeyes写道:

virtualeyes

unread,
Sep 15, 2013, 1:39:54 AM9/15/13
to scala...@googlegroups.com
You'll have to play around with it to get it working, but a general approach might be something like:

// in your model
case class User(id: Int, name: String, address: addressID)
case class Address(id: Int, street: String, city: String)
case class UserAddress(u: User, a: Address) // for form binding

// in your User DAO
val q = for{
  id <- Parameters[Int]
  u <- Users if u.id is id
  a <- Addresses if u.addressID is a.id
} yield (u,a)

def get(id: Int): Option[UserAddress] = {
  q(id).firstOption.map{case( (u:User, a: Address)=> UserAddress(u,a) )}
}

// in your User controller
def show(id: Int) = Action {
  userForm.fill(repo.user.get(id))
}

userForm will need to be updated accordingly if you take the UserAddress approach (i.e. it expects a User and Address to bind the form to)

Play user group is probably best place for this kind of question (only Slick related issue is with model mapping, which as Christoper has pointed out is best solved via foreign keys and NOT Hibernate style)

Good luck!

Tiger

unread,
Sep 15, 2013, 8:37:48 PM9/15/13
to scala...@googlegroups.com
Thanks virtualeyes very much!

Best regards!
Tiger

在 2013年9月15日星期日UTC+8下午1时39分54秒,virtualeyes写道:
Reply all
Reply to author
Forward
0 new messages