slick-doc-ja 1.0 

Slick 1.0 documentationの日本語訳です。

他のバージョンのドキュメント 

Slick 1.0.0 documentation - 01 導入 Permalink to Introduction — Slick 1.0.0 documentation

導入 

Slickとは 

SlickはTypesafe社によって開発が行われている,Scalaのためのモダンなデータベースラッパーである.データベースにアクセスしながらScalaのコレクションを扱うかのようにデータを操作する事が出来る.また,SQLを直接書く事も可能である.

val limit = 10.0
// クエリはこのように書く事が出来る
( for( c <- Coffees; if c.price < limit ) yield c.name ).list
// SQLを直接書いた例
sql"select name from coffees where price < $limit".as[String].list

クエリをSQLを用いて書く代わりにScalaを用いるとコンパイル時に合成が安全に行われ,より良い形で利用する事が出来る.Slickは独自のクエリコンパイラを用いてDBに対しクエリを発行する.

Slickの特徴 

Slickは以下のような特徴を持っている:

Easy 

Concise 

Safe 

Composable 

SlickはScala2.10を必要とします. (Scala2.9を利用する際にはSlickの前身である,ScalaQueryを利用してください.)

サポートするデータベース 

他のSQLデータベースもSlickなら簡単にアクセスする事が出来るでしょう.独自のSQLベースのバックエンドを持つデータベースも,プラグインを作成する事でSlickを利用することが出来ます.そのようなプラグインの作成は大きな貢献となるでしょう. NoSQLのような他のバックエンドを持つようなデータベースに関しては現在開発中であるため,まだ利用する事はできません..

簡単な概説 

SlickのLiftedEmbeddingによるデータベースへのアクセスは以下のステップで行う事ができます.

1.Slickのjarファイルをプロジェクトのdependenciesへ追加する

2.利用するDBに応じたDriverをimportし,セッションを作成する(もしくは単純にthreadLocalSessionをimportする).

import scala.slick.driver.H2Driver.simple._
import Database.threadLocalSession

3.データベーススキーマを記述する

object Coffees extends Table[(String, Double)]("COFFEES") {
  def name = column[String]("COF_NAME", O.PrimaryKey)
  def price = column[Double]("PRICE")
  def * = name ~ price
}

4.セッションスコープ内にて,for式かmap/flatMapを用いてクエリを記述する

Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
  ( for( c <- Coffees; if c.price < 10.0 ) yield c.name ).list
  // or
  Coffees.filter(_.price < 10.0).map(_.name).list
}

次の章では,より詳細な扱い方や機能について説明します.

License 

Slick is released under a BSD-Style free and open source software license.

Slick 1.0.0 documentation - 02 始めよう

Permalink to Getting Started — Slick 1.0.0 documentation

始めよう 

最も簡単なSlickアプリケーションの設定方法はSlick Examplesのプロジェクトを用いる事です.このプロジェクトに含まれているREADMEに従ってビルドをして,実行してみてください.

依存性 

プロジェクトではどのようにしてSlickを用いれば良いのか確認してみよう.まず初めに,Slickと組み込みデータベースを追加する必要がある.もしsbtを使っているのなら, build.sbt に対して以下のような記述を追加すれば良い.

libraryDependencies ++= List(
  // 適切なSlickのversionをここに指定しよう
  "com.typesafe.slick" %% "slick" % "1.0.0",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.h2database" % "h2" % "1.3.166"
)

SlickはデバッグログにSLF4Jを用いている.そのためSLF4Jについても追加する必要がある.ここではロギングを無効にするために slf4j-nop を用いている.もしログの出力を見たいのならばLogbackのようなロギング用のフレームワークに替えなくてはならない.

Imports 

Slick example lifted/FirstExampleは,独立した1つのアプリケーションとなっている.このアプリケーションでは以下のようなimport文を記述している.

// H2 databaseへ接続するためにH2Driverをimport
import scala.slick.driver.H2Driver.simple._
...
// Use the implicit threadLocalSession
import Database.threadLocalSession

H2 Databaseを用いているため,Slickの H2Driver をimportする必要がある.このdriverに含まれる simple オブジェクトにはsession handlingといったSlickに必要な共通の機能が含まれている.それ以外にimportする必要があるのは threadLocalSession である.これは取り扱うスレッドにセッションを付与する事でセッションの取り扱いを単純化させるものである.これにより不必要なimplicit変数を割り当てたりといった実装を行わなくて済む.

Databaseへの接続 

アプリケーションの中では,どのようにデータベースに接続するのかを明示する Database オブジェクトを初めに作る.そしてセッションを開き,続くブロック内に処理を記述する.

Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
  // セッションは明示的に名付けられる事はない
  // セッションは現在のスレッドに対し,importしたthreadLocalSessionとして束縛されるのである
}

Java SEの環境においては,データベースセッションはJDBCドライバークラスを用いてJDBC URLへ接続する事で作られる(正しいURLの記述法はJDBCドライバーのドキュメントを見て欲しい).もしplain SQL queriesのみを用いるのであれば,それ以上何もする必要はない.しかし,もしdirect embeddinglifted embeddingを用いるのであれば,SlickがSQL文を作成する事になるため, H2Driver のようなSlickのdriverを適宜importして欲しい.

スキーマ 

このアプリケーションではlifted embeddingを用いているため,データベースのテーブルに対応する Table オブジェクトを書かなくてはならない.

// SUPPLIERSテーブルの定義
object Suppliers extends Table[(Int, String, String, String, String, String)]("SUPPLIERS") {
  def id = column[Int]("SUP_ID", O.PrimaryKey) // 主キー
  def name = column[String]("SUP_NAME")
  def street = column[String]("STREET")
  def city = column[String]("CITY")
  def state = column[String]("STATE")
  def zip = column[String]("ZIP")
  // 全てのテーブルではテーブルの型パラメタと同じタイプの射影*を定義する必要がある.
  def * = id ~ name ~ street ~ city ~ state ~ zip
}
...
// COFFEESテーブルの定義
object Coffees extends Table[(String, Int, Double, Int, Int)]("COFFEES") {
  def name = column[String]("COF_NAME", O.PrimaryKey)
  def supID = column[Int]("SUP_ID")
  def price = column[Double]("PRICE")
  def sales = column[Int]("SALES")
  def total = column[Int]("TOTAL")
  def * = name ~ supID ~ price ~ sales ~ total
  // 他のテーブルとの結合のため作成された関係を表す外部キー
  def supplier = foreignKey("SUP_FK", supID, Suppliers)(_.id)
}

