slick-doc-ja 2.0 

Slick 2.0 documentationの日本語訳です。

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

Slick 2.0.0 documentation - 01 導入(Introduction) Permalink to Introduction — Slick 2.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を通してSQLを発行すると、コンパイル時により良いクエリを型安全に提供する事が出来る。Slickは独自のクエリコンパイラを用いてDBに対するクエリを発行する。

すぐにSlickを試したいのなら、Typesafe ActivatorにあるHello Slickテンプレートを使うと良い。

Slickの特徴 

Scala 

class Coffees(tag: Tag) extends Table[(String, Double)](tag, "COFFEES") {
  def name = column[String]("COF_NAME", O.PrimaryKey)
  def price = column[Double]("PRICE")
  def * = (name, price)
}
val coffees = TableQuery[Coffees]
// name というカラムを返すクエリ
coffees.map(_.name)
// 価格が 10.0 未満という条件を用いたクエリ
coffees.filter(_.price < 10.0)

Type Safe 

// `select PRICE from COFFEES` の結果はSeq[Double]になる
// これは型安全な処理が行なわれるためである
val coffeeNames: Seq[Double] = coffees.map(_.price).list
// クエリを作るのも型安全に行なわれる
coffees.filter(_.price < 10.0)
// もし条件の中で異なる型が比較されていたのなら、コンパイルエラーになる

Composable 

// 10.0 未満の価格で、名前順にソートしたコーヒーの名前を取り出すクエリを作る
coffees.filter(_.price < 10.0).sortBy(_.name).map(_.name)
// ここで作られるSQLは次のものと等価になる
// select name from COFFEES where PRICE < 10.0 order by NAME

Compatibility 

SlickはScalaのバージョン2.10が必要になる。(もし2.9以下で使いたいならScalaQueryを使うと良い)

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

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

License 

Slick is released under a BSD-Style free and open source software license. See the chapter on the commercial Slick Extensions add-on package for details on licensing the Slick drivers for the big commercial database systems.

Query APIs 

Lifted Embedding は型安全なクエリや更新が行えるSlickの基本的なAPIである。Getting Startedでは Lifted Embedding を用いた例を紹介する。

SQL文を直接発行したい場合には、Plain SQL API を利用することが出来る。

Direct Embeddingはまだ実験的なものではあるが、Lifted Embeddingに替わりとして利用出来る。

Lifted Embedding 

Lifted Embeddingという名前は基本的なScalaの型を用いるのではなく、 Rep型へと持ち上げ(lifted)されたものを用いるという事に基づいている。これはScalaのコレクションを操作する例と比べると明らかだろう。

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

… Lifted Embeddingを用いる際には次のように書ける

class Coffees(tag: Tag) extends Table[(String, Double)](tag, "COFFEES") {
  def name = column[String]("COF_NAME")
  def price = column[Double]("PRICE")
  def * = (name, price)
}
val coffees = TableQuery[Coffees]
...
val q = coffees.filter(_.price > 8.0).map(_.name)
//                       ^       ^          ^
//               Rep[Double]  Rep[Double]  Rep[String]

全ての型はRepへと持ち上げられる。カラムの型であるCoffeesも同様にRep[(String, Double)]へと持ち上げられる。数値リテラルである8.0も自動的にRep[Double]へと持ち上げられる。これは条件式>の左辺がRep[Double]であることから、右辺には暗黙的な変換が行われるためである。生のScalaの型や値を用いることは、SQLへの変換を行う上で充分な情報を提供しない。これらの変換はそのために行なわれるのである。

Slick 2.0.0 documentation - 02 始めよう(Getting Started)

Permalink to Getting Started — Slick 2.0.0 documentation

始めよう 

軽くSlickを試すのなら、Typesafe Activatorを使うのが良い。Slickの基本を学びたいのなら、Hello Slickテンプレートを使うと良い。Slickを使ったPlay Frameworkアプリケーションを使いたいのなら、Play Slick with Typesafe IDsテンプレートを試すと良いだろう。

Slickを既存のプロジェクトに導入するには各プロジェクトに応じた依存関係を記述する。 sbtプロジェクトにはlibraryDependenciesに次のように書き加える。

"com.typesafe.slick" %% "slick" % "2.0.0",
"org.slf4j" % "slf4j-nop" % "1.6.4"

Mavenプロジェクトには以下の様な依存を書き加える。

<dependency>
  <groupId>com.typesafe.slick</groupId>
  <artifactId>slick_2.10</artifactId>
  <version>2.0.0</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-nop</artifactId>
  <version>1.6.4</version>
</dependency>

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

Slick Examples 

Slick Examplesでは、複数のデータベースを使ったり、生のクエリを発行したりといったサンプルを公開している。

Quick Introduction 

Slickを使う際、まず初めに、利用するデータベースに応じたAPIを以下のようにインポートする必要がある。

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

H2 Databaseを用いているため、Slickの H2Driver をimportする必要がある。このdriverに含まれる simple オブジェクトにはsession handlingといったSlickに必要な共通の機能が含まれている。

Databaseへの接続 

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

Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
  implicit session =>
  // <- クエリはここへ書こう
}

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

スキーマ 

ここでは lifted embeddingを用いたアプリケーションを書いてみる。初めに、データベースのテーブル毎にTable型を継承させたクラスと、TableQuery型の値を定義する。これらは、code generatorを使うとデータベーススキーマから自動的に作成することができるし、直接手で書いても良い。

// SUPPLIERSテーブルの定義
class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "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)
}
val suppliers = TableQuery[Suppliers]
...
class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "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)
  // 全てのテーブルではテーブルの型パラメタと同じタイプの射影*を定義する必要がある。?
  // A reified foreign key relation that can be navigated to create a join
  def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
}
val coffees = TableQuery[Coffees]

全ての列は名前(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, , )
)

TableQueryddl 関数は、テーブルやその他データベースのエンティティを作成したり削除したりするための、データベース特有のコードを用いて 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 2.0.0 documentation - 03 v2.0 移行ガイド

Permalink to Migration Guide from Slick 1.0 to 2.0 — Slick 2.0.0 documentation

Slick v2.0 Migration Guide 

Slick2.0はSlick1.0に互換性のない拡張が含まれている。アプリケーションを1.0から2.0へ移行する際には、以下のような変更が必要になるだろう。

Code generation 

