戦略 Scala 日記

素人プログラマの思考のセンス

Hello Slick! チュートリアルで学ぶSlickの基本作法

Typesafe Activatorのチュートリアルとして用意されている、Hello Slick!のソースコードを元に、Slickの基本的な使い方を紹介する。

activator uiコマンドを実行し、ウェブブラウザに表示されたチュートリアルの一覧の中から、hello-slickを選択すると、ソースコードが展開される。これを元に解説を進める。

このチュートリアルで利用するファイル

  • CaseClassMapping.scala
  • HelloSclick
  • InvokerMethods
  • Tables.scala

メインスレッドはHelloSlickになるので、まずは一連の処理内容を確認する。

まず、DB接続用のドライバをインポートする。 JDBCの代わりになるようなものと思って良い。

import scala.slick.driver.H2Driver.simple._

Slickでは、テーブルのスキーマに対してクエリを実行するためのインタフェースとしてTableQueryというクラスを用意している。 実際のクエリを発行するには、このTableQueryを生成する。

例として、SuppliersCoffeesというテーブルへのアクセスを準備しよう。

val suppliers: TableQuery[Suppliers] = TableQuery[Suppliers]
val coffees: TableQuery[Coffees] = TableQuery[Coffees]

ここで型引数として渡しているSuppliersCoffeesTables.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としているのは、listheadメソッドが、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のコレクションフレームワークに用意されているfiltermapforeachなどのメソッドを利用するイメージである。

// 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をかけて、タプルをパターンマッチすることで、その内容を取得する事もできる。 mapfileterなどのコレクション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)
}