全ての列は名前(ScalaにおけるキャメルケースやSQLにおける大文字とアンダースコアの組み合わせ)とScalaの型(SQLの型はScalaの型から自動的に推測される)を持つ.これらは val ではなく def を用いて定義しなくてはならない.テーブルオブジェクトもScalaでの名前とSQLでの名前と型を持つ必要がある.テーブルの型引数は射影*と一致してなくてはならない.全ての列をタプルで取り出すといった簡単な処理以外にも,より複雑なオブジェクトへのマッピングを行う事も出来る.

Coffees テーブルで定義した 外部キー は, Coffees テーブルの supID のフィールドが, Suppliers テーブルで存在している id と同じ値を持っている事を保証している.要するに,ここでは多:1の関係を作成しているのである.ある Coffees の列は特定の Suppliers の列を指すが,複数のCoffeeが同じSupplierを指していたりする.この構成はデータベースレベルで強制されている.

Populating the Database 

組み込みのH2データベースエンジンへ接続すると,空のデータベースが作られる.クエリを実行する前に,データベーススキーマ( Coffees テーブルと Suppliers テーブルから成るもの)を作成し,いくつかのテストデータを挿入してみる.

// 主キーと外部キーを含むテーブルを作成する
(Suppliers.ddl ++ Coffees.ddl).create
...
// supplierをいくつか挿入する
Suppliers.insert(101, "Acme, Inc.",      "99 Market Street", "Groundsville", "CA", "95199")
Suppliers.insert( 49, "Superior Coffee", "1 Party Place",    "Mendocino",    "CA", "95460")
Suppliers.insert(150, "The High Ground", "100 Coffee Lane",  "Meadows",      "CA", "93966")
...
// coffeeをいくつか挿入する(DBがサポートしている場合には,JDBCのバッチ処理を用いる)
Coffees.insertAll(
  ("Colombian",         101, 7.99, , ),
  ("French_Roast",       49, 8.99, , ),
  ("Espresso",          150, 9.99, , ),
  ("Colombian_Decaf",   101, 8.99, , ),
  ("French_Roast_Decaf", 49, 9.99, , )
)

テーブルの ddl 関数は,テーブルやその他データベースのエンティティを作成したり削除したりするための,データベース特有のコードを用いて DDL (data definition language)オブジェクトを作成する.複数の DDL++ を用いる事で,お互いが依存し合っていたとしても,全てのエンティティに対し正しい順序で作成と削除を行う.

複数のデータを挿入する際は insertinsertAll といった関数を用いる.デフォルトではデータベースの Sessionauto-commit モードになっている事に注意して欲しい. insertinsertAll のようなデータベースへの呼び出しはトランザクションにおいて,原子性が保たれるよう実行される(つまり,それらの処理は完全に実行するか全く実行しないかのいずれかが保証される).このモードにおいては, Coffee が対応するSupplierのIDのみを参照するため, Supplier テーブルに対し先にデータを挿入しなくてはならない.

これらの記述を全て包括した明示的なトランザクションのブラケットを用いることも可能である.その際,トランザクションによって処理が強制されるため,順序は重要視されない.

Querying 

最も簡単なクエリ例として,の一つにテーブルのデータを全て順々に取り出す処理を考える.

// coffeeのデータを全て取り出し,順に出力する
Query(Coffees) foreach { case (name, supID, price, sales, total) =>
  println("  " + name + "t" + supID + "t" + price + "t" + sales + "t" + total)
}

この処理はSQLに SELECT * FROM COFFEES を投げた結果と同じである(ただし射影関数*を異なる形式で作成した場合には,少し違う結果となる).ループの中で得られる値の型は当然 Coffees の型引数と一致する.

上記の例に射影処理を追加してみよう.これにはScalaで mapfor式 を用いる事で記述することが出来る.

// なぜデータベースでは文字列の変換や連結が出来ないんだろう...?
val q1 = for(c <- Coffees) // Coffeesは自動的にQueryへとなる
  yield ConstColumn("  ") ++ c.name ++ "\t" ++ c.supID.asColumnOf[String] ++
    "\t" ++ c.price.asColumnOf[String] ++ "\t" ++ c.sales.asColumnOf[String] ++
    "\t" ++ c.total.asColumnOf[String]
// 初めの文字定数はConstColumへ手動で変換する必要がある.
// その後++オペレータにより結合させる
q1 foreach println

全ての行がタブによって区切られた文字列として連結した結果が得られるだろう.違いはデータベースの内側で処理が行われた事であり,結果として得られる連結した文字列は同様に取得出来る.Scalaの + オペレータはしばしばオーバーライドされてしまうため,seqの結合で一般的に用いられている ++ の方を利用すべきだ.また,他の引数型から文字列への自動的な型変換は存在しない.この処理は型変換関数である asColumnOf により明示的に行うべきである.

テーブルの結合やフィルタリングはScalaのコレクションと同じように処理する事が出来る.

// 2つのテーブルを結合し,coffeeの値段が$9より安いもののうち,
// coffeeの名前とsupplierの名前の組みを検索
val q2 = for {
  c <- Coffees if c.price < 9.0
  s <- Suppliers if s.id === c.supID
} yield (c.name, s.name)

2つの値が等しいかを比較する際に, == の代わりに === を用いている事に注意して欲しい.同様に,LiftedEmbeddingでは != の代わりに =!= を用いている.それ以外の比較に関するオペレータ( < , <= , >= , > )はScalaで用いているものと同じである

Suppliers if s.id === c.supID という表現は Coffees.supplier という外部キーにより作成された関係に基いている.joinの条件を繰り返す代わりに,このような方法で直接的に外部キーを用いた結合が行える.

val q3 = for {
  c <- Coffees if c.price < 9.0
  s <- c.supplier
} yield (c.name, s.name)

Slick 1.0.0 documentation - 03 Lifted Embedding

Permalink to Lifted Embedding — Slick 1.0.0 documentation

Lifted Embedding 

lifted embedding はSlickにおいて型安全なクエリ操作が行える基本的なAPIである.導入には*始めよう*を読んで欲しい.この章ではSlick及び lifted embedding の特徴と詳細について説明する.

Lifted Embedding という名前はScalaの基本的な型を用いる*direct embedding*と異なり,scala.slick.lifted.Repの型コンストラクタへと変化するような型を用いている事に基づいている.これはScalaのシンプルなコレクションと, lifted embedding を用いたコードを比べると明らかである.

こちらがScalaのコレクションの操作,

case class Coffee(name: String, price: Double)
val l: List[Coffee] = //...
val l2 = l.filter(_.price > 8.0).map(_.name)
//                  ^       ^          ^
//                Double  Double     String