以前は手で書いていたテーブルへのマッピングを、2.0ではデータベーススキーマを用いて自動的に生成出来るようになった。code-generaterは柔軟にカスタマイズすることも出来るため、より最適化されたものに変更する事も出来る。詳細については、code-generationを参考にして欲しい。

Table descriptions 

Slick1.0では、テーブルはvaltable objectと呼ばれるobjectによって定義がなされ、射影*では~オペレータを用いてタプルを表していた。

// --------------------- Slick 1.0 code -- v2.0では動かない ---------------------
object Suppliers extends Table[(Int, String, String)]("SUPPLIERS") {
  def id = column[Int]("SUP_ID", O.PrimaryKey)
  def name = column[String]("SUP_NAME")
  def street = column[String]("STREET")
  def * = id ~ name ~ street
}

Slick2.0ではTagを引数にテーブルクラスの定義を行い、実際のデータベーステーブルを表すTableQueryのインスタンスを定義する。射影*に対し、基本的なタプルを用いて定義を行うことも出来る。

class Suppliers(tag: Tag) extends Table[(Int, String, String)](tag, "SUPPLIERS") {
  def id = column[Int]("SUP_ID", O.PrimaryKey)
  def name = column[String]("SUP_NAME")
  def street = column[String]("STREET")
  def * = (id, name, street)
}
val suppliers = TableQuery[Suppliers]

以前に用いていた~シンタックスをそのまま使いたい場合には、TupleMethod._をインポートすれば良い。TableQuery[T]を用いると、内部的にはnew TableQuery(new T(_))のような処理が行われ、適切なTableQueryインスタンスが作成される。Slick1.0では共通処理に関して、静的メソッドでテーブルオブジェクトに定義がなされていた。2.0においても以下のようにカスタムされたTableQueryオブジェクトを用いて、同様の事が出来る。

object suppliers extends TableQuery(new Suppliers(_)) {
  // put extra methods here
  val findByID = this.findBy(_.id)
}

TableQueryはデータベーステーブルのためのQueryオブジェクトのことである。予期せぬ場所で適用されるQueryへの暗黙的な変換はもはや必要無い。Slick 1.0において生身の table object を扱っていた場所は、全て table query が代わりに用いられることになる。例として、以下に挙げられる挿入(inserting)や、外部キー関連などがある。

Profile Hierarchy 

Slick 1.0ではBasicProfileExtendedProfileの2つのプロファイルを提供していた。Slick 2.0ではこれら2つのプロファイルをJdbcProfileとして統合している。今ではRelationalProfileに挙げられるようなより抽象的なプロファイルを提供している。RelationalProfileJdbcProfileの全ての特徴を持っているわけではないが、新しく出来たHeapDriverDistributedDriberといった機能を支えている。Slick 1.0からコードを移植する際、JdbcProfileへとプロファイルを変更して欲しい。特にSlick 2.0におけるBasicProfileは1.0におけるBasicProfilと非常に異なったものになっているので注意して欲しい。

Inserting 

Slick1.0では挿入時にtable objectの一部を射影していた。

// --------------------- Slick 1.0 code -- does not compile in 2.0 ---------------------
(Suppliers.name ~ Suppliers.street) insert ("foo", "bar")
  1. 0において生身のtable objectは存在していないため、table queryから射影しなくてはならない。
suppliers.map(s => (s.name, s.street)) += ("foo", "bar")

+=オペレータはScalaコレクションとの互換性のために用いられており、insertという古い名前の関数はエイリアスとして依然用いる事が出来る。

Slick 2.0ではデータを挿入する際自動的にデフォルトでAutoIncのついたカラムを除外する。1.0では、そのようなカラムについて手動で除外した射影関数を別に用意しなくてはならなかった。

// --------------------- Slick 1.0 code -- does not compile in 2.0 ---------------------
case class Supplier(id: Int, name: String, street: String)
...
object Suppliers extends Table[Supplier]("SUPPLIERS") {
  def id = column[Int]("SUP_ID", O.PrimaryKey, O.AutoInc)
  def name = column[String]("SUP_NAME")
  def street = column[String]("STREET")
  // Map a Supplier case class:
  def * = id ~ name ~ street <> (Supplier.tupled, Supplier.unapply)
  // Special mapping without the 'id' field:
  def forInsert = name ~ street <> (
    { case (name, street) => Supplier(-1, name, street) },
    { sup => (sup.name, sup.street) }
  )
}
...
Suppliers.forInsert.insert(mySupplier)
  1. 0においてこのような冗長な記述は必要無くなる。デフォルトの射影関数を挿入時に用いる事で、自動インクリメントのついたidというカラムをSlickが除外してくれる。

逆にAutoIncのついたカラムに対し値を挿入したいのならば、新しく出来たforceInsertforceInsertAllといった関数を用いれば良い。

  1. 0において双方向マッピングを行っていた<>関数はオーバーロードされ、今やケースクラスのapply関数を直接渡す事が出来る。
// --------------------- Slick 1.0 code -- does not compile in 2.0 ---------------------
def * = id ~ name ~ street <> (Supplier _, Supplier.unapply)

上記のような記述はもはや2.0ではサポートされていない。その理由の1つとして、このようなオーバーロードはエラーメッセージを複雑にしすぎるためである。現在では適切なタプル型を用いて関数を定義する事が出来る。もしケースクラスをマッピングしたいのならば、コンパニオンオブジェクトの.tupledを単純に用いれば良いのである。

def * = (id, name, street) <> (Supplier.tupled, Supplier.unapply)

Pre-compiled updates 

Slickはselect文において用いられるのと同じ方法で、update文における事前コンパイルもサポートしている。これについては、Compliled-Queriesのセクションを見て欲しい。

Database and Session Handling 

Slick 1.0ではDatabaseのファクトリオブジェクトとして標準的なJDBCベースなDatabaseSessionといった型がscala.slick.sessionパッケージにある。Slick 2.0からはJDBCベースなデータベースに制限せず、このパッケージは(backendとしても知られる)DatabaseComponent階層 によって置き換えられている。もしJdbcProfile抽象レベルで動かしたいのならば、以前にscala.slick.sessionにあったものをインポートし、常にJdbcBackendを用いれば良い。ただし、simple._といったインポートを行うと自動的にスコープ内にこれらの型が持ち込まれてしまうので注意して欲しい。

Dynamically and Statically Scoped Sessions 

