Reputation: 13
I have the following simplified code to reflect the issue I'm facing. It's using slick 3.1.1, with scala 2.10.4 and mysql.
I have a User table, with one column that has Option[Seq[String]] and the other column is Seq[String].
There are 2 MappedColumnType; that will convert the Seq[String] to String, and Option[Seq[String]] to String
Below is the simplified code:
package models
import slick.driver.MySQLDriver.api._
case class User(id: Long,
name: Option[String],
cities: Option[Seq[String]] = None,
countries: Seq[String])
class UserMapping(tag: Tag) extends Table[User](tag, "USERS") {
implicit val stringListMapper = MappedColumnType.base[Seq[String], String](
list => list.mkString(","),
string => string.split(',').toSeq
)
implicit val stringOptionalListMapper = MappedColumnType.base[Option[Seq[String]], String](
list => list.get.mkString(","),
string => Some(string.split(',').toSeq)
)
def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)
def name: Rep[Option[String]] = column[Option[String]]("NAME")
def cities: Rep[Option[Seq[String]]] = column[Option[Seq[String]]]("CITIES")(stringOptionalListMapper)
def countries: Rep[Seq[String]] = column[Seq[String]]("COUNTRIES")(stringListMapper)
// scalastyle:off method.name public.methods.have.type
def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
// scalastyle:on method.name public.methods.have.type
}
The compiler is failing at the projection:
[error] Slick does not know how to map the given types.
[error] Possible causes: T in Table[T] does not match your * projection. Or you use an unsupported type in a Query (e.g. scala List).
[error] Required level: slick.lifted.FlatShapeLevel
[error] Source type: (slick.lifted.Rep[Long], slick.lifted.Rep[Option[String]], slick.lifted.Rep[Option[Seq[String]]], slick.lifted.Rep[Seq[String]])
[error] Unpacked type: (Long, Option[String], Option[Seq[String]], Seq[String])
[error] Packed type: Any
[error] def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
[error] ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
[error] Total time: 2 s, completed Jan 23, 2018 3:24:23 PM
It works fine if I have only one MappedColumnType defined in here; unfortunately, my data model needs one to be optional and the other to be required. Any ideas of what's happening in here? Thanks!
Upvotes: 1
Views: 1397
Reputation: 23788
It looks like that the really important thing is not that there are two mapped column but the fact that they are of the same shape except for Option
. This is bad because it makes you introduce two implicit val
s for mappings and this makes them ambiguous for conversion of (id, name, cities, countries)
into a ProvenShape
If the logic for such shape is actually the same as in your example, then Slick seems to be able to add Option
wrapper on its own so you are OK with just one (non-Option
) implicit such as:
class UserMapping(tag: Tag) extends Table[User](tag, "USERS") {
implicit val stringListMapper = MappedColumnType.base[Seq[String], String](
list => list.mkString(","),
string => string.split(',').toSeq
)
def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)
def name: Rep[Option[String]] = column[Option[String]]("NAME")
def cities: Rep[Option[Seq[String]]] = column[Option[Seq[String]]]("CITIES") // share stringListMapper
def countries: Rep[Seq[String]] = column[Seq[String]]("COUNTRIES") // share stringListMapper
// scalastyle:off method.name public.methods.have.type
def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
// scalastyle:on method.name public.methods.have.type
}
However if you are unlucky and mappings for the same shape are actually different so you need to pass them both explicitly to column
(for example if the separator chars are different), then you will have to explicitly provide an implicit
evidence of the proper Shape
such as:
class UserMapping(tag: Tag) extends Table[User](tag, "USERS") {
val stringListMapper = MappedColumnType.base[Seq[String], String](
list => list.mkString(","),
string => string.split(',').toSeq
)
val stringOptionalListMapper = MappedColumnType.base[Option[Seq[String]], String](
list => list.get.mkString(","),
string => Some(string.split(',').toSeq)
)
def id: Rep[Long] = column[Long]("ID", O.PrimaryKey, O.AutoInc)
def name: Rep[Option[String]] = column[Option[String]]("NAME")
def cities: Rep[Option[Seq[String]]] = column[Option[Seq[String]]]("CITIES")(stringOptionalListMapper)
def countries: Rep[Seq[String]] = column[Seq[String]]("COUNTRIES")(stringListMapper)
// explicitly provide proper Shape evidence
import slick.lifted.Shape
implicit val shape = Shape.tuple4Shape(
Shape.repColumnShape(longColumnType),
Shape.optionShape(Shape.repColumnShape(stringColumnType)),
Shape.repColumnShape(stringOptionalListMapper),
Shape.repColumnShape(stringListMapper))
// scalastyle:off method.name public.methods.have.type
def * = (id, name, cities, countries) <> (User.tupled, User.unapply)
// scalastyle:on method.name public.methods.have.type
}
Upvotes: 1