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)
Fork me on GitHub