Slick 2.0では依然としてスレッドローカルな動的セッションと静的スコープセッションを提供している。しかしシンタックスが変わっており、静的スコープセッションを用いる際にはより簡潔な記述が推奨される。以前のthreadLocalSessiondynamicSessionという名前に変わっており、関連するwithSessionwithTransactionといった関数もwithDynSessionwithDynTransactionという名前にそれぞれ変わっている。Slick 1.0で記述されていた以下のようなシンタックスは、

// --------------------- Slick 1.0 code -- does not compile in 2.0 ---------------------
import scala.slick.session.Database.threadLocalSession
...
myDB withSession {
  // use the implicit threadLocalSession here
}

Slick 2.0で以下のようなシンタックスへ変わる。

import scala.slick.jdbc.JdbcBackend.Database.dynamicSession
...
myDB withDynSession {
  // use the implicit dynamicSession here
}

一方で、Slick 1.0で必要になっていた静的スコープセッションにおける明示的な型宣言は

myDB withSession { implicit session: Session =>
  // use the implicit session here
}
  1. 0においてもはや必要なくなる。
myDB withSession { implicit session =>
  // use the implicit session here
}

また、動的セッションを使うことは確かな情報を取得できるか分からない事から推奨されていない。静的セッションを用いる方がより安全である。

Mapped Column Types 

Slick 1.0のMappedTypeMapperMappedColumnTypeへと名前が変わった。`MappedColumnType.base`を用いるような基本的な操作はRelationalProfileレベル(高度な利用法をするのならば依然としてJdbcProfileが必要)において現在も利用できる。

// --------------------- Slick 1.0 code -- does not compile in 2.0 ---------------------
case class MyID(value: Int)
...
implicit val myIDTypeMapper =
  MappedTypeMapper.base[MyID, Int](_.value, new MyID(_))

この記述は、次のように変わる。

case class MyID(value: Int)
...
implicit val myIDColumnType =
  MappedColumnType.base[MyID, Int](_.value, new MyID(_))

もしこの例のように単純なラッパー型へマッピングするのなら、MappedToを用いてもっと簡単に書くことが出来る。

case class MyID(value: Int) extends MappedTo[Int]
// No extra implicit required any more

Slick 2.0.0 documentation - 04 Connection/Transactions

Permalink to Connections/Transactions - Slick 2.0.0 documentation

Connections / Transactions 

クエリはプログラムのどこにでも書くことが出来る。クエリを実行する際には、データベースコネクションが必要になる。

Database connection 

用いるJDBCデータベースに対してどのように接続するのかを、それらの情報をカプセル化したDatabaseオブジェクトを作成することで、Slickへ伝える事が出来る。Databaseオブジェクトを作成するにはscala.slick.jdbc.JdbcBackend.Databaseにいくつかのファクトリメソッドが用意されており、どのような方法で接続するかによって使い分ける事が出来る。

Using a JDBC URL 

JDBC URLを用いて接続を行う際には、`forURL`を用いる事が出来る。(正しいURLを記述する際には、データベースのJDBCドライバー用ドキュメントを参照して欲しい)

val db = Database.forURL("jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1", driver="org.h2.Driver")

ここでは例として、新しく空のデータベースへと接続をしてみる。用いるのはインメモリ型のH2データベースであり、データベース名がtest1、そしてJVMが終了するまで残り続けるような(DB_CLOSE_DELAY=-1はH2データベース特有のオプション)データベースとなっている。

Using a DataSource 

DataSourceオブジェクトを既に持っているのなら、`forDataSource`を用いてDatabaseオブジェクトを作成出来る。もしアプリケーションフレームワークのコネクションプールからDataSourceオブジェクトを取得出来るのなら、Slickのプールへと繋いで欲しい。

val db = Database.forDataSource(dataSource: javax.sql.DataSource)

後でセッションを作成する時には、コネクションはプールから取得出来るし、セッションが閉じた時に、コネクションはプールへ返却される。

Using a JNDI Name 

もしJNDIを用いているのなら、DataSourceオブジェクトが見つかるJNDIの名前をforNameに渡してあげたら良い。

Session handling 

Databaseオブジェクトを持っているのなら、SessionオブジェクトにSlickがカプセル化したデータベースコネクションを開く事が出来る。

Automatically closing Session scope 

DatabaseオブジェクトのwithSession)関数は、関数を引数に、実行後に接続の閉じるSessionを作る。もしコネクションプールを用いたのならば、Sessionを閉じるとコネクションはプールへと返却される。

val query = for (c <- coffees) yield c.name
val result = db.withSession {
  session =>
    query.list()( session )
}

withSessionのスコープの外側で定義されたクエリが使われている事を上の例から確認出来る。データベースに対してクエリを実行する関数はSessionを必要とする。先ほどの例ではlist関数を用いてクエリを実行し、Listとして結果を取得している。(クエリを実行する関数は暗黙的な変換を通して作られる)

ただし、デフォルトの設定ではデータベースセッションはauto-commitモードになっている。insertinsertAllのようなデータベースへの呼び出しは原子的(必ず成功するか失敗するかのいずれかが保証されている)に実行される。いくつかの状態を包括するにはTransactionsを用いる。

注意: もしSessionオブジェクトがwithSessionのスコープ以外で用いられていたのなら、その接続は既に閉じられており、妥当な利用法にはなっていない。利用を避けるべきではあるば、このような状態を避ける方法がいくつかあり、例としてクロージャを用いる(withSessionスコープ内にてFutureを用いるなど)、varへセッションを割り当てる、withSessionスコープの返り値としてセッションを返却するといった方法がある。

Implicit Session 

Sessionを暗黙的なものとしてマークすると、データベースに対する呼び出しを行う関数に対して明示的にSessionを渡す必要がなくなる。

val query = for (c <- coffees) yield c.name
val result = db.withSession {
  implicit session =>
    query.list // <- takes session implicitly
}
// query.list // <- would not compile, no implicit value of type Session

これはオプショナルな使い方ではあるが、用いるとよりコードを綺麗にする事が出来る。

Transactions 

SessionオブジェクトのwithTransaction関数をトランザクションを作成するために使う事が出来る。そのブロックにおいて、1つのトランザクション処理が実行されることになる。もし例外が発生したのなら、Slickはトランザクションをブロックの終了箇所までロールバックさせる。ブロック内のどこからでもrollback関数を呼び出すことでブロックの末尾までロールバックを強制して起こさせる事も出来る。注意して欲しいのは、Slickはデータベースのオペレーションとしてのロールバックを行うのであり、他のScalaコードの影響を引き起こさない。

