Slick 3.0.0 documentation - 03 Getting Started

Permalink to Getting Started — Slick 3.0.0 documentation

はじめよう 

Slickを試す最も簡単方法は、Typesafe Activatorを使ってアプリケーションのテンプレートを作成することだ。以下のテンプレートはSlickのチームによって作られたものであり、Slickの新しいバージョンがリリースされる毎に更新されるだろう。

これ以外にも、他のSlickのリリースバージョンにも対応した、コミュニティにより作られたSlickのテンプレートが数多く存在する。これらのテンプレートはTypesafeのウェブサイト上の、all Slick templatesから見つける事ができる。

Adding Slick to Your Project 

Slickを既存のプロジェクトで利用するには、Maven Centralにあるライブラリを用いれば良い。sbtプロジェクトの場合、以下の記述をbuild.sbtproject/Build.scalaに追加すれば良い。

libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "|release|", "org.slf4j" %
  "slf4j-nop" % "1.6.4"
)

Mavenプロジェクトの場合<dependencies>へ以下のような記述を書き加える。Scalaのバージョンプレフィックス(_2.10_2.11)を正しく付け加える必要がある。

<dependency>
  <groupId>com.typesafe.slick</groupId>
  <artifactId>slick_2.10</artifactId>
  <version>3.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のようなロギングフレームワークをslf4j-nopの代わりに追加して欲しい。

リアクティブストリームAPIは自動的に追従的な依存で取得される。

もしコネクションプールを用いたいのなら、HikariCPの依存性を追加して欲しい。

Quick Introduction 

Note

このチャプターの残り部分は、Hello Slick templateを基にしている。Activatorからコードを手元に用意して、編集・実行しながらチュートリアルを読むと良い。

Slickを利用するには、あなたの利用するデータベースに対応したAPIのimport文を以下のように書き加える必要がある。

// H2データベースに接続するためのH2Driver
import slick.driver.H2Driver.api._
import scala.concurrent.ExecutionContext.Implicits.global

この例ではH2データベースを利用しているため、SlickのH2Driverをimportしている。ドライバのapiオブジェクトはdatabase handlingのようなSlickの一般的なAPIを含んでいる。

SlickのAPIは、分離されたスレッドプールに置いて、全て非同期でデータベース処理を実行する。DBIOAction構成内のあなたのコードやFutureの値を実行して取得するには、globalなExecutionContextをインポートする必要がある。SlickをPlayAkkaを用いた大きなアプリケーションの一部として用いる場合には、そのようなフレームワークが提供しているより良いExecutionContextを利用すべきだ。

Database Configuration 

データベースに接続する方法を指定するために、アプリケーションの中でDatabaseオブジェクトを作る必要がある。大抵の場合、Typesafe Configを用いて記述したapplication.confから、データベースコネクションの設定を行うだろう。application.confPlayAkkaでも設定を記述するために用いられている。

h2mem1 = {
  url = "jdbc:h2:mem:test1"
  driver = org.h2.Driver
  connectionPool = disabled
  keepAliveConnection = true
}

この例ではコネクションプールは用いないで、keep-alive接続をリクエストするように設定している(インメモリデータベースにコネクションプールは必要無いし、keep-aliveはデータベース利用中に接続を切らないようにするためである)。データベースオブジェクトは以下のように利用される。

val db = Database.forConfig("h2mem1")
try {
  // ...
} finally db.close

Note

Databaseオブジェクトは通常スレッドプールとコネクションプールを管理する。必要がなくなった段階で、適切にシャットダウンすべきである(JVMプロセスが終了するしないに関わらず)。

Schema 

Slickのクエリを記述する前に、テーブル毎にTableTableQueryを用いてデータベーススキーマを書く必要がある。直接手で書いても良いし、スキーマコードの生成を利用して既存のデータベーススキーマから自動生成しても良い。

// 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]
...
// COFFEESテーブルの定義
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)
  // joinなどを発行する際に用いられる外部キー
  def supplier = foreignKey("SUP_FK", supID, suppliers)(_.id)
}
val coffees = TableQuery[Coffees]

全てのカラムは名前とScalaの型が必要になる。一般的に名前はSQL側では大文字とアンダースコアで、Scala側ではcamelCaseで記述される。SQLの型はScalaの型から自動的に導出される。テーブルオブジェクトにもScalaの名前とSQLの名前とその型が必要になる。テーブルの型引数は、*射影の型と合っている必要がある。このような単純な例では、全てのカラムをタプルで表現出来るが、より複雑なマッピングも可能である。

coffeesテーブルのforeignKeyの定義は、supIDの値がsuppliersテーブルのidとして存在している事を表す制約を表現するものである。ここではn:1関係を作成している。1つのCoffeesの列に対して1つのSuppliersの列が対応しているが、複数のCoffeesの列が同じSuppliersの列を指し示す事もある。この制約はデータベースレベルで強制されるものになる。

Populating the Database 

インメモリのH2データベースエンジンへのコネクションは、空のデータベースを提供してくれる。クエリを実行する前に、データベーススキーマ(coffeessuppliersテーブルを含むもの)を作成して、テストデータを挿入してみよう。

