-->

How to write class and tableclass mapping for slic

2019-09-10 11:07发布

问题:

I use case class to transform the class object to data for slick2 before, but current I use another play plugin, the plugin object use the case class, my class is inherent from this case class. So, I can not use case class as the scala language forbidden use case class to case class inherent.

before:

case class User()

class UserTable(tag: Tag) extends Table[User](tag, "User") {
  ...
  def * = (...)<>(User.tupled,User.unapply)
}

it works. But now I need to change above to below:

case class BasicProfile()

class User(...) extends BasicProfile(...){
  ...
  def unapply(i:User):Tuple12[...]= Tuple12(...)
}
class UserTable(tag: Tag) extends Table[User](tag, "User") {
  ...
  def * = (...)<>(User.tupled,User.unapply)
}

I do not know how to write the tupled and unapply(I am not my writing is correct or not) method like the case class template auto generated. Or you can should me other way to mapping the class to talbe by slick2.

Any one can give me an example of it?

回答1:

First of all, this case class is a bad idea:

case class BasicProfile()

Case classes compare by their member values, this one doesn't have any. Also the name is not great, because we have the same name in Slick. May cause confusion.

Regarding your class

class User(...) extends BasicProfile(...){
  ...
  def unapply(i:User):Tuple12[...]= Tuple12(...)
}

It is possible to emulate case classes yourself. Are you doing that because of the 22 field limit? FYI: Scala 2.11 supports larger case classes. We are doing what you are trying at Sport195, but there are several aspects to take care of.

apply and unapply need to be members of object User (the companion object of class User). .tupled is not a real method, but generated automatically by the Scala compiler. it turns a method like .apply that takes a list of arguments into a function that takes a single tuple of those arguments. As tuples are limited to 22 columns, so is .tupled. But you could of course auto-generated one yourself, may have to give it another name.

We are using the Slick code generator in combination with twirl template engine (uses @ to insert expressions. The $ are inserted as if into the generated Scala code and evaluated, when the generated code is compiled/run.). Here are a few snippets that may help you:

Generate apply method

  /** Factory for @{name} objects
    @{indentN(2,entityColumns.map(c => "* @param "+c.name+" "+c.doc).mkString("\n"))}
    */
  final def apply(
    @{indentN(2,
        entityColumns.map(c =>
          colWithTypeAndDefault(c)
        ).mkString(",\n")
    )}
  ) = new @{name}(@{columnsCSV})

Generate unapply method:

@{if(entityColumns.size <= 22)
  s"""
  /** Extractor for ${name} objects */
  final def unapply(o: ${name}) = Some((${entityColumns.map(c => "o."+c.name).mkString(", ")}))
  """.trim
  else
  ""}

Trait that can be mixed into User to make it a Scala Product:

trait UserBase with Product{
  // Product interface
  def canEqual(that: Any): Boolean = that.isInstanceOf[@name]
  def productArity: Int = @{entityColumns.size}
  def productElement(n: Int): Any = Seq(@{columnsCSV})(n)

  override def toString = @{name}+s"(${productIterator.toSeq.mkString(",")})"
...

case-class like .copy method

  final def copy(
    @{indentN(2,columnsCopy)}
  ): @{name} = @{name}(@{columnsCSV})

To use those classes with Slick you have several options. All are somewhat newer and not documented (well). The normal <> operator Slick goes via tuples, but that's not an option for > 22 columns. One option are the new fastpath converters. Another option is mapping via a Slick HList. No examples exist for either. Another option is going via a custom Shape, which is what we do. This will require you to define a custom shape for your User class and another class defined using Column types to mirror user within queries. Like this: http://slick.typesafe.com/doc/2.1.0/api/#scala.slick.lifted.ProductClassShape Too verbose to write by hand. We use the following template code for this:

/** class for holding the columns corresponding to @{name}
 * used to identify this entity in a Slick query and map
 */
class @{name}Columns(
  @{indent(
    entityColumns
      .map(c => s"val ${c.name}: Column[${c.exposedType}]")
      .mkString(", ")
  )}
) extends Product{
  def canEqual(that: Any): Boolean = that.isInstanceOf[@name]
  def productArity: Int = @{entityColumns.size}
  def productElement(n: Int): Any = Seq(@{columnsCSV})(n)
}

/** shape for mapping @{name}Columns to @{name} */
object @{name}Implicits{
  implicit object @{name}Shape extends ClassShape(
    Seq(@{
      entityColumns
        .map(_.exposedType)
        .map(t => s"implicitly[Shape[ShapeLevel.Flat, Column[$t], $t, Column[$t]]]")
        .mkString(", ")
    }),
    vs => @{name}(@{
      entityColumns
        .map(_.exposedType)
        .zipWithIndex
        .map{ case (t,i) => s"vs($i).asInstanceOf[$t]" }
        .mkString(", ")
    }),
    vs => new @{name}Columns(@{
      entityColumns
        .map(_.exposedType)
        .zipWithIndex
        .map{ case (t,i) => s"vs($i).asInstanceOf[Column[$t]]" }
        .mkString(", ")
    })
  )
}
import @{name}Implicits.@{name}Shape

A few helpers we put into the Slick code generator:

  val columnsCSV = entityColumns.map(_.name).mkString(", ")
  val columnsCopy = entityColumns.map(c => colWithType(c)+" = "+c.name).mkString(", ")
  val columnNames = entityColumns.map(_.name.toString)


  def colWithType(c: Column) = s"${c.name}: ${c.exposedType}"

  def colWithTypeAndDefault(c: Column) =
    colWithType(c) + colDefault(c).map(" = "+_).getOrElse("")

  def indentN(n:Int,code: String): String = code.split("\n").mkString("\n"+List.fill(n)(" ").mkString(""))

I know this may a bit troublesome to replicate, especially if you are new to Scala. I hope to to find the time get it into the official Slick code generator at some point.