session.withTransaction {
  // your queries go here
  if (/* some failure */ false){
    session.rollback // signals Slick to rollback later
  }
} // <- rollback happens here, if an exception was thrown or session.rollback was called

もしSessionオブジェクトをまだ持っていないのなら、DatabaseオブジェクトのwithTransaciton関数を直接呼ぶ事が出来る。

db.withTransaction{
  implicit session =>
    // your queries go here
}

Manual Session handling 

この方法は推奨されない。もししなければならない場面があるのなら、Sessionを手動で取り扱うことも出来る。

val query = for (c <- coffees) yield c.name
val session : Session = db.createSession
val result  = query.list()( session )
session.close

Passing sessions around 

Slickのクエリに対し、再利用可能な関数を書くことが出来る。これらの関数はSessionを必要としないものであり、クエリのフラグメントやアセンブリ化されたクエリを生成する。もしこれらの関数内でクエリを実行したいのなら、Sessionが必要になる。その際は、関数のシグネチャにおいて(出来れば暗黙的なものとして)引数にあたえてあげるか、もしくはいくつかの同様の関数を包括して、共通化したコードを削除するためにセッションを保持したクラスにする。

class Helpers(implicit session: Session){
  def execute[T](query: Query[T,_]) = query.list
  // ... place further helpers methods here
}
val query = for (c <- coffees) yield c.name
db.withSession {
  implicit session =>
  val helpers = (new Helpers)
  import helpers._
  execute(query)
}
// (new Helpers).execute(query) // <- Would not compile here (no implicit session)

Dynamically scoped sessions 

セッションは長い間開きっぱなしにはしたくないが、必要な時にはすぐに開いたり閉じたりしたいと考えるだろう。上記の例では、クエリを実行するために必要な時に暗黙的なセッション引数を用いてセッションスコープトランザクションスコープを使っていた。

別の方法として、共通化したコードの部分的なものを保存する、ということが、ファイルの先頭に追加に次の行を追加する事で行える。これにより、セッション引数無しのセッションスコープやトランザクションスコープを利用する事が出来る。

import Database.dynamicSession // <- implicit def dynamicSession : Session

現在のコールスタック内のどこかでwithDynSessionwithDynTransactionスコープが開かれていた場合において、dynamicSessionは適切なSessionを返却する暗黙的な関数となる。

db.withDynSession {
  // your queries go here
}

注意して欲しいのは、もしdynamicSessionがインポートさあれ、withDynSessionwithDynTransactionスコープの外側でクエリが実行されようとしているのならば、実行時例外を吐いてしまう事である。つまり、静的な安全性を犠牲にしているのである。dynamicSessionは内部的にDynamicVariableを用いる。これは動的にスコープのある変数を作成し、JavaのInheritableThreadLocalを順々に用いるものである。静的であることの安全性とスレッドの安全性に配慮して欲しい。

Connection Pools 

Slickは独自のコネクションプール実装を持っていない。JEEやSpringのようなある種のコンテナにおけるアプリケーションを動かす際、一般的にコンテナに提供されたコネクションプールを用いる事になるだろう。スタンドアローンなアプリケションにおいてはDBCPc3p0BoneCPのような外部のコネクションプールの実装を用いる事が出来る。

ちなみに、Slickはどこでも利用可能なプリペアドステートメントを持ってはいるが、独自でキャッシュをしたりはしない。よって、コネクションプールの設定において、プレペア度ステートメントのキャッシュを有効にすべきであるし、充分に大きなプールサイズを用意すべきだ。

Slick 2.0.0 documentation - 05 Schema code generation

Permalink to Schema Code Generation — Slick 2.0.0 documentation

Schema code generation 

Slickコードジェネレータは既存のデータベーススキーマをそのまま動かす上で便利なツールとなっている。スタンドアローン形式で動かしたり、sbtのbuildに対し統合したり出来る。

Overview 

デフォルトでコードジェネレータは、TableQueryの値に対応するTableクラスを生成する。これらの値は、個々は行の値を包括するケースクラスとなり、全体としてコレクション操作関数が呼び出せるようなものになっている。もしScalaのタプルの限界数である22個より多いカラムが存在していたのなら、自動的にSlickの実験的な実装であるHListを用いた実装を出力する。(ちなみに、25カラムより多い場合には非常にコンパイルに時間がかかる事が分かっており、可能な限り早く修正する予定だ)

実装は実用的なものになってはいるが、コードジェネレータはSlick 2.0における新しい機能となっており、依然として実験的なものも含んでいる。必要なものを摘出し、必要のない機能を取り除いていく予定だ。将来的なバージョンにおけるコードジェネレータに対する修正は小さくする予定だ。もし必要ならば、Slickの他の部分から独立した実装にしても良い。我々はこの機能を用いた人々の挑戦に対する声に非常に関心がある。

ジェネレータについて、talk at Scala eXchange2013で軽く説明も行っている。

Run from the command line or Java/Scala 

Slickのコードジェネレータは以下のようにして手軽に動かすことが出来る。

scala.slick.model.codegen.SourceCodeGenerator.main(
  Array(slickDriver, jdbcDriver, url, outputFolder, pkg)
)

必要な引数は以下の通りである

コードジェネレータは指定されたパッケージ名に一致するサブフォルダを、指定された出力先フォルダの中に作成し、そこの“Tables.scala“というファイルへ結果を出力する。そのファイルには“Tables“オブジェクトが生成される。引数に与えたSlickドライバーと同じものが用いられているかを確認して欲しい。このファイルには同様に“Tables“トレイトがふくまれ、これはCakeパターンに用いられたものになっている。

Integrated into sbt 

コードジェネレータをコンパイル毎に事前に実行することも出来るし、手動で実行することも出来る。実際に使ってみた例がこちらにあるので見て欲しい。

Customization 

コードジェネレータはモデルデータに基づきコードを自動生成する関数をオーバーライドする事で、柔軟にカスタマイズ出来る。小さなカスタマイズであっても大きなカスタマイズであっても、このようなモデルドリブンなコードジェネレーションが同じように扱われる。例えば、とあるフレームワークにおけるバインディングや、その他のデータに関連するアプリケーションの繰り返しセクションにおいて用いられる。

