Extending SLICK Tables in a DRY manner

2020-05-27 10:34发布

问题:

I have an interesting question around Slick/Scala that I am hoping that one of you nice chaps might be able to assist me with.

I have several tables and by extension in SLICK case classes

case class A(...)
case class B(...)
case class C(...)

that share these common fields

(id: String, livemode: Boolean, created: DateTime, createdBy : Option[Account]) . 

Because these fields are repeated in every case class, I'd like to explore the possibility of extracting them into a single object or type.

However, when creating the SLICK table objects I would like things to end up where these common fields are included too so I can persist their individual values in each table.

object AsTable extends Table[A]("a_table") { 
  ...
  def id = column[String]("id", O.PrimaryKey)
  def livemode = column[Boolean]("livemode", O.NotNull)
  def created = column[DateTime]("created", O.NotNull)
  def createdBy = column[Account]("created_by", O.NotNull)
  ... 
} 

Effectively, the end result I'm looking for is to allow me make changes to the common fields without having to update each table.

Is there a way to do this?

Thanks in advance

回答1:

I have not tried this, but how about a trait you mix in:

trait CommonFields { this: Table[_] =>
  def id = column[String]("id", O.PrimaryKey)
  def livemode = column[Boolean]("livemode", O.NotNull)
  def created = column[DateTime]("created", O.NotNull)
  def createdBy = column[Account]("created_by", O.NotNull)

  protected common_* = id ~ livemode ~ created ~ createdBy 
}

Then you can do:

object AsTable extends Table[(String,Boolean,DateTime,Account,String)]("a_table") 
    with CommonFields { 
  def foo = column[String]("foo", O.NotNull)
  def * = common_* ~ foo
} 

The only thing you'll have to repeat now is the type of the elements.

UPDATE

If you want to do object-mapping and:

  1. You map to case-classes
  2. The fields in your case classes are in the same order

Just do:

case class A(
    id: String,
    livemode: Boolean,
    created: DateTime,
    createdBy: Account,
    foo: String)

object AsTable extends Table[A]("a_table") with CommonFields { 
  def foo = column[String]("foo", O.NotNull)
  def * = common_* ~ foo <> (A.apply _, A.unapply _)
}

This seems to be the most economical solution (rather then trying to define * in CommonFields and adding a type parameter). However, it requires you to change all case classes if your fields change.

We could try to mitigate this by using composition on the case classes:

case class Common(
    id: String,
    livemode: Boolean,
    created: DateTime,
    createdBy: Account)

case class A(
    common: Common,
    foo: String)

However, when constructing the mapper function, we will (somewhere) end up having to convert tuples of the form:

(CT_1, CT_2, ... CT_N, ST_1, ST_2, ..., ST_M)

CT Common type (known in CommonFields)
ST Specific type (known in AsTable)

To:

(CT_1, CT_2, ... CT_N), (ST_1, ST_2, ..., ST_M)

In order to pass them to subroutines individually converting Common and A to and from their tuples.

We have to do this without knowing the number or the exact types of either CT (when implementing in AsTable) or ST (when implementing in CommonFields). The tuples in the Scala standard library are unable to do that. You would need to use HLists as for exampled offered by shapeless to do this.

It is questionable whether this is worth the effort.

The basic outline could look like this (without all the implicit mess which will be required). This code will not compile like this.

trait CommonFields { this: Table[_] =>
  // like before

  type ElList = String :: Boolean :: DateTime :: Account :: HNil

  protected def toCommon(els: ElList) = Common.apply.tupled(els.tupled)
  protected def fromCommon(c: Common) = HList(Common.unapply(c))
}

object AsTable extends Table[A] with CommonFields {
  def foo = column[String]("foo", O.NotNull)

  def * = common_* ~ foo <> (x => toA(HList(x)), x => fromA(x) tupled)

  // convert HList to A
  protected def toA[L <: HList](els: L) = {
    // Values for Common
    val c_els = els.take[Length[ElList]]
    // Values for A
    val a_els = toCommon(c_els) :: els.drop[Length[ElList]]

    A.apply.tupled(a_els.tupled)
  }

  // convert A to HList
  protected def fromA(a: A) =
    fromCommon(a.common) :: HList(A.unapply(a)).drop[One]

}

Using some more type magic you can probably resolve the last two issues:

  1. Put toA and fromA into the base trait (by using type parameters in the trait, or using abstract type members)
  2. Avoid defining ElList explicitly by extracting it from Common.apply by using this technique


标签: scala slick