Slick 2.0 documentationの日本語訳です。
Slick 2.0.0 documentation - 01 導入(Introduction) Permalink to Introduction — Slick 2.0.0 documentation
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テンプレートを使うと良い。
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)
// `select PRICE from COFFEES` の結果はSeq[Double]になる
// これは型安全な処理が行なわれるためである
val coffeeNames: Seq[Double] = coffees.map(_.price).list
// クエリを作るのも型安全に行なわれる
coffees.filter(_.price < 10.0)
// もし条件の中で異なる型が比較されていたのなら、コンパイルエラーになる
// 10.0 未満の価格で、名前順にソートしたコーヒーの名前を取り出すクエリを作る
coffees.filter(_.price < 10.0).sortBy(_.name).map(_.name)
// ここで作られるSQLは次のものと等価になる
// select name from COFFEES where PRICE < 10.0 order by NAME
SlickはScalaのバージョン2.10が必要になる。(もし2.9以下で使いたいならScalaQueryを使うと良い)
他のSQLデータベースもSlickなら簡単にアクセスする事が出来る。独自のSQLベースのバックエンドを持つデータベースも、プラグインを作成する事でSlickを利用することが出来ます。そのようなプラグインの作成は大きな貢献となる。 NoSQLのような他のバックエンドを持つようなデータベースに関しては現在開発中であるため、まだ利用する事はできません。
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.
Lifted Embedding は型安全なクエリや更新が行えるSlickの基本的なAPIである。Getting Startedでは Lifted Embedding を用いた例を紹介する。
SQL文を直接発行したい場合には、Plain SQL API を利用することが出来る。
Direct 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を使う際、まず初めに、利用するデータベースに応じたAPIを以下のようにインポートする必要がある。
// H2 databaseへ接続するためにH2Driverをimport
import scala.slick.driver.H2Driver.simple._
H2 Databaseを用いているため、Slickの H2Driver
をimportする必要がある。このdriverに含まれる simple
オブジェクトにはsession handlingといったSlickに必要な共通の機能が含まれている。
アプリケーションの中では、どのようにデータベースに接続するのかを明示する 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 embeddingやlifted 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を指していたりする。この構成はデータベースレベルで強制されている。
組み込みの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, , )
)
TableQuery
の ddl
関数は、テーブルやその他データベースのエンティティを作成したり削除したりするための、データベース特有のコードを用いて DDL
(data definition language)オブジェクトを作成する。複数の DDL
は ++
を用いる事で、お互いが依存し合っていたとしても、全てのエンティティに対し正しい順序で作成と削除を行う。
複数のデータを挿入する際は insert
や insertAll
といった関数を用いる。デフォルトではデータベースの Session
は auto-commit モードになっている事に注意して欲しい。 insert
や insertAll
のようなデータベースへの呼び出しはトランザクションにおいて、原子性が保たれるよう実行される(つまり、それらの処理は完全に実行するか全く実行しないかのいずれかが保証される)。このモードにおいては、 Coffee
が対応するSupplierのIDのみを参照するため、 Supplier
テーブルに対し先にデータを挿入しなくてはならない。
これらの記述を全て包括した明示的なトランザクションのブラケットを用いることも可能である。その際、トランザクションによって処理が強制されるため、順序は重要視されない。
最も簡単なクエリ例として、テーブルのデータを全て順々に取り出す処理を考える。
// 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で map
や for式 を用いる事で実装出来る。
// なぜデータベースでは文字列の変換や連結が出来ないんだろう...?
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
Slick2.0はSlick1.0に互換性のない拡張が含まれている。アプリケーションを1.0から2.0へ移行する際には、以下のような変更が必要になるだろう。
以前は手で書いていたテーブルへのマッピングを、2.0ではデータベーススキーマを用いて自動的に生成出来るようになった。code-generaterは柔軟にカスタマイズすることも出来るため、より最適化されたものに変更する事も出来る。詳細については、code-generationを参考にして欲しい。
Slick1.0では、テーブルはval
やtable 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)や、外部キー関連などがある。
Slick 1.0ではBasicProfile
とExtendedProfile
の2つのプロファイルを提供していた。Slick 2.0ではこれら2つのプロファイルをJdbcProfile
として統合している。今ではRelationalProfile
に挙げられるようなより抽象的なプロファイルを提供している。RelationalProfile
はJdbcProfile
の全ての特徴を持っているわけではないが、新しく出来たHeapDriver
やDistributedDriber
といった機能を支えている。Slick 1.0からコードを移植する際、JdbcProfile
へとプロファイルを変更して欲しい。特にSlick 2.0におけるBasicProfile
は1.0におけるBasicProfil
と非常に異なったものになっているので注意して欲しい。
Slick1.0では挿入時にtable objectの一部を射影していた。
// --------------------- Slick 1.0 code -- does not compile in 2.0 ---------------------
(Suppliers.name ~ Suppliers.street) insert ("foo", "bar")
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)
id
というカラムをSlickが除外してくれる。
逆にAutoInc
のついたカラムに対し値を挿入したいのならば、新しく出来たforceInsert
やforceInsertAll
といった関数を用いれば良い。
<>
関数はオーバーロードされ、今やケースクラスの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)
Slickはselect文において用いられるのと同じ方法で、update文における事前コンパイルもサポートしている。これについては、Compliled-Queriesのセクションを見て欲しい。
Slick 1.0ではDatabase
のファクトリオブジェクトとして標準的なJDBCベースなDatabase
とSession
といった型がscala.slick.session
パッケージにある。Slick 2.0からはJDBCベースなデータベースに制限せず、このパッケージは(backendとしても知られる)DatabaseComponent
階層
によって置き換えられている。もしJdbcProfile
抽象レベルで動かしたいのならば、以前にscala.slick.session
にあったものをインポートし、常にJdbcBackend
を用いれば良い。ただし、simple._
といったインポートを行うと自動的にスコープ内にこれらの型が持ち込まれてしまうので注意して欲しい。
Slick 2.0では依然としてスレッドローカルな動的セッションと静的スコープセッションを提供している。しかしシンタックスが変わっており、静的スコープセッションを用いる際にはより簡潔な記述が推奨される。以前のthreadLocalSession
はdynamicSession
という名前に変わっており、関連するwithSession
やwithTransaction
といった関数もwithDynSession
とwithDynTransaction
という名前にそれぞれ変わっている。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
}
myDB withSession { implicit session =>
// use the implicit session here
}
また、動的セッションを使うことは確かな情報を取得できるか分からない事から推奨されていない。静的セッションを用いる方がより安全である。
Slick 1.0のMappedTypeMapper
はMappedColumnType
へと名前が変わった。`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
クエリはプログラムのどこにでも書くことが出来る。クエリを実行する際には、データベースコネクションが必要になる。
用いるJDBCデータベースに対してどのように接続するのかを、それらの情報をカプセル化したDatabase
オブジェクトを作成することで、Slickへ伝える事が出来る。Database
オブジェクトを作成するにはscala.slick.jdbc.JdbcBackend.Database
にいくつかのファクトリメソッドが用意されており、どのような方法で接続するかによって使い分ける事が出来る。
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データベース特有のオプション)データベースとなっている。
DataSource
オブジェクトを既に持っているのなら、`forDataSource`を用いてDatabase
オブジェクトを作成出来る。もしアプリケーションフレームワークのコネクションプールからDataSource
オブジェクトを取得出来るのなら、Slickのプールへと繋いで欲しい。
val db = Database.forDataSource(dataSource: javax.sql.DataSource)
後でセッションを作成する時には、コネクションはプールから取得出来るし、セッションが閉じた時に、コネクションはプールへ返却される。
もしJNDIを用いているのなら、DataSourceオブジェクトが見つかるJNDIの名前をforNameに渡してあげたら良い。
Databaseオブジェクトを持っているのなら、SessionオブジェクトにSlickがカプセル化したデータベースコネクションを開く事が出来る。
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モードになっている。insertやinsertAllのようなデータベースへの呼び出しは原子的(必ず成功するか失敗するかのいずれかが保証されている)に実行される。いくつかの状態を包括するにはTransactionsを用いる。
注意: もしSessionオブジェクトがwithSessionのスコープ以外で用いられていたのなら、その接続は既に閉じられており、妥当な利用法にはなっていない。利用を避けるべきではあるば、このような状態を避ける方法がいくつかあり、例としてクロージャを用いる(withSessionスコープ内にてFutureを用いるなど)、varへセッションを割り当てる、withSessionスコープの返り値としてセッションを返却するといった方法がある。
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
これはオプショナルな使い方ではあるが、用いるとよりコードを綺麗にする事が出来る。
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
}
この方法は推奨されない。もししなければならない場面があるのなら、Sessionを手動で取り扱うことも出来る。
val query = for (c <- coffees) yield c.name
val session : Session = db.createSession
val result = query.list()( session )
session.close
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)
セッションは長い間開きっぱなしにはしたくないが、必要な時にはすぐに開いたり閉じたりしたいと考えるだろう。上記の例では、クエリを実行するために必要な時に暗黙的なセッション引数を用いてセッションスコープやトランザクションスコープを使っていた。
別の方法として、共通化したコードの部分的なものを保存する、ということが、ファイルの先頭に追加に次の行を追加する事で行える。これにより、セッション引数無しのセッションスコープやトランザクションスコープを利用する事が出来る。
import Database.dynamicSession // <- implicit def dynamicSession : Session
現在のコールスタック内のどこかでwithDynSessionかwithDynTransactionスコープが開かれていた場合において、dynamicSessionは適切なSessionを返却する暗黙的な関数となる。
db.withDynSession {
// your queries go here
}
注意して欲しいのは、もしdynamicSessionがインポートさあれ、withDynSessionやwithDynTransactionスコープの外側でクエリが実行されようとしているのならば、実行時例外を吐いてしまう事である。つまり、静的な安全性を犠牲にしているのである。dynamicSessionは内部的にDynamicVariableを用いる。これは動的にスコープのある変数を作成し、JavaのInheritableThreadLocalを順々に用いるものである。静的であることの安全性とスレッドの安全性に配慮して欲しい。
Slickは独自のコネクションプール実装を持っていない。JEEやSpringのようなある種のコンテナにおけるアプリケーションを動かす際、一般的にコンテナに提供されたコネクションプールを用いる事になるだろう。スタンドアローンなアプリケションにおいてはDBCPやc3p0、BoneCPのような外部のコネクションプールの実装を用いる事が出来る。
ちなみに、Slickはどこでも利用可能なプリペアドステートメントを持ってはいるが、独自でキャッシュをしたりはしない。よって、コネクションプールの設定において、プレペア度ステートメントのキャッシュを有効にすべきであるし、充分に大きなプールサイズを用意すべきだ。
Slick 2.0.0 documentation - 05 Schema code generation
Permalink to Schema Code Generation — Slick 2.0.0 documentation
Slickコードジェネレータは既存のデータベーススキーマをそのまま動かす上で便利なツールとなっている。スタンドアローン形式で動かしたり、sbtのbuildに対し統合したり出来る。
デフォルトでコードジェネレータは、TableQueryの値に対応するTableクラスを生成する。これらの値は、個々は行の値を包括するケースクラスとなり、全体としてコレクション操作関数が呼び出せるようなものになっている。もしScalaのタプルの限界数である22個より多いカラムが存在していたのなら、自動的にSlickの実験的な実装であるHListを用いた実装を出力する。(ちなみに、25カラムより多い場合には非常にコンパイルに時間がかかる事が分かっており、可能な限り早く修正する予定だ)
実装は実用的なものになってはいるが、コードジェネレータはSlick 2.0における新しい機能となっており、依然として実験的なものも含んでいる。必要なものを摘出し、必要のない機能を取り除いていく予定だ。将来的なバージョンにおけるコードジェネレータに対する修正は小さくする予定だ。もし必要ならば、Slickの他の部分から独立した実装にしても良い。我々はこの機能を用いた人々の挑戦に対する声に非常に関心がある。
ジェネレータについて、talk at Scala eXchange2013で軽く説明も行っている。
Slickのコードジェネレータは以下のようにして手軽に動かすことが出来る。
scala.slick.model.codegen.SourceCodeGenerator.main(
Array(slickDriver, jdbcDriver, url, outputFolder, pkg)
)
必要な引数は以下の通りである
コードジェネレータは指定されたパッケージ名に一致するサブフォルダを、指定された出力先フォルダの中に作成し、そこの“Tables.scala“というファイルへ結果を出力する。そのファイルには“Tables“オブジェクトが生成される。引数に与えたSlickドライバーと同じものが用いられているかを確認して欲しい。このファイルには同様に“Tables“トレイトがふくまれ、これはCakeパターンに用いられたものになっている。
コードジェネレータをコンパイル毎に事前に実行することも出来るし、手動で実行することも出来る。実際に使ってみた例がこちらにあるので見て欲しい。
コードジェネレータはモデルデータに基づきコードを自動生成する関数をオーバーライドする事で、柔軟にカスタマイズ出来る。小さなカスタマイズであっても大きなカスタマイズであっても、このようなモデルドリブンなコードジェネレーションが同じように扱われる。例えば、とあるフレームワークにおけるバインディングや、その他のデータに関連するアプリケーションの繰り返しセクションにおいて用いられる。
この例ではカスタマイズされたコードジェネレータを用いており、メインリソースをコンパイルする前にコードジェネレータを走らせるマルチプロジェクトの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
ここでは、Lifted Emebedding APIにおいて、データベーススキーマをどのようにして取り扱うのかということについて説明する。初めに、手でスキーマを記述する方法についての説明を行う。手で書く以外にもコードジェネレータを使うこともできる。
型安全なクエリを扱う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
において定義されている。
PrimaryKey
Default[T](defaultValue: T)
DBType(dbType: String)
String
型のカラムに対してDBType("VARCHAR(20")
を用いるなど)。
AutoInc
AutoInc
として適切にマークされているのかどうかをチェックする。
NotNull
, Nullable
Option
型がOption
型でないかといった違いからも決定される。一般的にこれらのオプションを使う理由は無い。
全てのテーブルではデフォルトの射影を表す*
関数が必要になる。これはクエリを通して行を取り出す際に戻ってくるものが何になるべきかを示すものである。Slickの*
射影はデータベースの*
とは一致したものになる必要は無い。何かしらの計算を行った新たなカラムを足してもいいし、特定のカラムを省いても良いし好きにして良い。*
射影と一致するような持ち上げられていない(non-lifted)型はTable
へと型パラメータとして与えられる。例えば、マッピングのないテーブルにおいて、これは単一のカラム型もしくはカラムのタプル型になるだろう。
両方向マッピングを行う<>
オペレータを用いる事で、*
射影に対し、自由な型をテーブルへマッピングする事が出来る。
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
型を返すシンプルなapply
やunapply
関数のある)ケースクラスを用いる事で最適化されるが、自由なマッピング関数を用いても良い。この場合、適切な型を推測するのにタプルの.shaped
を呼ぶのが役に立つ。一方で、マッピング関数に充分な型アノテーションを付与しても良いだろう。
外部キー制約はテーブルの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
関数をオーバーライドする事でカスタマイズ出来る。
テーブルのDDLステートメントは、TableQuery
のddl
関数を用いて作成される。複数のDDL
オブジェクトは++
関数を用いて連結する事ができ、テーブル間にサイクルした依存関係が存在していたとしても、適切な順序で全てのテーブルを作成、削除する事が出来る。ステートメントはcreate
とdrop
関数を用いて実行される。
val ddl = coffees.ddl ++ suppliers.ddl
db withDynSession {
ddl.create
//...
ddl.drop
}
createStatements
やdropStatements
関数を用いると、実際に吐かれるSQLについて確認する事が出来る。
ddl.createStatements.foreach(println)
ddl.dropStatements.foreach(println)
Slick 2.0.0 documentation - 07 Queries
Permalink to Queries — Slick 2.0.0 documentation
ここでは、Lifted Embedding APIを用いたデータの選択、挿入、更新、削除について、どのようにして型安全なクエリを書くか、ということについて説明を行う
(レコードでもコレクションでもない)スカラー値は、TypedType[T]
が必ず存在しているという条件の元、(Rep[T]
のサブタイプである)Column[T]
によって表される。内部的な利用のために、Column
クラスにおいて、いくつかの特別な関数が直接定義されている。
それらのオペレータやlifted embeddingにおいて一般的に用いられる他の関数は、ExtensionMethodConversions
において定義された暗黙的な変換を通して追加されている。実際に用いる関数についてはAnyExtensionMethods
、ColumnExtensionMethods
、NumericColumnExtensionMethods
、BooleanColumnExtensionMethods
、StringColumnExtensionMethods
といったクラスにおいて定義がなされている(参照: ExtensionMethods)。
コレクション値はQuery
(Rep[Seq[T]]
)クラスによって表される。これは、flatMap
、filter
、take
、groupBy
のような多くの標準的なコレクション関数を持っている。2つの異なるQuery
の複合型により、これらの関数のシグネチャは非常に複雑なものになっているが、本質的にはScalaのコレクションと同様の意味合いを持つ。
他にも、スカラー値のクエリに対しいくつかの関数がSingleColumnQueryExtensionMethods
への暗黙的な変換を通して存在する。
ソートやフィルタリングのための関数がいくつか用意されている(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
結合(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コレクションにおいてzip
やzipWith
関数を用いた処理と同様の意味合いを持つものである。
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)
両立可能な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
と違って、++
は、より効率的な個々のクエリの結果を、単純に連結させる。
最も単純な集合操作は、単一カラムを返却する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
において行なわれるようにそれらの値(もしくは個々のカラム)をまとめることで、ネストされたクエリを即座に平滑化する必要がある。
クエリはInvokerトレイト(もしくはパラメータが無い場合にはUnitInvoker)において定義された関数を用いて実行される。Query
に対する暗黙的な変換が存在しているため、直接的にQuery
を実行できるのである。最も一般的な利用法として、list
やto
のような特定の関数を用いて、適切にコレクションの値を結果として読み込み事がある。
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)
もし単一の結果値が欲しいのなら、first
やfirstOption
といった関数を用いる事が出来る。foreach
、foldLeft
、elements
といった関数はScalaコレクションに全てのデータをコピーしたりせずに、結果をイテレートさせる事が出来る。
データの削除はクエリの実行と同じように処理させる。削除したいデータを取得するクエリを書いた後に、delete
関数を呼び出せば良い。Query
からdelete
関数と自己参照用のdeleteInvoker
を提供するDeleteInvokerへの暗黙的な変換が存在している。
val affectedRowsCount = q.delete
val invoker = q.deleteInvoker
val statement = q.deleteStatement
削除のためのクエリは単一のテーブルからデータを取得すべきだ。どんな射影も無視されるだろう(常に行を丸々削除する)。
データの挿入は単一のテーブルに対し、カラムの射影に基づいて実行される。テーブルを直接用いる際には、挿入はテーブルの*
射影関数を用いて実行する。挿入時にテーブルのカラムをいくつか省くと、データベースはテーブル定義に基づき、デフォルト値を利用する。明示的なデフォルト値が無い場合には、型特有なデフォルト値を用いる。挿入に対する全ての関数はInsertInvokerとFullInsertInvokerにおいて定義がなされている。
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
カラムは無視されない。
データの更新は、更新するデータを取得し、新たなデータに差し替えるクエリを記述する事で行える。クエリは単一のテーブルから選択された(計算のされていない)カラムが返却されるべきである。更新に関連する関数は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'
今現在、スカラー表現やデータベースに存在するデータを変換して用いる更新処理を行う方法は無い。
データベースに対する処理は基本的にいくつかのパラメータに依存している(これはデータベースから探索を行いたいデータの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
ここでは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)) }
Slick 2.0.0 documentation - 09 Plain SQL Queries
Permalink to Plain SQL Queries — Slick 2.0.0 documentation
高度な操作について、SQL文を直接書きたくなる事があるかもしれない。Slickの Plain SQL クエリでは、JDBCの低レイアに触れる事無しに、よりScalaベースな記述を行う事が出来る。
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 {
}
最もシンプルな 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 (?,?,?,?,?))となっている。
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)
SQL を発行する string interpolation 接頭辞である、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 2.0.0 documentation - 10 Slick Extensions
Permalink to Slick Extensions — Slick 2.0.0 documentation
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は新しい、しかしまだ不完全で実験的なクエリAPIである。現在実験中。開発中の段階であるため、リリースに応じて非推奨な期間など無しに変更される事がある。安全に利用する事の出来る、安定したlifted embeddingクエリAPIに取って代わるような予定は無く、direct embeddingは共存させていく。lifted embeddingと違って、direct enbeddingは実装のための暗黙的な変換やオーバーロードするオペレータの代わりにマクロを用いて操作を行う。ユーザのために、コード内における違いは少なくしているが、direct enbeddingを用いるクエリは普遍的なScalaの型を用いて機能している。これは表示されるエラーメッセージの理解性を上げるためでもある。
以下の説明は*lifted embedding*の説明に類似した例である。
direct embeddingは型検査のために実行時にScalaコンパイラにアクセスする必要がある。Slickは必要性に駆られない限り、アプリケーションに対し、依存性を避けるためにScalaコンパイラへの依存性を任意としている。そのため、direct embeddingを用いる際にはプロジェクトの build.sbt
に対し明示的にその依存性を記述しなくてはならない。
libraryDependencies <+= (scalaVersion)("org.scala-lang" % "scala-compiler" % _)
import scala.slick.driver.H2Driver
import H2Driver.simple.Database
import Database.{threadLocalSession => session}
import scala.slick.direct._
import scala.slick.direct.AnnotationMapper._
スキーマは現在でえは行を保持しているケースクラスに対してアノテーションを付与する事で記述する事が出来る。今後、より柔軟にスキーマの情報を拡張出来るような機能を提供する予定だ。
// describe schema for direct embedding
@table(name="COFFEES")
case class Coffee(
@column(name="NAME")
name : String,
@column(name="PRICE")
price : Double
)
Queryableはテーブルデータに対しクエリの演算を行うためのものであり、注釈付けられた型引数を取る。
_.price
はここではInt型である。潜在的な、マクロベースの実装においてはmapやfilterに与えられた引数はJVM上で実行されないが、その代わりにデータベースクエリへと変換される事を覚えておいて欲しい。
// query database using direct embedding
val q1 = Queryable[Coffee]
val q2 = q1.filter( _.price > 3.0 ).map( _ .name )
クエリを実行するためには、選択したデータベースのドライバーを用いる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 ) )
}
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
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に対し、独自のデータベースドライバーを記述する際には、きちんと動作するのか、何が現時点で実装されていないのかなどを確認するために、ユニットテスト(もしくは加えて他の独自のカスタマイズしたテスト)をきちんと記述して欲しい。簡単にテストを記述するためのサポートとして、Slickユニットテスト用のSlick Test Kitプロジェクトを別に用意している。
これを用いるためには、Slickの基本的なPostgreSQLドライバーと、ビルドするために必要なものを全て含んだSlick TestKit Exampleをクローンして使って欲しい。
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フレームワーク以外のものを使う事も出来る。
ドライバーの実装はsrc/main/scalaの中にある。
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
}
}
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
sbt test を実行すると、 MyPostgresTest を探索し、TestKitのJUnit runnerを用いて実行される。これはテストハーネスを通してセットアップされたデータベースを用いており、ドライバーを用いて適応可能な全てのテストが実行される事になる。