この例ではカスタマイズされたコードジェネレータを用いており、メインリソースをコンパイルする前にコードジェネレータを走らせるマルチプロジェクトのsbtビルドに対しどのように設定を行うのかを示している。

コードジェネレータの実装は構造化されており、いくつかの階層化されたサブジェネレータに責務を委譲している。つまり完全なる出力を出す際に、部分化した結果を各ジェネレータにおいて出力している。各サブジェネレータの実装は、対応するファクトリメソッドをオーバーライドすることで、カスタマイズしたものへ変更する事が出来る。SourceCodeGeneratorはファクトリメソッドであるTableを持っており、これは各テーブルのためのサブジェネレータを生成するために用いられるものである。サブジェネレータであるTableは、Tableクラス、エンティティケースクラス、カラム、キー、インデックス、といった情報のための、別個複数のサブジェネレータを持っている。

Slickに部分的に関連するサブジェネレータにおいて、データモデルはコード生成のために用いられる。

カスタマイズする際にオーバーライドする関数については、APIドキュメントを是非見てもらいたい。

コードジェネレータをカスタマイズする例として、以下のようなものがある。

import scala.slick.jdbc.meta.createModel
import scala.slick.model.codegen.SourceCodeGenerator
// fetch data model
val model = db.withSession{ implicit session =>
  createModel(H2Driver.getTables.list,H2Driver) // you can filter specific tables here
}
// customize code generator
val codegen = new SourceCodeGenerator(model){
  // override mapped table and class name
  override def entityName =
    dbTableName => dbTableName.dropRight(1).toLowerCase.toCamelCase
  override def tableName =
    dbTableName => dbTableName.toLowerCase.toCamelCase
  // add some custom import
  override def code = "import foo.{MyCustomType,MyCustomTypeMapper}" + "\n" + super.code
  // override table generator
  override def Table = new Table(_){
    // disable entity class generation and mapping
    override def EntityType = new EntityType{
      override def classEnabled = false
    }
    // override contained column generator
    override def Column = new Column(_){
      // use the data model member of this column to change the Scala type, e.g. to a custom enum or anything else
      override def rawType =
        if(model.name == "SOME_SPECIAL_COLUMN_NAME") "MyCustomType" else super.rawType
    }
  }
}
codegen.writeToFile(
  "scala.slick.driver.H2Driver","some/folder/","some.packag","Tables","Tables.scala"
)

Slick 2.0.0 documentation - 06 Schemas

Permalink to Schemas — Slick 2.0.0 documentation

Schemas 

ここでは、Lifted Emebedding APIにおいて、データベーススキーマをどのようにして取り扱うのかということについて説明する。初めに、手でスキーマを記述する方法についての説明を行う。手で書く以外にもコードジェネレータを使うこともできる。

Tables 

型安全なクエリを扱うLifted Embedding APIを用いるには、データベーススキーマに対応するTableクラスと、TableQuery値を定義する必要がある。

class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "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", O.Default(0))
  def total = column[Int]("TOTAL", O.Default(0))
  def * = (name, supID, price, sales, total)
}
val coffees = TableQuery[Coffees]

全てのカラムはcolumn関数を通して定義される。各カラムはScalaの型を持っており、アッパーケースで通常記述されるデータベース用のカラム名を持つ。以下に挙げるようなプリミティブ型はJdbcProfileにおいて、JDBCベースなデータベースのためにボクシングされた型が適応される。(各種データベースドライバーによって恣意的に割り当てられているものもある)

nullを許容するカラムはTがサポートされたプリミティブ型である際に、Option[T]を用いて表せば良い。ただし、このOptionに対する全ての操作は、ScalaのOption操作と異なり、データベースのnullプロパゲーションセマンティクスを用いてしまう事に注意して欲しい。特に、None === Noneという式はNoneになる。これはSlickのメジャーリリースで将来的に変更されるかもしれない。

column関数には、カラム名の後にカラムのオプションを追加する事が出来る。適用出来るオブションはテーブルのOオブジェクトを通して利用出来る。以下のようなオプションがJdbcProfileにおいて定義されている。

全てのテーブルではデフォルトの射影を表す*関数が必要になる。これはクエリを通して行を取り出す際に戻ってくるものが何になるべきかを示すものである。Slickの*射影はデータベースの*とは一致したものになる必要は無い。何かしらの計算を行った新たなカラムを足してもいいし、特定のカラムを省いても良いし好きにして良い。*射影と一致するような持ち上げられていない(non-lifted)型はTableへと型パラメータとして与えられる。例えば、マッピングのないテーブルにおいて、これは単一のカラム型もしくはカラムのタプル型になるだろう。

Mapped Tables 

両方向マッピングを行う<>オペレータを用いる事で、*射影に対し、自由な型をテーブルへマッピングする事が出来る。

case class User(id: Option[Int], first: String, last: String)
...
class Users(tag: Tag) extends Table[User](tag, "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.tupled, User.unapply)
}
val users = TableQuery[Users]

(Option型を返すシンプルなapplyunapply関数のある)ケースクラスを用いる事で最適化されるが、自由なマッピング関数を用いても良い。この場合、適切な型を推測するのにタプルの.shapedを呼ぶのが役に立つ。一方で、マッピング関数に充分な型アノテーションを付与しても良いだろう。

Constraints 

外部キー制約はテーブルのforeignKey関数を用いて定義出来る。この関数は制約のための名前、ローカルカラム(もしくは射影、つまりここでは複合外部キーを定義出来る)、関連するテーブル、そしてテーブルから一致するカラムに対する関数を引数に取る。テーブルのためのDDLステートメントが作成される際、外部キー定義が追加される。

class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") {
  def id = column[Int]("SUP_ID", O.PrimaryKey)
  //...
}
val suppliers = TableQuery[Suppliers]
...
class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") {
  def supID = column[Int]("SUP_ID")
  //...
  def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
  // compiles to SQL:
  //   alter table "COFFEES" add constraint "SUP_FK" foreign key("SUP_ID")
  //     references "SUPPLIERS"("SUP_ID")
  //     on update NO ACTION on delete NO ACTION
}
val coffees = TableQuery[Coffees]

データベースに定義された実際の制約とは独立して、joinなどで用いられるような関連データについてのナビゲーションとしても外部キーは用いる事が出来る。この目的において、結合されるデータを探すための便利関数を手動で定義させる。

def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
def supplier2 = suppliers.filter(_.id === supID)