val setup = DBIO.seq(
  // 主キーや外部キーを含むテーブルを作成
  (suppliers.schema ++ coffees.schema).create,
...
  // supplierをいくつか挿入
  suppliers += (101, "Acme, Inc.",      "99 Market Street", "Groundsville", "CA", "95199"),
  suppliers += ( 49, "Superior Coffee", "1 Party Place",    "Mendocino",    "CA", "95460"),
  suppliers += (150, "The High Ground", "100 Coffee Lane",  "Meadows",      "CA", "93966"),
  // 以下のSQLと等価
  // insert into SUPPLIERS(SUP_ID, SUP_NAME, STREET, CITY, STATE, ZIP) values (?,?,?,?,?,?)
...
  // coffeeをいくつか挿入(もしDBがサポートしてる場合にはバッチinsertが用いられる)
  coffees ++= Seq(
    ("Colombian",         101, 7.99, 0, 0),
    ("French_Roast",       49, 8.99, 0, 0),
    ("Espresso",          150, 9.99, 0, 0),
    ("Colombian_Decaf",   101, 8.99, 0, 0),
    ("French_Roast_Decaf", 49, 9.99, 0, 0)
  )
  // 以下のSQLと等価
  // insert into COFFEES(COF_NAME, SUP_ID, PRICE, SALES, TOTAL) values (?,?,?,?,?)
)
...
val setupFuture = db.run(setup)

TableQueryddlメソッドは、テーブルを作成・削除するためDDL(data definition language)オブジェクトを生成する。複数のDDL++により結合した場合には、たとえ循環依存が存在したとしても、正しい順番に作成と削除を実行する。

データの挿入には+=++=が用いられる。これはScalaのミュータブルなコレクション操作APIとよく似ている。

create+=++=といったメソッドは、データベースへの処理の後に一定時間後に結果を生成するActionを返却する。複数のActionをシーケンスに結合し、他のActionを生成するためのコンビネータが、いくつか存在する。最もシンプルな方法は、Action.seqであり、これは返り値を破棄しながら複数のActionを順に結合するものである。例として、ActionUnitを返却する場合などに用いる。準備されたActiondb.runにより実行され、Future[Unit]が生成される。

Note

データベースのコネクションとトランザクションはSlickにより自動的に管理される。デフォルトでは、auto-commitモードの際にはコネクションは都度開放される。このモードでは、外部キーの影響により、suppliersテーブルのデータをcoffeesのデータより先に挿入しなくてはならない。明示的なトランザクションブラケットで内包された処理を実行することもできる(db.run(setup.transactionally))。そのような記述を行う際には、トランザクションがコミットされる際にのみ制約が課せられるため、記述時の順序などを気にする必要はない。

Querying 

テーブルからデータをイテレートさせる最もシンプルな方法を見てみよう。

// 全てのcoffeeを読み込んで、コンソールに出力する
println("Coffees:")
db.run(coffees.result).map(_.foreach {
  case (name, supID, price, sales, total) =>
    println("  " + name + "\t" + supID + "\t" + price + "\t" + sales + "\t" + total)
})
// 以下のSQLと等価
// select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES

上の例はSELECT * FROM COFFEESというSQLと等価である(ただしこれは*がテーブルに定義された*射影と等しいためである)。ループの中で得られる型は、まぁ驚くこともなく、Coffeesの型引数と同じものになる。

基本的なクエリに対し、射影を追加してみよう。ここでは、Scalaのmapメソッドか、for式が用いて記述される。

// Why not let the database do the string conversion and concatenation?
val q1 = for(c <- coffees)
  yield LiteralColumn("  ") ++ c.name ++ "\t" ++ c.supID.asColumnOf[String] ++
    "\t" ++ c.price.asColumnOf[String] ++ "\t" ++ c.sales.asColumnOf[String] ++
    "\t" ++ c.total.asColumnOf[String]
// 最初の文字列は、自動的に`LiteralColumn`へ持ち上げられる
...
// これは以下のSQLと等価になる
// select '  ' || COF_NAME || '\t' || SUP_ID || '\t' || PRICE || '\t' SALES || '\t' TOTAL from COFFEES
...
db.stream(q1.result).foreach(println)

出力は同じで、全てのカラムがタブで区切られて結合されたものになる。異なるのは、データベースエンジン内で行われた処理のみで、結果は全く変わらないまま得られる。注意して欲しいのは、ここでは文字列結合にScalaの+オペレータは使わずに、++を用いている。また、他の型から文字列への自動的な変換は存在しない。ここでは明示的にasColumnOfを用いて変換を行っている。

Reactive Streamsでも、データベースから値をストリームとして取り出し、全ての結果を得る前に順に出力させるという処理を記述出来る。

テーブルの結合と結果のフィルタリング処理は、Scalaのコレクション操作と同様の記述で行える。

// 9.0未満のpriceとなるcoffeeから、coffeeの名前とsupplierの名前を、joinを用いて取得する
val q2 = for {
  c <- coffees if c.price < 9.0
  s <- suppliers if s.id === c.supID
} yield (c.name, s.name)
// 以下のSQLと等価
// select c.COF_NAME, s.SUP_NAME from COFFEES c, SUPPLIERS s where c.PRICE < 9.0 and s.SUP_ID = c.SUP_ID

Warning

2つの値の比較には、==の代わりに===を、!=の代わりに=!=を用いて欲しい。なぜならこれは既にAnyを基に実装されたオペレータであり、拡張することが出来ないためである。<<=>=>のような比較オペレータはそのままのものを用いる事が出来る。

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)
// 以下のSQLと等価
// select c.COF_NAME, s.SUP_NAME from COFFEES c, SUPPLIERS s where c.PRICE < 9.0 and s.SUP_ID = c.SUP_ID
Fork me on GitHub