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から見つける事ができる。
Slickを既存のプロジェクトで利用するには、Maven Centralにあるライブラリを用いれば良い。sbtプロジェクトの場合、以下の記述をbuild.sbt
やproject/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の依存性を追加して欲しい。
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をPlayやAkkaを用いた大きなアプリケーションの一部として用いる場合には、そのようなフレームワークが提供しているより良いExecutionContext
を利用すべきだ。
データベースに接続する方法を指定するために、アプリケーションの中でDatabase
オブジェクトを作る必要がある。大抵の場合、Typesafe Configを用いて記述したapplication.conf
から、データベースコネクションの設定を行うだろう。application.conf
はPlayやAkkaでも設定を記述するために用いられている。
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プロセスが終了するしないに関わらず)。
Slickのクエリを記述する前に、テーブル毎にTable
とTableQuery
を用いてデータベーススキーマを書く必要がある。直接手で書いても良いし、スキーマコードの生成を利用して既存のデータベーススキーマから自動生成しても良い。
// 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
の列を指し示す事もある。この制約はデータベースレベルで強制されるものになる。
インメモリのH2データベースエンジンへのコネクションは、空のデータベースを提供してくれる。クエリを実行する前に、データベーススキーマ(coffees
とsuppliers
テーブルを含むもの)を作成して、テストデータを挿入してみよう。
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)
TableQuery
のddl
メソッドは、テーブルを作成・削除するためDDL
(data definition language)オブジェクトを生成する。複数のDDL
を++
により結合した場合には、たとえ循環依存が存在したとしても、正しい順番に作成と削除を実行する。
データの挿入には+=
や++=
が用いられる。これはScalaのミュータブルなコレクション操作APIとよく似ている。
create
、+=
、++=
といったメソッドは、データベースへの処理の後に一定時間後に結果を生成するAction
を返却する。複数のAction
をシーケンスに結合し、他のAction
を生成するためのコンビネータが、いくつか存在する。最もシンプルな方法は、Action.seq
であり、これは返り値を破棄しながら複数のAction
を順に結合するものである。例として、Action
がUnit
を返却する場合などに用いる。準備されたAction
はdb.run
により実行され、Future[Unit]
が生成される。
Note
データベースのコネクションとトランザクションはSlickにより自動的に管理される。デフォルトでは、auto-commitモードの際にはコネクションは都度開放される。このモードでは、外部キーの影響により、
suppliers
テーブルのデータをcoffees
のデータより先に挿入しなくてはならない。明示的なトランザクションブラケットで内包された処理を実行することもできる(db.run(setup.transactionally)
)。そのような記述を行う際には、トランザクションがコミットされる際にのみ制約が課せられるため、記述時の順序などを気にする必要はない。
テーブルからデータをイテレートさせる最もシンプルな方法を見てみよう。
// 全ての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