Hello Slick! チュートリアルで学ぶSlickの基本作法
Typesafe Activatorのチュートリアルとして用意されている、Hello Slick!のソースコードを元に、Slickの基本的な使い方を紹介する。
- Hello Slick! - Typesafe Activator | Typesafe http://typesafe.com/activator/template/hello-slick
activator ui
コマンドを実行し、ウェブブラウザに表示されたチュートリアルの一覧の中から、hello-slick
を選択すると、ソースコードが展開される。これを元に解説を進める。
このチュートリアルで利用するファイル
メインスレッドはHelloSlick
になるので、まずは一連の処理内容を確認する。
まず、DB接続用のドライバをインポートする。 JDBCの代わりになるようなものと思って良い。
import scala.slick.driver.H2Driver.simple._
Slickでは、テーブルのスキーマに対してクエリを実行するためのインタフェースとしてTableQuery
というクラスを用意している。
実際のクエリを発行するには、このTableQuery
を生成する。
例として、Suppliers
とCoffees
というテーブルへのアクセスを準備しよう。
val suppliers: TableQuery[Suppliers] = TableQuery[Suppliers] val coffees: TableQuery[Coffees] = TableQuery[Coffees]
ここで型引数として渡しているSuppliers
とCoffees
はTables.scala
のなかで、Table
として定義している。
次に、DBに接続部分。
h2データベースのアドレスをjdbc:h2:mem:hello
として設定している。
ドライバとしてorg.h2.Driver
を使うことを明示的に示している。
val db = Database.forURL("jdbc:h2:mem:hello", driver = "org.h2.Driver")
このデータベースに対して、実際にクエリを投げる部分がここから先の部分である。
withSession
のブロックに実際のクエリ生成と、実行の処理を記述する。
また、implicit session
としているのは、list
やhead
メソッドが、sessionを暗黙の引数として受け取るため、ここでimplicit
キーワードを付けている。
db.withSession { implicit session => // 最後まで続く }
定義済のテーブル(データ構造)をDB上に生成する。
ここで見慣れない演算子として、++
というddlが作るクラスのメソッドを利用している。
(suppliers.ddl ++ coffees.ddl).create
下記が、行を追加するSlick特有の構文になる。
suppliers += (100, "String1", "String String1") suppliers += (200, "String2", "String String2")
このように、TableQueryのインスタンスに対して、タプルを渡している。 これで新たに行が追加される。
また、JDBCのバッチ挿入機能を利用する場合は下記のように、タプルをSeqにまとめて これを連結することで、クエリが実行される。
val coffeesInsertResult: Option[Int] = coffees ++= Seq ( ("Colombian", 101, 7.99, 0, 0), ("French_Roast", 49, 8.99, 0, 0), // (...) )
追加された行数を取得する場合は、次のようにします。
結果はOption
で返ってくるため、foreachを利用している。
coffeesInsertResult foreach { numRows => println(s"Inserted $numRows rows into the Coffees table") }
クエリの作成と実行
ここからは実際にクエリを発行する処理を見ていくが、クエリの作成の流れは次のようなイメージである。
collection_api
の部分にはScalaのコレクションフレームワークに用意されているfilter
、map
、foreach
などのメソッドを利用するイメージである。
// Selectを発行し結果を得る val result = table.collection_api.list // クエリを文字列として取得する val query = table.collection_api.selectStatement // クエリをクラスとして取得する val query = table.collection_api // 例えば val query = table.filter(_ => _)
SELECT
次に、SELECTを発行するやり方を確認する。 例えば、テーブルから全件を取得するような処理はどうしたらよいか。
次のように、カラムの値のタプルをリストで受け取る変数を用意し、TableQuery[Suppliers]
として定義しているテーブルsuppliers
に対して、list
メソッドを実行する。
val allSuppliers: List[(Int, String, String, String, String, String)] = suppliers.list
例えば、Coffeesテーブルに対して全件を取得するクエリを確認する方法は下記のようになる。
println("Generated SQL for base Coffees query:¥n" + coffees.selectStatement)
db.withSession
の中では、TableQuery[A]
で生成しているテーブル(ここではTableQuery[Coffees]
)は、Seq[Tuple]のようなものなので、foreachをかけて、タプルをパターンマッチすることで、その内容を取得する事もできる。
map
やfileter
などのコレクションAPIを適用するとクエリを取得するがforeach
はIO処理というイメージなので、直感的である。
イメージとしては、レコードはひとつのタプルでまとまっていると考えて良い。 INSERTの時もタプルを利用したことを思い出すと、合点がいく。
coffees foreach { case (name, supID, price, sales, total) => // println(name + " " + supID + " " + ...) }
あたかもDBのテーブルが、コレクションとして存在しているかのようにこれらを利用できるところが、Slickの利点である。
SELECT WHERE
次に条件を指定し、WHERE句を用いたSELECT文を発行する方法を確認する。
コレクションAPIを使う場合は、実行結果ではなくクエリのクラスが返ってくることに注意したい。 コレクションAPIの実行結果としてコレクションが返ってくるようなイメージである。
val filterQuery: Query[Coffees, (String, Int, Double, Int, Int), Seq] = coffees.filter(_.price > 9.0)
SQL文を確認する。
println("Generated SQL for filter query:¥n" + filterQuery.selectStatement)
実際のクエリは下記のようになる。
select x2."COF_NAME", x2."SUP_ID", x2."PRICE", x2."SALES", x2."TOTAL" from "COFFEES" x2 where x2."PRICE" > 9.0
実行結果リストが欲しい場合は最後にlist
を実行する必要がある。
println(filterQuery.list) // List((Espresso,150,9.99,0,0), (French_Roast_Decaf,49,9.99,0,0))
UPDATE
同じような要領で、Update文を作成し、実行する。
例えば、sales
の項目をいっせいにアップデートするようなクエリを作成するには次のようにする。
val updateQuery: Query[column[Int], Int, Seq] = coffees.map(_.sales) println("Generated SQL for Coffees update:¥n" + updateQuery.updateStatement) // update "COFFEES" set "SALES" = ? val numUpdateRows = updateQuery.update(1) println(s"Updated $numUpdatedRows rows") // Updated 5 rows
DELETE
レコードの削除をかけるDELETE文を発行する処理。
val deleteQuery: Query[Coffees,(String,Int,Double,Int,Int),Seq] = coffees.filter(_.price < 8.0) println("Generated SQL for Coffees delete:¥n" + deleteQuery.deleteStatement) // delete from "COFFEES" where "COFFEES"."PRICE" < 8.0 val numDeletedRows = deleteQuery.delete println(s"Deleted $numDeletedRows rows") // Deleted 1 rows
特定のカラムを取得するSELECT
結果として得られるリストは、例えばカラムがStringの場合はList[Sting]となる。
val justNameQuery: Query[Column[String], String, Seq] = coffees.map(_.name) println("Generated SQL for query returning just the name:¥n" + justNameQuery.selectStatement) // select x2."COF_NAME" from "COFFEES" x2 println(justNameQuery.list) // List(French_Roast, Espresso, Colombian_Decaf, French_Roast_Decaf)
ORDER BY
ORDER BYによるソート結果を返す処理
val sortByPriceQuery: Query[Coffees,(String,Int,Double,Int,Int),Seq] = coffees.sortBy(_.price) println("Generated SQL for query sorted by price¥n" + sortByPriceQuery.selectStatement) // select x2."COF_NAME", x2."SUP_ID", x2."PRICE", x2."SALES", x2."TOTAL" from "COFFEES" x2 order by x2."PRICE" println(sortByPriceQuery.list) // List((French_Roast,49,8.99,1,0), (Colombian_Decaf,101,8.99,1,0), (Espresso,150,9.99,1,0), (French_Roast_Decaf,49,9.99,1,0)) G
SELECT文の結合(UNION)
クエリを合成する。つまり、複数の条件をメソッドチェーンでつなぐ。
val composedQuery: Query[Column[String], String, Seq] = coffees.sortBy(_.name).take(3).filter(_.price > 9.0).map(_.name) println(composedQuery.selectStatement) // select x2."COF_NAME" from (select x3."TOTAL" as "TOTAL", x3."COF_NAME" as "COF_NAME", x3."PRICE" as "PRICE", x3."SUP_ID" as "SUP_ID", x3."SALES" as "SALES" from "COFFEES" x3 order by x3."COF_NAME" limit 3) x2 where x2."PRICE" > 9.0 println(composedQuery.list) // List(Espresso)
この例の場合では、filter(_.price > 9.0).map(_.map)
以外の実行結果に対して、再度SELECT文を投げるクエリが作られる。
JOIN
JOIN句の生成には、for内包表記を利用する。 (実際に実行されるクエリはJOIN句を利用しないクエリになっている)
ここで、c.supplier
としているが、これはTable.scala
で定義しているCoffees
テーブルに事前に外部キーとして記述している。
class Coffees(tag: Tag) extends Table[(String, Int, ...)](tag, "COFEES") { // カラム定義 def supID: Column[Int] = column[Int]("SUP_ID") // ... def supplier: ForeignKeyQuery[Suppliers, (Int, String, ...)] = foreignKey("SUP_FK", supID, TableQuery[Suppliers])(_.id) }
この外部キーの設定を事前に定義した後に、JOIN句の生成を下記のように行う。
val joinQuery: Query[(Column[String],Column[String]), (String, String), Seq] = for { c <- coffees if c.price > 9.0 s <- c.supplier } yield (c.name, s.name) println("Generated SQL for the join query:¥n + joinQuery.selectStatement") // select x2."COF_NAME", x3."SUP_NAME" from "COFFEES" x2, "SUPPLIERS" x3 where (x2."PRICE" > 9.0) and (x3."SUP_ID" = x2."SUP_ID") println(joinQuery.list) // List((Espresso,The High Ground), (French_Roast_Decaf,Superior Coffee))
計算を含むクエリを作成する
計算を含むクエリを作成する場合は、list
ではなく、run
メソッドを実行して結果を得る。
結果はOption
で返ってくるのが基本となる。
val maxPriceColumn: Column[Option[Double]] = coffees.map(_.price).max println("Generated SQL for max price column:¥n" + maxPriceColumn.selectStatement) // select x2.x3 from (select max(x4.x5) as x3 from (select x6."PRICE" as x5 from "COFFEES" x6) x4) x2 println(maxPriceColumn.run) Some(9.99)
SQL文を利用する
最後に、SQL文を直接記述するケースを扱う。 JDBCのStaticQueryをライブラリとして読み込む必要がある。
import scala.slick.jdbc.StaticQuery.interpolation
このように、クエリの中に変数をおき、動的にクエリを作ることもできる。
val stete = "CA" val plainQuery = sql"select SUP_NAME from SUPPLIERS where STATE = $state".as[String] println(plainQuery.getStatement) // select SUP_NAME from SUPPLIERS where STATE = ? println(plainQuery.list) // List(Acme, Inc., Superior Coffee, The High Ground)
さてここまで、クエリの作成と実行方法を先に確認してきた。 その実行対象となるテーブルはどのように定義されているのかを次に確認したい。
Table定義
Tables.scala
を参照。
インポートが必要なライブラリは下記。
import scala.slick.driver.H2Driver.simple._ import scala.slick.lifted.{ProvenShape, ForeignKeyQuery}
基本構造としては、Tableクラスに渡す型が、カラムの型をタプルにしたものとなる。 また、Tableに対して、tagを渡すこともどうじに行う。 実装は、各カラム名をつけたメソッドを用意し、columnのインスタンスを返すようにする。
*
は、すべてのカラムを関数として持つタプルを返すのだが、
この時返る型は、ProvenShape
となるようだ。
columnはファクトリメソッドである。
はじめに、SUPPLIERS
テーブルを生成するコードは次のようになる。
カラムは"id, name, street, city, state, zip"の6つである。
class Suppliers(tag: Tag) extends Table[(Int, String, String, String, String, String)](tag, "SUPPLIERS") { def id: Column[Int] = column[Int]("SUP_ID") def name: Column[String] = column[String]("SUP_NAME") def street: Column[String] = column[String]("STREET") def city: Column[String] = column[String]("CITY") def state: Column[String] = column[String]("STATE") def zip: Column[String] = column[String]("ZIP") def * : ProvenShape[(Int, String, String, String, String, String)] = (id, name, street, city, state, zip) }
先ほどJOIN句の生成で利用した、外部キーの設定もここで行う。 外部キーの生成は次のような記法を用いる。
class Coffees(tag: Tag) extends Table[(String, Int, Double, Int, Int)](tag, "COFFEES") { def name: Column[String] = column[String]("COF_NAME", O.PrimaryKey) def supID: Column[Int] = column[Int]("SUP_ID") def price: Column[Double] = column[Double]("PRICE") def sales: Column[Int] = column[Int]("SALES") def total: Column[Int] = column[Int]("TOTAL") def * : ProvenShape[(String, Int, Double, Int, Int)] = (name, supID, price, sales, total) def supplier: ForeignKeyQuery[Suppliers, (Int, String, String, String, String, String)] = foreignKey("SUP_FK", supID, TableQuery[Suppliers])(_.id) }