そしてこちらが Lifted Embedding を用いた操作である.

object Coffees extends Table[(String, Int, Double, Int, Int)]("COFFEES") {
  def name = column[String]("COF_NAME", O.PrimaryKey)
  def price = column[Double]("PRICE")
  //...
}
val q = Query(Coffees)
val q2 = q.filter(_.price > 8.0).map(_.name)
//                  ^       ^          ^
//          Rep[Double]  Rep[Double]  Rep[String]

シンプルな型はRepへと変換させられる.レコードの型であるCoffeesは,Rep[(String, Int, Double, Int, Int)]のサブタイプへ,8.0といった数値リテラルも,自動的なimplicit conversionにより,Rep[Double]へと変化する.というのも,Rep[Double]における>オペレータの右辺にその型が必要になるためである.

テーブル 

lifted embeddingを用いるためには,データベースのテーブル毎に,テーブルオブジェクトを定義する必要がある.

object Coffees extends Table[(String, Int, Double, Int, Int)]("COFFEES") {
  def name = column[String]("COF_NAME", O.PrimaryKey)
  def supID = column[Int]("SUP_ID")
  def price = column[Double]("PRICE")
  def sales = column[Int]("SALES")
  def total = column[Int]("TOTAL")
  def * = name ~ supID ~ price ~ sales ~ total
}

Slickはテーブルオブジェクトを複製してテーブルを作成するため,不要な状態等を付与すべきではない(操作するための関数に関しては問題無い).Tableを継承するオブジェクトは 静的な位置 (トップレベルや他のオブジェクトの中でのみネストされた場所)で定義されることは無い.これはscalacによって行われる最適化で,不要な責務まで持ってしまうという問題を防止するためである.匿名の構造型を用いたり区別されたクラス定義を用いることで,テーブル内でvalを用いる事は推奨している.

全てのカラムはカラムメソッドを通して定義される.カラムはvalではなくdefを用いて定義しなくてはならない.これはカラムは複製する必要があるためである.各々のカラムはScalaの型とデータベースにおけるカラム名(一般的には大文字)を持っている.以下のプリミティブ型については,各データベースドライバーによって課せられた特定の制限を持ちつつも,基本的にはそのまま用いる事が出来る.

nullを許可するカラムについては,Tがプリミティブ型である際には,Option[T]を用いて表す事が出来る.ただし,Optionの全ての操作に関して,現在ではScalaのOptionセマンティクスとは少し異なったデータベースのnullプロパゲーションセマンティクスを用いている.(例として,None === None はfalseを返す).このような挙動は将来的に改善される予定だ.

カラム名の後には,カラムの定義に関するオプションを追記することができる.それらのオプションはテーブルのOオブジェクトを通して利用する事が出来る.例として,以下のようなオプションを用いることが出来る.

NotNull, Nullable

nullを許可する,nullを許可しないといったことを,DDLの作成時に明示するもの.nullに出来るかどうかといったことは,OptionかOptionで無いかといったように,型からも決定させることが出来る.

PrimaryKey

DDLを作成する際に,主キーとしてカラムを宣言する.

Default\T\

データをテーブルに挿入する際に用いるデフォルト値を指定する.この情報はDDLを作成する時にのみ用いられる.

DBType(dbType: String)

DDLに特定のデータベースの型を用いる際に利用する.例えばStringのカラムに対してVARCHAR(20)を指定する際には,DBType(“VARCHAR(20)”)と明示する.

AutoInc

DDLを作成する際に,カラムに対して自動インクリメントなキーとして指定させる.他のカラムのオプションとは異なり,このオプションはDDLを作成する時以外に意味を持つ.多くのデータベースではデータを挿入する際に,自動インクリメントでないカラムを返す事を許可していない.そこでSlickでは返されるカラムが自動インクリメントとなっているかどうかを必要に応じてチェックする.

全てのテーブルではデフォルトの射影となる*関数を必要とする.これはクエリから行を返すときどのような形で値を返すかを指定するものである.Slickの*射影はデータベースから通常得られる射影と全く同じにする必要は無い.何らかの計算を行った新しいカラムを作成してもいいし.いくつかのカラムを省略してしまっても良い,*射影で得られる型と同じ型パラメータをテーブルへ指定する.おおよそこれは単一のカラム型か,カラム型のタプルになるだろう.

拡張テーブル 

自分で定義したクラスに対し,テーブルをマッピングする事も出来る.オペレータを用いる事で,双方向マッピングにより*射影がその型に対応するようになる.

case class User(id: Option[Int], first: String, last: String)
...
object Users extends Table[User]("users") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def first = column[String]("first")
  def last = column[String]("last")
  def * = id.? ~ first ~ last <> (User, User.unapply _)
}

Optionで結果をラップしたシンプルなapplyメソッドとunapplyメソッドを持つcase Classへと最適化されるが,マッピングされた型を直接操作するオーバーロードも存在している.

制約 

外部キー制約はテーブルのforeignKeyメソッドによって定義される.制約,カラム(または行),リンクされるテーブル,そしてテーブルから一致する行への関数として,特定の名前を与える必要がある.DDLを作成する際に,外部キーはその名前を用いて追加される.

object Suppliers extends Table[(Int, String, String, String, String, String)]("SUPPLIERS") {
  def id = column[Int]("SUP_ID", O.PrimaryKey)
  //...
}
...
object Coffees extends Table[(String, Int, Double, Int, Int)]("COFFEES") {
  def supID = column[Int]("SUP_ID")
  //...
  def supplier = foreignKey("SUP_FK", supID, Suppliers)(_.id)
}

データベースにおける実際の制約とは独立に, join を用いてデータを結合する際に,外部キーは利用される.この時,joinされたデータを探すための自分で定義した便利な関数のように動作させる事が出来る.

def supplier = foreignKey("SUP_FK", supID, Suppliers)(_.id)
def supplier2 = Suppliers.where(_.id === supID)

主キー制約はprimaryKey関数を追加する事で,同様に定義することが出来る.これは複合主キーを定義する際に役立つ.複合主キーを用いる際は,カラムのオプションにO.PrimaryKeyをつける事は出来ない.

object A extends Table[(Int, Int)]("a") {
  def k1 = column[Int]("k1")
  def k2 = column[Int]("k2")
  def * = k1 ~ k2
  def pk = primaryKey("pk_a", (k1, k2))
}

他のindexについてはindex関数を用いて同様に定義することが可能だ.uniqueパラメータを設定しなければ,デフォルトでは各indexはuniqueでは無い状態になっている.