主キー制約は外部キーと同じように、primaryKey関数を用いる事で定義出来る。これは複合主キーを定義するのに便利なものとなっている。(column関数のオプションであるO.PrimaryKeyでは複合主キーは定義出来ない)

class A(tag: Tag) extends Table[(Int, Int)](tag, "a") {
  def k1 = column[Int]("k1")
  def k2 = column[Int]("k2")
  def * = (k1, k2)
  def pk = primaryKey("pk_a", (k1, k2))
  // compiles to SQL:
  //   alter table "a" add constraint "pk_a" primary key("k1","k2")
}

またインデックスについても同様にindex関数を用いて定義出来る。uniqueパラメータが内場合にはユニークなものではない、として定義される。

class A(tag: Tag) extends Table[(Int, Int)](tag, "a") {
  def k1 = column[Int]("k1")
  def k2 = column[Int]("k2")
  def * = (k1, k2)
  def idx = index("idx_a", (k1, k2), unique = true)
  // compiles to SQL:
  //   create unique index "idx_a" on "a" ("k1","k2")
}

全ての制約は、テーブルにおいて定義された適切な返却型を用いて、反射的に探索が行なわれる。このような挙動に対して、tableConstraints関数をオーバーライドする事でカスタマイズ出来る。

Data Definition Language 

テーブルのDDLステートメントは、TableQueryddl関数を用いて作成される。複数のDDLオブジェクトは++関数を用いて連結する事ができ、テーブル間にサイクルした依存関係が存在していたとしても、適切な順序で全てのテーブルを作成、削除する事が出来る。ステートメントはcreatedrop関数を用いて実行される。

val ddl = coffees.ddl ++ suppliers.ddl
db withDynSession {
  ddl.create
  //...
  ddl.drop
}

createStatementsdropStatements関数を用いると、実際に吐かれるSQLについて確認する事が出来る。

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

Slick 2.0.0 documentation - 07 Queries

Permalink to Queries — Slick 2.0.0 documentation

Queries 

ここでは、Lifted Embedding APIを用いたデータの選択、挿入、更新、削除について、どのようにして型安全なクエリを書くか、ということについて説明を行う

Expressions 

(レコードでもコレクションでもない)スカラー値は、TypedType[T]が必ず存在しているという条件の元、(Rep[T]のサブタイプである)Column[T]によって表される。内部的な利用のために、Columnクラスにおいて、いくつかの特別な関数が直接定義されている。

それらのオペレータやlifted embeddingにおいて一般的に用いられる他の関数は、ExtensionMethodConversionsにおいて定義された暗黙的な変換を通して追加されている。実際に用いる関数についてはAnyExtensionMethodsColumnExtensionMethodsNumericColumnExtensionMethodsBooleanColumnExtensionMethodsStringColumnExtensionMethodsといったクラスにおいて定義がなされている(参照: ExtensionMethods)。

コレクション値はQuery(Rep[Seq[T]])クラスによって表される。これは、flatMapfiltertakegroupByのような多くの標準的なコレクション関数を持っている。2つの異なるQueryの複合型により、これらの関数のシグネチャは非常に複雑なものになっているが、本質的にはScalaのコレクションと同様の意味合いを持つ。

他にも、スカラー値のクエリに対しいくつかの関数がSingleColumnQueryExtensionMethodsへの暗黙的な変換を通して存在する。

Sorting and Filtering 

ソートやフィルタリングのための関数がいくつか用意されている(Queryを取り、新しい同じ型のQueryを返す)。例として、以下のようなものがある。

val q1 = coffees.filter(_.supID === 101)
// compiles to SQL (simplified):
//   select "COF_NAME", "SUP_ID", "PRICE", "SALES", "TOTAL"
//     from "COFFEES"
//     where "SUP_ID" = 101
...
val q2 = coffees.drop(10).take(5)
// compiles to SQL (simplified):
//   select "COF_NAME", "SUP_ID", "PRICE", "SALES", "TOTAL"
//     from "COFFEES"
//     limit 5 offset 10
...
val q3 = coffees.sortBy(_.name.desc.nullsFirst)
// compiles to SQL (simplified):
//   select "COF_NAME", "SUP_ID", "PRICE", "SALES", "TOTAL"
//     from "COFFEES"
//     order by "COF_NAME" desc nulls first

Joining and Zipping 

結合(join)は2つの異なるテーブルを結合し、何らかのクエリ処理を1つのクエリで実行するために用いられる。

結合を行うには2つの方法がある。明示的な結合では、2つのクエリを1つのクエリへと結合させる関数(innerJoinなど)を呼び出すことにより処理を実行させる。暗黙的な結合では、そのような関数を呼び出す事はせず、特有の記述を行うことで結合を行わさせる。

暗黙的な交差結合(cross join)Queryに対しflatMap操作を行うことで実行させる事が出来る(すなわち、for式を用いる事で同様の記述が行える)。

val implicitCrossJoin = for {
  c <- coffees
  s <- suppliers
} yield (c.name, s.name)
// compiles to SQL:
//   select x2."COF_NAME", x3."SUP_NAME"
//     from "COFFEES" x2, "SUPPLIERS" x3

もし結合の際にフィルタリングを行ったのなら、これは暗黙的な内部結合(inner join)となる。

val implicitInnerJoin = for {
  c <- coffees
  s <- suppliers if c.supID === s.id
} yield (c.name, s.name)
// compiles to SQL:
//   select x2."COF_NAME", x3."SUP_NAME"
//     from "COFFEES" x2, "SUPPLIERS" x3
//     where x2."SUP_ID" = x3."SUP_ID"

このような暗黙的結合は、ScalaコレクションのflatMapを扱うのと同様の意味合いを持つ。

明示的結合は適切なjoin関数を呼び出す事で実行出来る。

val explicitCrossJoin = for {
  (c, s) <- coffees innerJoin suppliers
} yield (c.name, s.name)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     inner join "SUPPLIERS" x3
...
val explicitInnerJoin = for {
  (c, s) <- coffees innerJoin suppliers on (_.supID === _.id)
} yield (c.name, s.name)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     inner join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"
...
val explicitLeftOuterJoin = for {
  (c, s) <- coffees leftJoin suppliers on (_.supID === _.id)
} yield (c.name, s.name.?)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     left outer join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"
...
val explicitRightOuterJoin = for {
  (c, s) <- coffees rightJoin suppliers on (_.supID === _.id)
} yield (c.name.?, s.name)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     right outer join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"
...
val explicitFullOuterJoin = for {
  (c, s) <- coffees outerJoin suppliers on (_.supID === _.id)
} yield (c.name.?, s.name.?)
// compiles to SQL (simplified):
//   select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2
//     full outer join "SUPPLIERS" x3
//     on x2."SUP_ID" = x3."SUP_ID"

