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
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:
- You map to case-classes
- 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:
- Put
toA
and fromA
into the base trait (by using type parameters in the trait, or using abstract type members)
- Avoid defining
ElList
explicitly by extracting it from Common.apply
by using this technique