object A extends Table[(Int, Int)]("a") {
  def k1 = column[Int]("k1")
  def k2 = column[Int]("k2")
  def * = k1 ~ k2
  def idx = index("idx_a", (k1, k2), unique = true)
}

全ての制約はテーブルで定義された適切な返り値を持つ関数を探す際に,反射的に適応される.これはtableConstraints関数をオーバーライドすることにより自由にカスタマイズする事が出来る.

Data Definition Language 

DDLステートメントはddl関数を用いて作成される.複数のDDLオブジェクトは++を用いて正しい順番にcreateとdropが行われるように連結される.これはテーブルの依存関係が循環している場合にも上手く機能する.ステートメントはcreate関数やdrop関数を用いて実行される.

val ddl = Coffees.ddl ++ Suppliers.ddl
db withSession {
  ddl.create
  //...
  ddl.drop
}

SQLのコードを取得するには,createStatementsやdropStatements関数を用いると良い.

ddl.createStatements.foreach(println)
ddl.dropStatements.foreach(println)

Expressions 

プリミティブ値(not 複合型,not コレクション)は,TypeMapper[T]が存在していれば,Rep[T]のサブタイプであるColumn[T]という型によって表される.内部的に用いられるいくつかの特別な関数と,nullを許可するかnullを許可しないカラム間の変換を行う関数のみ,Columnクラスで定義がなされている.

lifted embeddingにおいて一般的に用いられているオペレータや他の関数については,ExtensionMethodConversionsで定義された暗黙的な変換を通して追加される.実際の関数は,AnyExtensionMethods,ColumnExtensionMethods,NumericColumnExtensionMethods,BooleanColumnExtensionMethods,StringColumnExtensionMethodsといったクラスの中にある.

コレクションはflatMap,filter,take,groupByといったコレクションに本来用意されている基本的な関数を持った,Queryクラス(Rep[Seq[T]])によって表されている.変換された型とシンプルな型といったようなQueryの異なる2つの複合型により,先のような関数はとても複雑なものになる.しかし,意味的にはScalaのコレクションと本質は同じになる.

それ以外にも,複合でない値のクエリのための関数がSingleColumnQueryExtensionMethodsへ暗黙的な変換を通して追加されている.

ソートとフィルタリング(Sorting, Filtering) 

様々な種類のソートやフィルタリングが存在している.例えばQueryを引数に,同じ型である新しいQueryを返すものなどがある.

val q = Query(Coffees)
val q1 = q.filter(_.supID === 101)
val q2 = q.drop(10).take(5)
val q3 = q.sortBy(_.name.desc.nullsFirst)

結合(Join, Zipping) 

joinは1つのクエリで異なる2つの異なったテーブルやクエリを結合させるために用いられる.

joinを記述するには2つの方法がある. 明示的な joinは2つのクエリを,個々の結果であるタプルの単一クエリへと結合させる関数を呼び出すことによって行う. 暗黙的な joinは特別な関数を呼び出すこと無く,クエリを特定の形へと変形させる.

暗黙的な交差結合(cross-join) はQueryにおいてflatMapを用いることで実装出来る.for式を用いることでより簡単に表現することが可能だ.

val implicitCrossJoin = for {
  c <- Coffees
  s <- Suppliers
} yield (c.name, s.name)

もしfilterのような操作を行った場合には,それは暗黙的な内部結合(inner-join)となる.

val implicitInnerJoin = for {
  c <- Coffees
  s <- Suppliers if c.supID === s.id
} yield (c.name, s.name)

これらの暗黙的な結合はScalaコレクションにおけるflatMapを用いた時と全く同じような意味を持つ.

明示的な結合はjoin関数を呼び出す事で作成出来る.

val explicitCrossJoin = for {
  (c, s) <- Coffees innerJoin Suppliers
} yield (c.name, s.name)
...
val explicitInnerJoin = for {
  (c, s) <- Coffees innerJoin Suppliers on (_.supID === _.id)
} yield (c.name, s.name)
...
val explicitLeftOuterJoin = for {
  (c, s) <- Coffees leftJoin Suppliers on (_.supID === _.id)
} yield (c.name, s.name.?)
...
val explicitRightOuterJoin = for {
  (c, s) <- Coffees rightJoin Suppliers on (_.supID === _.id)
} yield (c.name.?, s.name)
...
val explicitFullOuterJoin = for {
  (c, s) <- Coffees outerJoin Suppliers on (_.supID === _.id)
} yield (c.name.?, s.name.?)

交差結合や内部結合の明示的なversionsは,暗黙的なversions(大抵はSQLにおける暗黙的な結合)として生成されるSQLのコードへと帰着する.外部結合における.?の利用には注意して欲しい.これらの結合は付随的なNULL(左外部結合における右辺,右外部結合における左辺,完全外部結合における両辺)を生み出すが,そこからOption値を取得する事が出来る.

交差結合や外部結合によらない関係データベースによってサポートされる一般的なjoinオペレータに加えて,Slickでは2つのクエリのペアワイズ結合を生成するzip joinを用意している.またそれは,ScalaのコレクションにおけるzipやzipWith関数を用いた時と全く同じような意味を持つ.

val zipJoinQuery = for {
  (c, s) <- Coffees zip Suppliers
} yield (c.name, s.name)
...
val zipWithJoin = for {
  res <- Coffees.zipWith(Suppliers, (c: Coffees.type, s: Suppliers.type) => (c.name, s.name))
} yield res

zip joinにはzipWithIndexのような特有な結合がある.これは0から始まる無限長の数列をクエリの結果と結合させる.SQLデータベースではそのような数列を表すことが出来ないし,Slickも現在ではそれについてサポートしていない(今後変更するかもしれない).しかし,zipされたクエリ結果というのは行番号関数を用いる事でSQLにおいても表現する事が出来る.つまりzipWithIndexはプリミティブなオペレータとしてサポートされているのである.

val zipWithIndexJoin = for {
  (c, idx) <- Coffees.zipWithIndex
} yield (c.name, idx)

連結(Unions) 

適応可能な型については,unionやunionAllというオペレータを用いて2つのクエリを連結させる事が出来る.

val q1 = Query(Coffees).filter(_.price < 8.0)
val q2 = Query(Coffees).filter(_.price > 9.0)
val unionQuery = q1 union q2
val unionAllQuery = q1 unionAll q2

複製された値をフィルタリングするような結合と異なり,unionAllではシンプルに,時折より効率的に,複数のクエリを結合させる.

集約(Aggregation) 

集約の簡単な例として,単一のカラムを返すクエリからプリミティブ値を計算する事を考える.単一のカラムというのは基本的に数値型となる.

val q = Coffees.map(_.price)
val q1 = q.min
val q2 = q.max
val q3 = q.sum
val q4 = q.avg

