Slick 3.0.0 documentation - 09 User-Defined Features
Permalink to User-Defined Features — Slick 3.0.0 documentation
本章では、どのようにしてカスタマイズしたデータ型をSlickのScala APIを通して利用するのか、ということについて説明する。
もしデータベースシステムがSlickで利用できないメソッドを関数としてサポートしているのならば、SimpleFunctionを通してその関数を利用することが出来る。固定されたパラメータと返り値を用いる1つ・2つ・3つ組といった関数が様々なデータベースに存在している。
// H2データベースでは day_of_week() 関数により、timestampから曜日を取得することが出来る
val dayOfWeek = SimpleFunction.unary[Date, Int]("day_of_week")
...
// 曜日別にグループ化したクエリを用いたクエリは以下のようになる
val q1 = for {
(dow, q) <- salesPerDay.map(s => (dayOfWeek(s.day), s.count)).groupBy(_._1)
} yield (dow, q.map(_._2).sum)
もっと柔軟に型を変形したい場合(複数引数であったり、OptionとNon-Optionの型を使い分けたい)などには、SimpleFunction.apply
を使って、適切な型チェックを行うラッパー関数を書く事が出来る。
def dayOfWeek2(c: Rep[Date]) =
SimpleFunction[Int]("day_of_week").apply(Seq(c))
SimpleBinaryOperatorとSimpleLiteralも同じように扱うことが出来る。もっと柔軟な操作を行いたい場合には、SimpleExpressionを用いると良い。
val current_date = SimpleLiteral[java.sql.Date]("CURRENT_DATE")
salesPerDay.map(_ => current_date)
全てのテーブルを返すようなデータベースの関数を利用したり、ストアドプロシージャを用いたいといった場合には、Plain SQLクエリを用いて欲しい。
もしカラムに対しカスタマイズした型を適用したいのなら、ColumnTypeを実装して欲しい。アプリケーション特有の型を、データベースにおいて既にサポートされた型へマッピングする事はよくある事例だろう。これを実現するには、MappedColumnTypeを用いて、これに対するボイラープレートを実装するだけで済む。これはドライバをimportする中に含まれており、JdbcDriverのシングルトンオブジェクトから別途importしなくても良い。
// booleanの代数的表現
sealed trait Bool
case object True extends Bool
case object False extends Bool
...
// BoolをIntの1と0にマッピングするためのColumnType
implicit val boolColumnType = MappedColumnType.base[Bool, Int](
{ b => if(b == True) 1 else 0 }, // map Bool to Int
{ i => if(i == 1) True else False } // map Int to Bool
)
...
// この状態で、Boolをビルトインされた型としてテーブルやクエリで利用出来る。
MappedJdbcTypeを使うと、もっと柔軟なマッピングが行える。
もし既にサポートされた型のラッパークラス(ケースクラスやバリュークラスになりえるもの)があるのなら、マクロで生成される暗黙的なColumnType
を自由に取得出来るMappedToを継承したものを利用する。そのようなラッパークラスは一般的に、型安全でテーブル特有な主キーの型に用いられる。
// カスタマイズされたテーブルのID型
case class MyID(value: Long) extends MappedTo[Long]
...
// MyIDをテーブルのID型としてそのまま用いる -- 特別なボイラープレートは必要ない
class MyTable(tag: Tag) extends Table[(MyID, String)](tag, "MY_TABLE") {
def id = column[MyID]("ID")
def data = column[String]("DATA")
def * = (id, data)
}
レコード型は、個々に宣言された型のコンポーネントをいくつか含んだデータ構造として表される。SlickはScalaのタプルをサポートしている以外にも、22個より大きい数のカラム数に対応するためにSlick独自にHListというものを用意している。
カスタマイズされたレコード型(ケースクラス、カスタマイズされたHLists、タプルに似た型など…)を用いるために、Slickに対しどのようにしてクエリと結果型をマッピングするのかというのを伝える必要がある。これに対しては、MappedScalaProductShapeを継承したShapeを用いると良い。
ポリモーフィックなレコード型は、は要素となる型を抽象化する。つまりここでは、持ち上げられた要素の型と生の要素の型の双方で同じレコード型を用いることが出来るようになる。カスタマイズしたポリモーフィックなレコード型を利用するには、適切な暗黙的Shapeを用意してあげたら良い。
Pair
というクラスを使う例は以下のようになる。
// カスタマイズされたレコード型
case class Pair[A, B](a: A, b: B)
...
// PairのためのShape実装
final class PairShape[Level <: ShapeLevel, M <: Pair[_,_], U <: Pair[_,_] : ClassTag, P <: Pair[_,_]](
val shapes: Seq[Shape[_, _, _, _]])
extends MappedScalaProductShape[Level, Pair[_,_], M, U, P] {
def buildValue(elems: IndexedSeq[Any]) = Pair(elems(0), elems(1))
def copy(shapes: Seq[Shape[_ <: ShapeLevel, _, _, _]]) = new PairShape(shapes)
}
...
implicit def pairShape[Level <: ShapeLevel, M1, M2, U1, U2, P1, P2](
implicit s1: Shape[_ <: Level, M1, U1, P1], s2: Shape[_ <: Level, M2, U2, P2]
) = new PairShape[Level, Pair[M1, M2], Pair[U1, U2], Pair[P1, P2]](Seq(s1, s2))
この例における暗黙的なメソッドであるpairShape
は、2つの要素型を持つPair
のためのShapeを提供している(個々の要素型のためのShapeは、いつでも利用可能となる)。
これらの定義を用いれば、Slickを利用するどの場所においてもPair
をレコード型として利用出来る。
// テーブル定義にPairを利用する
class A(tag: Tag) extends Table[Pair[Int, String]](tag, "shape_a") {
def id = column[Int]("id", O.PrimaryKey)
def s = column[String]("s")
def * = Pair(id, s)
}
val as = TableQuery[A]
...
// カスタマイズされた型のデータを挿入する
val insertAction = DBIO.seq(
as += Pair(1, "a"),
as += Pair(2, "c"),
as += Pair(3, "b")
)
...
// クエリからPairを返却してもらう
val q2 = as
.map { case a => Pair(a.id, (a.s ++ a.s)) }
.filter { case Pair(id, _) => id =!= 1 }
.sortBy { case Pair(_, ss) => ss }
.map { case Pair(id, ss) => Pair(id, Pair(42 , ss)) }
// returns: Vector(Pair(3,Pair(42,"bb")), Pair(2,Pair(42,"cc")))
カスタマイズされたケースクラスが単一的なレコード型としてしばしば用いられる(要素型が固定されたレコード型など)。Slickにおいてこのようなケースクラスを用いるためには、レコードの生の値を取り扱うケースクラスを定義するのに加えて、持ち上げられたレコードの値を取り扱うケースクラスを定義する必要がある。
カスタマイズしたケースクラスのShapeを提供するためには、CaseClassShapeを用いると良い。
// 2つのケースクラスを用意
case class LiftedB(a: Rep[Int], b: Rep[String])
case class B(a: Int, b: String)
...
// 定義したケースクラスに対するマッピング
implicit object BShape extends CaseClassShape(LiftedB.tupled, B.tupled)
...
class BRow(tag: Tag) extends Table[B](tag, "shape_b") {
def id = column[Int]("id", O.PrimaryKey)
def s = column[String]("s")
def * = LiftedB(id, s)
}
val bs = TableQuery[BRow]
...
val insertActions = DBIO.seq(
bs += B(1, "a"),
bs.map(b => (b.id, b.s)) += ((2, "c")),
bs += B(3, "b")
)
...
val q3 = bs
.map { case b => LiftedB(b.id, (b.s ++ b.s)) }
.filter { case LiftedB(id, _) => id =!= 1 }
.sortBy { case LiftedB(_, ss) => ss }
...
// returns: Vector(B(3,"bb"), B(2,"cc"))
このメカニズムは、<> オペレータを用いたクライアントサイドマッピングの代わりとして用いられている。これにはすこしばかりボイラープレートが必要になるが、生のレコードと持ち上げられたレコードの双方において同じフィールド名を持たせてくれる。
以下の例では、マッピングされたケースクラスと、他でマッピングされたケースクラスでマッピングされたPair
の2つを組み合わせている。
// 複数のマッピングされた型を組み合わせている
case class LiftedC(p: Pair[Rep[Int],Rep[String]], b: LiftedB)
case class C(p: Pair[Int,String], b: B)
...
implicit object CShape extends CaseClassShape(LiftedC.tupled, C.tupled)
...
class CRow(tag: Tag) extends Table[C](tag, "shape_c") {
def id = column[Int]("id")
def s = column[String]("s")
def projection = LiftedC(
Pair(column("p1"),column("p2")), // (cols defined inline, type inferred)
LiftedB(id,s)
)
def * = projection
}
val cs = TableQuery[CRow]
...
val insertActions2 = DBIO.seq(
cs += C(Pair(7,"x"), B(1,"a")),
cs += C(Pair(8,"y"), B(2,"c")),
cs += C(Pair(9,"z"), B(3,"b"))
)
...
val q4 = cs
.map { case c => LiftedC(c.projection.p, LiftedB(c.id,(c.s ++ c.s))) }
.filter { case LiftedC(_, LiftedB(id,_)) => id =!= 1 }
.sortBy { case LiftedC(Pair(_,p2), LiftedB(_,ss)) => ss++p2 }
...
// returns: Vector(C(Pair(9,"z"),B(3,"bb")), C(Pair(8,"y"),B(2,"cc")))