ここでは外部結合において.?といったものを用いている。これは、このような結合ではnull値が新たに追加されてしまうため、そのような値に対しOption値が取得される事を保証するためである。(左外部結合、右外部結合においても同様である)

リレーショナルデータベースによってサポートされた一般的な結合処理に加えて、Slickでは2つのクエリのペアワイズ結合を作成するzip結合というものを提供している。これはScalaコレクションにおいてzipzipWith関数を用いた処理と同様の意味合いを持つものである。

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

ある種のzip結合はzipWithIndexにより提供される。これはクエリの結果を0から始まる無限数列とzipしたものとなる。そのような数列についてはSQLデータベースでは表す事が出来ず、Slickでも現在ではサポートしていない。しかし、行番号(row number)関数を利用する事でSQLにおいてzipクエリの結果については表す事が出来る。ゆえにzipWithIndexは原子的なオペレータとしてサポートされているのである。

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

Unions 

両立可能な2つのクエリは、++(もしくはunionAll)やunionオペレータを用いる事で連結する事が出来る。

val q1 = coffees.filter(_.price < 8.0)
val q2 = coffees.filter(_.price > 9.0)
...
val unionQuery = q1 union q2
// compiles to SQL (simplified):
//   select x8."COF_NAME", x8."SUP_ID", x8."PRICE", x8."SALES", x8."TOTAL"
//     from "COFFEES" x8
//     where x8."PRICE" < 8.0
//   union select x9."COF_NAME", x9."SUP_ID", x9."PRICE", x9."SALES", x9."TOTAL"
//     from "COFFEES" x9
//     where x9."PRICE" > 9.0
...
val unionAllQuery = q1 ++ q2
// compiles to SQL (simplified):
//   select x8."COF_NAME", x8."SUP_ID", x8."PRICE", x8."SALES", x8."TOTAL"
//     from "COFFEES" x8
//     where x8."PRICE" < 8.0
//   union all select x9."COF_NAME", x9."SUP_ID", x9."PRICE", x9."SALES", x9."TOTAL"
//     from "COFFEES" x9
//     where x9."PRICE" > 9.0

重複した値を弾くunionと違って、++は、より効率的な個々のクエリの結果を、単純に連結させる。

Aggregation 

最も単純な集合操作は、単一カラムを返却するQueryからプリミティブな値(大抵は数値型)を計算させる事で取得を行う。

val q = coffees.map(_.price)
...
val q1 = q.min
// compiles to SQL (simplified):
//   select min(x4."PRICE") from "COFFEES" x4
...
val q2 = q.max
// compiles to SQL (simplified):
//   select max(x4."PRICE") from "COFFEES" x4
...
val q3 = q.sum
// compiles to SQL (simplified):
//   select sum(x4."PRICE") from "COFFEES" x4
...
val q4 = q.avg
// compiles to SQL (simplified):
//   select avg(x4."PRICE") from "COFFEES" x4

これらの集合クエリはコレクションではなく、スカラー値を返却する事に注意して欲しい。いくつかの集合関数は恣意的なクエリにより定義がなされている。

val q1 = coffees.length
// compiles to SQL (simplified):
//   select count(1) from "COFFEES"
...
val q2 = coffees.exists
// compiles to SQL (simplified):
//   select exists(select * from "COFFEES")

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)
}
// compiles to SQL:
//   select x2."SUP_ID", count(1), avg(x2."PRICE")
//     from "COFFEES" x2, "SUPPLIERS" x3
//     where x3."SUP_ID" = x2."SUP_ID"
//     group by x2."SUP_ID"

ここで、中間クエリであるqはネストされた型Queryの値を保持している。つまり、クエリを実行する際にはネストされたコレクションが現れる。これは現在サポートがされていない。それゆえ、q2において行なわれるようにそれらの値(もしくは個々のカラム)をまとめることで、ネストされたクエリを即座に平滑化する必要がある。

Querying 

クエリはInvokerトレイト(もしくはパラメータが無い場合にはUnitInvoker)において定義された関数を用いて実行される。Queryに対する暗黙的な変換が存在しているため、直接的にQueryを実行できるのである。最も一般的な利用法として、listtoのような特定の関数を用いて、適切にコレクションの値を結果として読み込み事がある。

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

このスニペットは暗黙的な変換関数を呼び出す事無しに、どのようにして手動でinvokerに対する参照を取得するのかを示している。

クエリを実行する全ての関数は暗黙的なSessionを必要とする。もちろん明示的にSessionを渡してあげてもよい。

val l = q.list()(session)

もし単一の結果値が欲しいのなら、firstfirstOptionといった関数を用いる事が出来る。foreachfoldLeftelementsといった関数はScalaコレクションに全てのデータをコピーしたりせずに、結果をイテレートさせる事が出来る。

Deleting 

データの削除はクエリの実行と同じように処理させる。削除したいデータを取得するクエリを書いた後に、delete関数を呼び出せば良い。Queryからdelete関数と自己参照用のdeleteInvokerを提供するDeleteInvokerへの暗黙的な変換が存在している。

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

削除のためのクエリは単一のテーブルからデータを取得すべきだ。どんな射影も無視されるだろう(常に行を丸々削除する)。

Inserting 

データの挿入は単一のテーブルに対し、カラムの射影に基づいて実行される。テーブルを直接用いる際には、挿入はテーブルの*射影関数を用いて実行する。挿入時にテーブルのカラムをいくつか省くと、データベースはテーブル定義に基づき、デフォルト値を利用する。明示的なデフォルト値が無い場合には、型特有なデフォルト値を用いる。挿入に対する全ての関数はInsertInvokerFullInsertInvokerにおいて定義がなされている。