以下のような集約関数は任意のクエリにおいて実行する事が出来る.

val q = Query(Coffees)
val q1 = q.length
val q2 = q.exists

グループ化はgroupByメソッドによって実装されている.これもScalaのコレクション操作と同じ様に機能する.

val q = (for {
  c <- Coffees
  s <- c.supplier
} yield (c, s)).groupBy(_._1.supID)
...
val q2 = q.map { case (supID, css) =>
  (supID, css.length, css.map(_._1.price).avg)
}

Note that the intermediate query q contains nested values of type Query. These would turn into nested collections when executing the query, which is not supported at the moment. Therefore it is necessary to flatten the nested queries by aggregating their values (or individual columns) as done in q2.

クエリの実行(Querying) 

QueryはInvokerトレイト(パラメータが無い場合にはUnitInvoker )において定義されたメソッドを用いて実行される.Queryからの暗黙的な変換が存在しているため,全てのQueryを直接的に実行する事が出来る.もっとも一般的な使用方法は,listといった特定の関数や,様々な種類のコレクションを生成する関数(to[Vector]など)を用いてコレクションへと結果を変換させる事である.

val l = q.list
val v = q.to[Vector]
val invoker = q.invoker
val statement = q.selectStatement

この例では,明示的に暗黙的な変換メソッドを呼び出す事無しに,invokerへの参照をどのようにして取得するのかを表している.

クエリを実行する全てのメソッドは暗黙的にSessionの値を保持している.もちろん,明示的にsessionを通しても構わない.

val l = q.list(session)

もし返り値として一つだけの値を返したいなら,firstやfirstOptionといったメソッドを使う事が出来る.foreach, foldLeft, elementsといったメソッドは全てのデータをScalaのコレクションへとコピーする事なしに,得られた結果をイテレートさせて利用する事が出来る.

削除(Deleting) 

データの削除は先のQueryingと同じように機能する.何かデータを削除する時には,適当な行をselectした後にdeleteを呼び出すだろう.Queryからdelete関数や自己参照するdeleteInvokerを持ったDeleteInvokerへの暗黙的な変換が存在している.

val affectedRowsCount = q.delete
val invoker = q.deleteInvoker
val statement = q.deleteStatement

削除を行うクエリは単一のテーブルのみを指定する.どんな射影も無視される.

挿入(Inserting) 

データの挿入は単一のテーブルに対し,カラムの射影を用いて行われる.テーブルを直接的に利用する場合,挿入は*射影を用いずに実行される.挿入時にテーブルのカラムを一部省くと,データベース作成時に定義されたデフォルト値か,もしくは適当に用意した,型に応じた非明示的なデフォルト値をデータベースに挿入する.データ挿入のための全てのメソッドはInsertInvokerFullInsertInvokerにおいて定義されている.

Coffees.insert("Colombian", 101, 7.99, 0, 0)
...
Coffees.insertAll(
  ("French_Roast", 49, 8.99, 0, 0),
  ("Espresso",    150, 9.99, 0, 0)
)
...
// "sales"と"total"はデフォルト値である0を用いる
(Coffees.name ~ Coffees.supID ~ Coffees.price).insert("Colombian_Decaf", 101, 8.99)
...
val statement = Coffees.insertStatement
val invoker = Coffees.insertInvoker

あるデータベースシステムではAutoIncとなっているカラムへの適切な値の挿入や作成された値を取得するためにNoneという値を挿入することを許可している一方,多くのデータベースではこのような操作を禁じているため,これらのカラムについて省けるかどうかは,データベースシステムについて調べて確認をしなくてはならない.Slickはまだ自動的にこの処理を行うような機能を持ってはいないが,将来的に追加する予定である.現時点では以下の例にあるforInsertのような,AutoIncとなっているカラムを含まない射影を用いるべきである.

case class User(id: Option[Int], first: String, last: String)
...
object Users extends Table[User]("users") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def first = column[String]("first")
  def last = column[String]("last")
  def * = id.? ~ first ~ last <> (User, User.unapply _)
  def forInsert = first ~ last <> ({ t => User(None, t._1, t._2)}, { (u: User) => Some((u.first, u.last))})
}
...
Users.forInsert insert User(None, "Christopher", "Vogt")

このような処理を行う際,AutoIncで自動生成された主キーのカラムを取得したいと考える事があるだろう.デフォルトでは,insert関数は影響を与えた行の数(大抵1になる)を返り値として返すし,insertAll関数はOption(もしデータベースが全ての行のための数え上げ機能を提供していない場合にはNoneとなる)における計算された数を返す.insertから単一の値やタプル,insertAllからSeqのような値として,返されるカラムを指定する場合には,returning関数を用いる事で指定が可能になる.

val userId =
  Users.forInsert returning Users.id insert User(None, "Stefan", "Zeiger")

多くのデータベースでは単一のカラムを返す際に,テーブルのAutoIncな主キーを返す事を許可している.もし他のカラムが叩かれたら,SlickExceptionが実行中に(データベースが実際にそれをサポートしていない限り)投げられてしまう.

クライアント側からデータを挿入する代わりに,データベースサーバーにおいて実行されるQueryやスカラー表現によって作られたデータを挿入すること事も出来る.

object Users2 extends Table[(Int, String)]("users2") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def * = id ~ name
}
...
Users2.ddl.create
Users2 insert (Users.map { u => (u.id, u.first ++ " " ++ u.last) })
Users2 insertExpr (Query(Users).length + 1, "admin")

更新(Updating) 

データの更新は該当するデータをselectしてから,新たなデータへ更新することになるだろう.そのようなクエリは単一テーブルからselectされた生のカラム(計算された値ではない)のみを返すべきである.更新に関係する関数はUpdateInvokerにおいて定義されている.

val q = for { c <- Coffees if c.name === "Espresso" } yield c.price
q.update(10.49)
...
val statement = q.updateStatement
val invoker = q.updateInvoker

現時点では,更新のための,データベース内にあるデータのスカラー表現や変換を利用する方法は無い.

クエリテンプレート 

クエリテンプレートは任意のパラメータが決められたクエリのことである.複数のパラメータを取る関数のようにテンプレートは機能し,より効率的にQueryを返す.通常,クエリを作成するために関数を評価する際,新しいクエリとなるASTをその関数は構築し,そのクエリを実行する際に,たとえ同じSQL文が結果を返したとしても,常にクエリコンパイラによって毎度クエリはコンパイルされる.一方で,クエリテンプレートは単一のSQL文(全てのパラメータが変数へバインドされるが)に制限され,たった一度しかクエリはビルド,コンパイルされない.

