Slick 1.0.0 documentation - 02 始めよう

Permalink to Getting Started — Slick 1.0.0 documentation

始めよう 

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

依存性 

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

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

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

Imports 

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

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

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

Databaseへの接続 

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

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

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

スキーマ 

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

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

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

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

Populating the Database 

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

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

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

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

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

Querying 

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

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

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

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

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

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

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

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

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

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

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