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)