クエリテンプレートはParametersオブジェクトのflatMapを呼び出すことによって作る事が出来る.大抵の場合,for式を1つ書くことで,テンプレートを作成する事が出来る.

val userNameByID = for {
  id <- Parameters[Int]
  u <- Users if u.id is id
} yield u.first
...
val name = userNameByID(2).first
...
val userNameByIDRange = for {
  (min, max) <- Parameters[(Int, Int)]
  u <- Users if u.id >= min && u.id < max
} yield u.first
...
val names = userNameByIDRange(2, 5).list

ユーザ定義関数とユーザ定義型 

もしデータベースシステムがSlickにおける関数として利用出来るスカラー関数を用意していたとしたら,それをSimpleFunctionとして定義することが出来る.パラメータと返り値を固定した1つ,2つ,もしくは3つの関数を作成するための関数が既に定義されている.

// H2はタイムスタンプから曜日を抽出する関数であるday_of_week()を持っている
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)

もし特定の型(varargsや,ポリモーフィックな関数,もしくはある関数におけるOptionや非Option型をサポートするため)を評価する,より柔軟な機能が欲しいのならば,型の決められていないインスタンスを得るためにSimpleFunction.applyを用いる事ができるし,適切な型検査をつけたラッパー関数を自身で書くことも出来る.

def dayOfWeek2(c: Column[Date]) =
  SimpleFunction("day_of_week")(TypeMapper.IntTypeMapper)(Seq(c))

SimpleBinaryOperatorSimpleLiteralはよく似た機能を持っている.さらにより柔軟な機能(普遍的でないシンタックスを持った関数に近い表現など)のために,SimpleExpressionを用いる事が出来る.

もしカスタムしたカラム型を必要とするのならば,TypeMapperTypeMapperDelegateを実装すれば良い.大抵の場面,アプリケーション特有の型をデータベースに既にサポートされた型へとマッピングする事になる.これは全ての共通事項(boilerplate)を考慮したMappedTypeMapperを用いることによってより簡単に実装する事が出来る.

// booleanの代数型
sealed trait Bool
case object True extends Bool
case object False extends Bool
...
// 上記のbooleanを1と0のIntへと変換するTypeMapper
implicit val boolTypeMapper = MappedTypeMapper.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型をテーブルやクエリなどにおいて,ビルドインされたカラム型であるかのように扱う事が出来る. さらにより柔軟な操作にはサブクラスであるMappedTypeMapperを用いる事が出来る.

Slick 1.0.0 documentation - 04 Plain SQL Queries

Permalink to Plain SQL Queries — Slick 1.0.0 documentation

Plain SQL Queries 

抽象的で高度な操作について,SQLコードを直接書きたくなる事があるかもしれない.Slickの Plain SQL クエリでは,JDBCの低レイアに触れる事無しに,よりScalaベースな記述を行う事が出来る.

Scaffolding 

SLick example jdbc/PlainSQLでは Plain SQL の特徴についていくつか説明している.インポートすべきパッケージが*lifted embedding**direct embedding*とは異なっている事に注意して欲しい.

import scala.slick.session.Database
import Database.threadLocalSession
import scala.slick.jdbc.{GetResult, StaticQuery => Q}

まず初めに, Slick driver をインポートする必要がない.SlickのJDBCに基づくAPIはJDBC自身のみに依存しているし,データベース特有の抽象化を全く実装する必要がない.データベースに接続するために必要なものは,scala.slick.session.Databaseとセッション処理を単純化したthreeadLocalSessionのみである.

Plain SQL クエリを用いるために必要なクラスは,ここではQという名前でインポートしている,scala.slick.jdbc.StaticQueryである.

データベースの接続方法は*in the usual way*にある.例を示すために,以下のようなcase classを定義した.

case class Supplier(id: Int, name: String, street: String, city: String, state: String, zip: String)
case class Coffee(name: String, supID: Int, price: Double, sales: Int, total: Int)
...
Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
}

DDL/DML Statements 

最もシンプルなStaticQueryのメソッドは,updateNAである(NA = no args).updateNAは,結果の代わりにDDLステートメントから行数を返すStaticQuery[Unit, Int]を作成する,これは[lifted embedding][3]を用いるクエリと同じように実行する事が出来る.ここでは結果を得ずに,クエリを.executeを用いて実行させている.

// 主キーと外部キーを含むテーブルを作成する
Q.updateNA("create table suppliers("+
  "id int not null primary key, "+
  "name varchar not null, "+
  "street varchar not null, "+
  "city varchar not null, "+
  "state varchar not null, "+
  "zip varchar not null)").execute
.updateNA("create table coffees("+
  "name varchar not null, "+
  "sup_id int not null, "+
  "price double not null, "+
  "sales int not null, "+
  "total int not null, "+
  "foreign key(sup_id) references suppliers(id))").execute

文字列を既存のStaticQueryオブジェクトに対し,+を用いて結合する事が出来る.この際,新しいStaticQueryが生成される.StaticQuery.uは,便利な関数であり,StaticQuery.updateNA(””)で生成される空の update クエリを生成する.SUPPLIERSテーブルにいくつかのデータを挿入するためにStaticQuery.uを用いてみる.

// 複数のsupplierを挿入する
(Q.u + "insert into suppliers values(101, 'Acme, Inc.', '99 Market Street', 'Groundsville', 'CA', '95199')").execute
(Q.u + "insert into suppliers values(49, 'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', '95460')").execute
(Q.u + "insert into suppliers values(150, 'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', '93966')").execute

SQLコード内にリテラルを埋め込む事は,一般的にセキュリティやパフォーマンスの観点から推奨されない.特に,ユーザが提供したデータを実行時に用いるような際には危険な処理になる.変数をクエリ文字列に追加するためには,特別な連結オペレータである+?を用いる.これはSQL文が実行される際に,渡された値を用いてインスタンス化するものである.

def insert(c: Coffee) = (Q.u + "insert into coffees values (" +? c.name +
  "," +? c.supID + "," +? c.price + "," +? c.sales + "," +? c.total + ")").execute
...
// Insert some coffees
Seq(
  Coffee("Colombian", 101, 7.99, , ),
  Coffee("French_Roast", 49, 8.99, , ),
  Coffee("Espresso", 150, 9.99, , ),
  Coffee("Colombian_Decaf", 101, 8.99, , ),
  Coffee("French_Roast_Decaf", 49, 9.99, , )
).foreach(insert)

SQL文は全ての呼び出しで同じもの(insert into coffees values (?,?,?,?,?))となっている.

Query Statements 

