Slick 2.0.0 documentation - 08 User-Defined Features
Permalink to User-Defined Features — Slick 2.0.0 documentation
ここではLifted Embedding APIにおいて、カスタムしたデータ型やデータベース関数をどのようにして用いるのか、についての説明を行う。
もしデータベースシステムがSlickにおける関数として利用できないスカラー関数をサポートしていたのならば、それはSimpleFunction
として別途定義する事が出来る。パラメータや返却型が固定されたunary, binary, ternaryな関数を生成するための関数が事前に用意されている。
// H2 has a day_of_week() function which extracts the day of week from a timestamp
val dayOfWeek = SimpleFunction.unary[Date, Int]("day_of_week")
...
// Use the lifted function in a query to group by day of week
val q1 = for {
(dow, q) <- salesPerDay.map(s => (dayOfWeek(s.day), s.count)).groupBy(_._1)
} yield (dow, q.map(_._2).sum)
より柔軟に型を取り扱いたいのなら、型の不定なインスタンスを取得し、適切な型チェックを行う独自のラッパー関数を書くためにSimpleFunction.apply
を用いる事ができる。
def dayOfWeek2(c: Column[Date]) =
SimpleFunction[Int]("day_of_week").apply(Seq(c))
SimpleBinaryOperatorやSimpleLiteralも同様に扱う事ができる。より柔軟なものを求めるのならば、SimpleExpressionを使うと良い。
完全なテーブル(complete tables)やストアドプロシージャを返却するデータベース関数を扱うのならば、Plain SQL Queriesを使えば良い。複数の結果を返却するストアドプロシージャは現在サポートしていない。
もしカスタムされたカラムが必要ならば、ColumnTypeを実装する事で扱える。最も一般的な利用法として、アプリケーション固有な型をデータベースに存在している型へとマッピングする事などが挙げられる。これはMappedColumnTypeを用いることでよりシンプルに書ける。
// An algebraic data type for booleans
sealed trait Bool
case object True extends Bool
case object False extends Bool
...
// And a ColumnType that maps it to Int values 1 and 0
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
)
...
// You can now use Bool like any built-in column type (in tables, queries, etc.)
より柔軟なものを用いたいのならば、MappedjdbcTypeのサブクラスを用いれば良い。
もしある型を基礎とした独自のラッパークラスを持っているなら、マクロで生成された暗黙的なColumnType
を自由に取得するために、そのクラスをMappedToへと拡張させれる。そのようなラッパークラスは一般的に型安全でテーブル固有な主キー型のために用いられる。
// A custom ID type for a table
case class MyID(value: Long) extends MappedTo[Long]
...
// Use it directly for this table's ID -- No extra boilerplate needed
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は(引数限度が22の)ScalaタプルとSLick独自の実験的な実装であるHList(引数制限が無いが、25個の引数を超えると異様にコンパイルが遅くなるもの)をサポートしている。レコード型はSlickにおいて恣意的にネストされ、混合されたものとして扱われている。
もし柔軟性を必要とするなら、暗黙的なShape定義を行う事で、独自のものをサポートする事が出来る。Pair
を用いた例は以下のようになる。
// A custom record class
case class Pair[A, B](a: A, b: B)
レコード型のためのScape
の実装はMappedScaleProductShapeを拡張する事で行う。一般的にこの実装はシンプルになるが、全ての型に関連するボイラープレートをいくつか必要とする。MappedScaleProductShape
は要素に対するShapeの配列を引数に取り、buildValue
(与えられた要素からレコード型のインスタンスを作成するもの)やcopy
(このShape
をコピーして新しいShape
を作るもの)オペレーションを提供する。
// A Shape implementation for Pair
final class PairShape[Level <: ShapeLevel, M <: Pair[_,_], U <: Pair[_,_], 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[_, _, _, _]]) = 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を提供している。
これらの定義を用いて、タプルやHList
を用いる事の出来るどんな場所においてでもPair
のレコード型を用いる事が出来る。
// Use it in a table definition
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]
as.ddl.create
...
// Insert data with the custom shape
as += Pair(1, "a")
as += Pair(2, "c")
as += Pair(3, "b")
...
// Use it for returning data from a query
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)) }