coffees += ("Colombian", 101, 7.99, 0, 0)
coffees ++= Seq(
  ("French_Roast", 49, 8.99, 0, 0),
  ("Espresso",    150, 9.99, 0, 0)
)
...
// "sales" and "total" will use the default value 0:
coffees.map(c => (c.name, c.supID, c.price)) += ("Colombian_Decaf", 101, 8.99)
val statement = coffees.insertStatement
val invoker = coffees.insertInvoker
// compiles to SQL:
//   INSERT INTO "COFFEES" ("COF_NAME","SUP_ID","PRICE","SALES","TOTAL") VALUES (?,?,?,?,?)

もしAutoIncなカラムが挿入操作において含まれていたなら、暗黙的に無視され、データベースは適切な値を生成しようとする。このような場合において、自動生成された主キーのカラムを返却して欲しいと思うだろう。デフォルトでは、+=関数は変更の合った行数(通常は1)を返却し、++=関数は蓄積したOptionの数を返却する(もしデータベースシステムがカウントを提供しなければ、Noneになるため)。もし特定のカラムを返却させたいのなら、returning関数を用いて変更する事が出来る。+=からは単一値もしくはタプルを、+==からはそれらの値のSeqを返す事が出来る。

val userId =
  (users returning users.map(_.id)) += User(None, "Stefan", "Zeiger")

ちなみに、多くのデータベースシステムではテーブルの自動インクリメントされる主キーを返却する事を許可している。もし他のカラムを返却しようとしたなら、(データベースがサポートしていない場合にも)SlickExceptionが実行時(at runtime)に投げられる。

クライアント側から挿入されるデータの代わりに、Queryによって作成されたデータもしくはデータベースサーバにおいて実行されたスカラー表現を挿入する事もできる。

class Users2(tag: Tag) extends Table[(Int, String)](tag, "users2") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def * = (id, name)
}
val users2 = TableQuery[Users2]
users2.ddl.create
users2 insert (users.map { u => (u.id, u.first ++ " " ++ u.last) })
users2 insertExpr (users.length + 1, "admin")

このような場合では、AutoIncカラムは無視されない。

Updating 

データの更新は、更新するデータを取得し、新たなデータに差し替えるクエリを記述する事で行える。クエリは単一のテーブルから選択された(計算のされていない)カラムが返却されるべきである。更新に関連する関数は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
...
// compiles to SQL:
//   update "COFFEES" set "PRICE" = ? where "COFFEES"."COF_NAME" = 'Espresso'

今現在、スカラー表現やデータベースに存在するデータを変換して用いる更新処理を行う方法は無い。

Compiled Queries 

データベースに対する処理は基本的にいくつかのパラメータに依存している(これはデータベースから探索を行いたいデータのIDの事などである)。クエリを実行するたびに、パラメータを入れたQueryオブジェクトを作成するような関数をしばしば記述する。しかし、これはSlickにおいてクエリをコンパイルしなおすコストを増長させる。そこで、このようなパラメータが固定されたクエリについて、事前コンパイルを行うことでより効率化する事が出来る。

def userNameByIDRange(min: Column[Int], max: Column[Int]) =
  for {
    u <- users if u.id >= min && u.id < max
  } yield u.first

val userNameByIDRangeCompiled = Compiled(userNameByIDRange _)
...
// The query will be compiled only once:
val names1 = userNameByIDRangeCompiled(2, 5).run
val names2 = userNameByIDRangeCompiled(1, 3).run
// Also works for .update and .delete

これはColumnパラメータ(もしくはカラムのレコード)を取ったり、Queryオブジェクトやクエリを返却する全ての関数において上手く機能する。CompiledやそのサブクラスのAPIドキュメントを見ると、コンパイルされたクエリの構成についての詳細を知ることが出来る。

コンパイルされたクエリをクエリ処理、更新、削除といった処理に対して用いる事ができる。

Slick 1.0の後方互換のために、ParametersオブジェクトのflatMapを呼ぶことで依然コンパイルされたクエリを作る事が出来る。多くの場合において、単一のfor式を書くことでコンパイルされたクエリを作る事が出来るだろう。

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 2.0.0 documentation - 08 User-Defined Features

Permalink to User-Defined Features — Slick 2.0.0 documentation

User-Defined Features 

ここではLifted Embedding APIにおいて、カスタムしたデータ型やデータベース関数をどのようにして用いるのか、についての説明を行う。

Scala Database functions 

もしデータベースシステムが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))

SimpleBinaryOperatorSimpleLiteralも同様に扱う事ができる。より柔軟なものを求めるのならば、SimpleExpressionを使うと良い。

Other Database functions and stored procedures 

完全なテーブル(complete tables)やストアドプロシージャを返却するデータベース関数を扱うのならば、Plain SQL Queriesを使えば良い。複数の結果を返却するストアドプロシージャは現在サポートしていない。

Scalar Types 

もしカスタムされたカラムが必要ならば、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)
}

Record Types 

レコード型は個別に宣言された型を持つ複数の混合物を含んだデータ構造となっている。 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)) }

Slick 2.0.0 documentation - 09 Plain SQL Queries

Permalink to Plain SQL Queries — Slick 2.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

String を既存の 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 といった値を読み込む getIntgetString といった 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 

SQL を発行する string interpolation 接頭辞である、sqlsqlu を用いるためには、以下のインポート文を追加する。

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 2.0.0 documentation - 10 Slick Extensions

Permalink to Slick Extensions — Slick 2.0.0 documentation

Slick Extensions 

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

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

// Use the right Slick version here:
libraryDependencies += "com.typesafe.slick" %% "slick-extensions" % "2.0.0"

resolvers += "Typesafe Releases" at "http://repo.typesafe.com/typesafe/maven-releases/"

Slick 2.0.0 documentation - 11 Direct Embedding (Experimental Feature)

Permalink to Direct Embedding — Slick 2.0.0 documentation

Direct Embedding (Experimental Feature) 

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.name.repr ) ))将来的には、コンパイル中にそのようなものもキャッチするような方法を検討している。

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

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

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

Slick 2.0.0 documentation - 12 Slick TestKit

Permalink to Slick TestKit — Slick 2.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" % "2.0.0-RC1",
  "com.typesafe.slick" %% "slick-testkit" % "2.0.0-RC1" % "test",
  "com.novocode" % "junit-interface" % "0.10" % "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:
# - Install PostgreSQL server with default options
# - Change password in mypostgres.password
# - Set mypostgres.enabled = true
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を用いて実行される。これはテストハーネスを通してセットアップされたデータベースを用いており、ドライバーを用いて適応可能な全てのテストが実行される事になる。

Fork me on GitHub