updateNAと似た,返り値となる行の型パラメータを取るqueryNAというメソッドがある.このメソッドは select文 を実行し,結果をiteratorで回す事が出来る.

Q.queryNA[Coffee]("select * from coffees") foreach { c =>
  println("  " + c.name + "t" + c.supID + "t" + c.price + "t" + c.sales + "t" + c.total)
}

これらを上手く機能させるためには,SlickはPositionedResultオブジェクトからCoffeeの値をどのようにして読み取ればいいのかを知らせなくてはならない.これは暗黙的なGetResultによって行われる.GetResultを持つ基本的なJDBCの型や,NULLを許可するカラムを表すためのOptionや,タプルに対して,暗黙的なGetResultが定義されていなくてはならない.この例においてはSupplierクラスやCoffeeクラスのためのGetResultを以下のように用意する必要がある.

// Result set getters
implicit val getSupplierResult = GetResult(r => Supplier(r.nextInt, r.nextString, r.nextString,
      r.nextString, r.nextString, r.nextString))
implicit val getCoffeeResult = GetResult(r => Coffee(r.<<, r.<<, r.<<, r.<<, r.<<))

GetResult[T]はPositionedResult => Tとなる関数のシンプルなラッパーである.上の例において,1つ目のGetResultでは現在の行から次のInt,次のStringといった値を読み込むgetInt,getStringといったPositionedResultの明示的なメソッドを用いている.2つ目のGetResultでは自動的に型を推測する簡易化されたメソッド<<を用いている.コンスタクタの呼び出しにおいて実際に型を判別出来る際にのみこれは用いる事ができる.

パラメータの無いクエリのための,queryNAメソッドは2つの型パラメータ(1つはクエリパラメータ,もう1つは返り値となる行の型パラメータ)を取るクエリによって補完される.同様に,updateNAのための適切なupdateが存在する.StaticQueryの実行関数は型パラメータを用いて呼ばれる必要がある.以下の例では.listがそれにあたる.

// 価格が$9.00より小さい全てのコーヒーに対し,coffeeのnameとsupplierのnameを取り出す
val q2 = Q.query[Double, (String, String)]("""
  select c.name, s.name
  from coffees c, suppliers s
  where c.price < ? and s.id = c.sup_id
""")
// この場合,結果はListとして読むことが出来る
val l2 = q2.list(9.0)
for (t <- l2) println("  " + t._1 + " supplied by " + t._2)

また,パラメータを直接的にクエリへ適用させる事も出来る.これを用いると,パラメータの無いクエリへと変換させることが出来る.これは通常の関数適用と同じように,クエリのパラメータを決めさせる事が出来る.

val supplierById = Q[Int, Supplier] + "select * from suppliers where id = ?"
println("Supplier #49: " + supplierById(49).first)

String Interpolation 

文字列補完接頭辞(string interpolation prefix)であるsqlやsqluを用いるためには,以下のインポート文を追加する必要がある.

import Q.interpolation

再利用可能なクエリを必要としない場合には,interpolationはパラメータが付与されたクエリを生成する,最も簡単で統語的にナイスな手法である.クエリを挿入するどんな変数や式も,バインドした変数を結果を返すクエリ文字列へと変換する事が出来る(クエリへ直接挿入されるリテラル値を取得するのに$の代わりに#$を用いることも出来る).返り値の型は呼び出しの中で,sql interpolatorによって作られたオブジェクトをStaticQueryへと変換させる.asによって指定される.

def coffeeByName(name: String) = sql"select * from coffees where name = $name".as[Coffee]
println("Coffee Colombian: " + coffeeByName("Colombian").firstOption)

update文を生成するためのよく似た補完(interpolator),sqluというものもある.これはInt値を返す事を強制するため,.asのような関数を必要としない.

def deleteCoffee(name: String) = sqlu"delete from coffees where name = $name".first
val rows = deleteCoffee("Colombian")
println(s"Deleted $rows rows")

Slick 1.0.0 documentation - 05 Direct Embedding

Permalink to Direct Embedding — Slick 1.0.0 documentation

Direct Embedding 

direct embeddingは新しい,しかしまだ不完全で実験的なクエリAPIである.現在実験中.開発中の段階であるため,リリースに応じて非推奨な期間など無しに変更される事がある.安全に利用する事の出来る,安定したlifted embeddingクエリAPIに取って代わるような予定は無く,direct embeddingは共存させていく.lifted embeddingと違って,direct enbeddingは実装のための暗黙的な変換やオーバーロードするオペレータの代わりにマクロを用いて操作を行う.ユーザのために,コード内における違いは少なくしているが,direct enbeddingを用いるクエリは普遍的なScalaの型を用いて機能している.これは表示されるエラーメッセージの理解性を上げるためでもある.

以下の説明は*lifted embedding*の説明に類似した例である.

Dependencies 

direct embeddingは型検査のために実行時にScalaコンパイラにアクセスする必要がある.Slickは必要性に駆られない限り,アプリケーションに対し,依存性を避けるためにScalaコンパイラへの依存性を任意としている.そのため,direct embeddingを用いる際にはプロジェクトの build.sbt に対し明示的にその依存性を記述しなくてはならない.

libraryDependencies <+= (scalaVersion)("org.scala-lang" % "scala-compiler" % _)

Imports 

import scala.slick.driver.H2Driver
import H2Driver.simple.Database
import Database.{threadLocalSession => session}
import scala.slick.direct._
import scala.slick.direct.AnnotationMapper._

Row class and schema 

スキーマは現在でえは行を保持しているケースクラスに対してアノテーションを付与する事で記述する事が出来る.今後,より柔軟にスキーマの情報を拡張出来るような機能を提供する予定だ.

// describe schema for direct embedding
@table(name="COFFEES")
case class Coffee(
  @column(name="NAME")
  name : String,
  @column(name="PRICE")
  price : Double
)

Query 

Queryableはテーブルデータに対しクエリの演算を行うためのものであり,注釈付けられた型引数を取る.

_.priceはここではInt型である.潜在的な,マクロベースの実装においてはmapやfilterに与えられた引数はJVM上で実行されないが,その代わりにデータベースクエリへと変換される事を覚えておいて欲しい.

// query database using direct embedding
val q1 = Queryable[Coffee]
val q2 = q1.filter( _.price > 3.0 ).map( _ .name )

Execution 

クエリを実行するためには,選択したデータベースのドライバーを用いるSlickBackendインスタンスを作成する必要がある.

val db = Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver")
db withSession {
    // execute query using a chosen db backend
    val backend = new SlickBackend( H2Driver, AnnotationMapper )
    println( backend.result( q2, session ) )
    println( backend.result( q2.length, session ) )
}

Alternative direct embedding bound to a driver and session 

ImplicitQueryableを用いると,queryableはバックエンドとセッションに束縛される.クエリはその上で以下のような方法で簡単に実行する事が出来る.

//
val iq1 = ImplicitQueryable( q1, backend, session )
val iq2 = iq1.filter( c => c.price > 3.0 )
println( iq2.toSeq ) //  <- triggers execution 
println( iq2.length ) // <- triggers execution

Features 

direct embeddingは現在, String, Int, Double といった値にたいしマッピングされるデータベースカラムのみサポートしている.

QueryableとImplicitQueryableは現在,次のようなメソッドを用意している.

map, flatMap, filter, length

これらのメソッドはimmutableな演算を行うが,関数呼び出しによる変化を包含した新しいQuaryableを返す.

上記の関数におけるシンタックスとして,以下の様なオペレータを利用する事が出来る.

Any: ==

Int, Double: + < >

String: +

Boolean: || &&

他に定義された独自のオペレータについても,型検査がマッチしていれば利用する事が出来る.しかし現時点では,それらのオペレータは実行時に失敗するクエリを生成するようなSQLへ変換する事が出来ない.(例: (coffees.map( c => c.ma,e.repr )))将来的には,コンパイル中にそのようなものもキャッチするような方法を検討している.

クエリは行を補完するようなオブジェクトを保持する,任意にネストされたタプルのシーケンスを結果として返す.

q1.map( c => (c.name, (c, c.price)) )

direct embeddingは現在データの挿入といった機能を持っていない.その代わりに*lifted embedding**plain SQL queries*などを用いる事ができる.

Slick 1.0.0 documentation - 06 Slick TestKit

“Permalink to Slick TestKit — Slick 1.0.0 documentation

Slick TestKit 

Slickに対し,独自のデータベースドライバーを記述する際には,きちんと動作するのか,何が現時点で実装されていないのかなどを確認するために基本となるユニットテスト(もしくは加えて他の独自のカスタマイズしたテスト)を実行して欲しい.このためにSlickユニットテストとしてのSlick Test Kitプロジェクトを別に用意している.

これを用いるためには,Slickの基本的なPostgreSQLドライバーと,それをビルドするために必要なものを全て含んだSlick TestKit Exampleをクローンして,それをテストして欲しい.

Scaffolding 

build.sbtは以下のように記述する.一般的な名前とバージョン設定と区別して,SlickとTestKit,junit-interface,Logback,PostgreSQL JDBC Driverへの依存性を追加する.そしてテストを行うためのオプションをいくつか記述する必要がある.

libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "1.0.0",
  "com.typesafe.slick" %% "slick-testkit" % "1.0.0" % "test",
  "com.novocode" % "junit-interface" % "0.10-M1" % "test",
  "ch.qos.logback" % "logback-classic" % "0.9.28" % "test",
  "postgresql" % "postgresql" % "9.1-901.jdbc4" % "test"
)
...
testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v", "-s", "-a")
...
parallelExecution in Test := false
...
logBuffered := false

src/test/resources/logback-test.xmlに,Slickのlogbackについての設定のコピーがある.しかし,もし異なるものを用いたい場合には,loggingフレームワークを取り替える事が出来る.

Driver 

実際のドライバーの実装はsrc/main/scalaの中にある.

Test Harness 

TestKitテストを実行するためには,DriberTestを継承したクラスを作成する必要がある.加えて,TestKitに対してどのようにtestデータベースへ接続するのか,テーブルのリストをどのように取得するのか,テスト間におけるクリーンをどのようにして行うのかなどといった事を伝えるTestDBの実装が必要になる.

PostgreSQLのテーストハーネス(src/test/scala/scala/slick/driver/test/MyPostgreTestの中にある)の場合は,大抵のデフォルトとなる実装はボックスの外で利用される.

@RunWith(classOf[Testkit])
class MyPostgresTest extends DriverTest(MyPostgresTest.tdb)
...
object MyPostgresTest {
  def tdb(cname: String) = new ExternalTestDB("mypostgres", MyPostgresDriver) {
    override def getLocalTables(implicit session: Session) = {
      val tables = ResultSetInvoker[(String,String,String, String)](_.conn.getMetaData().getTables("", "public", null, null))
      tables.list.filter(_._4.toUpperCase == "TABLE").map(_._3).sorted
    }
    override def getLocalSequences(implicit session: Session) = {
      val tables = ResultSetInvoker[(String,String,String, String)](_.conn.getMetaData().getTables("", "public", null, null))
      tables.list.filter(_._4.toUpperCase == "SEQUENCE").map(_._3).sorted
    }
    override lazy val capabilities = driver.capabilities + TestDB.plainSql
  }
}

Database Configuration 

PostgreSQLのテストハーネスは ExternalTestDB に基づいている一方, test-dbs/databases.properties において設定が行われてなくてはならない.

PostgreSQL quick setup:

mypostgres.enabled = false
mypostgres.url = jdbc:postgresql:[DB]
mypostgres.user = postgres
mypostgres.password = secret
mypostgres.adminDB = postgres
mypostgres.testDB = slick-test
mypostgres.create = CREATE TABLESPACE slick_test LOCATION '[DBPATH]'; CREATE DATABASE "[DB]" TEMPLATE = template0 TABLESPACE slick_test
mypostgres.drop = DROP DATABASE IF EXISTS "[DB]"; DROP TABLESPACE IF EXISTS slick_test
mypostgres.driver = org.postgresql.Driver

Testing 

sbt test を実行すると, MyPostgresTest を探索し,TestKitのJUnit runnerを用いて実行される.これはテストハーネスを通してセットアップされたデータベースを用いており,ドライバーを用いて適応可能な全てのテストが実行される事になる.

Slick 1.0.0 documentation - 07 Slick Extensions

Permalink to Slick Extensions — Slick 1.0.0 documentation

Slick Extensions 

OracleのためのSlickドライバー( com.typesafe.slick.driver.oracle.OracleDriver )とDB2( com.typesafe.slick.driver.db2.DB2Driver )は, Typesafe社によって商用サポートされたパッケージである,slick-extensions において利用する事が出来る.Typesafe Subscription Agreement (PDF)の諸条件の元,利用可能である.

もしsbtを用いているのならば, Typesafeのリポジトリを用いるために次のように記述すれば良い.

// Use the right Slick version here:
libraryDependencies += "com.typesafe.slick" %% "slick-extensions" % "1.0.0"
resolvers += "Typesafe Releases" at "http://repo.typesafe.com/typesafe/maven-releases/"
Fork me on GitHub