quill で DDL を実行

quillScala 用の DB ライブラリで、マクロを使ってコンパイル時に SQL や CQL(Cassandra)を組み立てるのが特徴となっています。

quill には Infix という機能が用意されており、これを使うと FOR UPDATE のような(quillが)未サポートの SQL 構文に対応したり、select 文を直接指定したりできるようですが、CREATE TABLE のような DDL(データ定義言語)の実行は無理そうでした。

そこで、API やソースを調べてみたところ、SQL を直接実行する probeexecuteAction という関数を見つけたので、これを使って CREATE TABLE を実行してみたいと思います。

ソースは http://github.com/fits/try_samples/tree/master/blog/20180502/

はじめに

今回は Gradle を使ってビルド・実行し、DB には H2 を(インメモリーで)使います。

build.gradle
apply plugin: 'scala'
apply plugin: 'application'

mainClassName = 'sample.SampleApp'

repositories {
    jcenter()
}

dependencies {
    compile 'org.scala-lang:scala-library:2.12.6'
    compile 'io.getquill:quill-jdbc_2.12:2.4.2'

    runtime 'com.h2database:h2:1.4.192'
    runtime 'org.slf4j:slf4j-simple:1.8.0-beta2'
}

DB の接続設定は以下のようにしました。

ctx の部分は任意の文字列を用いることができ、H2JdbcContext を new する際の configPrefix 引数で指定します。

src/main/resources/application.conf
ctx.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
ctx.dataSource.url="jdbc:h2:mem:sample"
ctx.dataSource.user=sa

1. probe・executeAction で DDL を実行

それでは、probeexecuteAction をそれぞれ使って CREATE TABLE を実行してみます。

JdbcContext における probe の戻り値は Try[Boolean]executeAction の戻り値は Long となっています。

sample1/src/main/scala/sample/SampleApp.scala
package sample

import io.getquill.{H2JdbcContext, SnakeCase}

case class Item(itemId: String, name: String)
case class Stock(stockId: String, itemId: String, qty: Int)

object SampleApp extends App {
  lazy val ctx = new H2JdbcContext(SnakeCase, "ctx")

  import ctx._

  // probe を使った CREATE TABLE の実行
  val r1 = probe("CREATE TABLE item(item_id VARCHAR(10) PRIMARY KEY, name VARCHAR(10))")
  println(s"create table1: $r1")

  // executeAction を使った CREATE TABLE の実行
  val r2 = executeAction("CREATE TABLE stock(stock_id VARCHAR(10) PRIMARY KEY, item_id VARCHAR(10), qty INT)")
  println(s"create table2: $r2")

  // item への insert
  println( run(query[Item].insert(lift(Item("item1", "A1")))) )
  println( run(query[Item].insert(lift(Item("item2", "B2")))) )

  // stock への insert
  println( run(query[Stock].insert(lift(Stock("stock1", "item1", 5)))) )
  println( run(query[Stock].insert(lift(Stock("stock2", "item2", 3)))) )

  // item の select
  println( run(query[Item]) )
  // stock の select
  println( run(query[Stock]) )

  // Infix を使った select
  val selectStocks = quote(
    infix"""SELECT stock_id AS "_1", name AS "_2", qty AS "_3"
            FROM stock s join item i on i.item_id = s.item_id""".as[Query[(String, String, Int)]]
  )
  println( run(selectStocks) )
}

実行結果は以下の通りで CREATE TABLE に成功しています。 probe の結果は Success(false) で executeAction の結果は 0 となりました。

実行結果
> cd sample1
> gradle run

・・・
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.

create table1: Success(false)
create table2: 0
1
1
1
1
List(Item(item1,A1), Item(item2,B2))
List(Stock(stock1,item1,5), Stock(stock2,item2,3))
List((stock1,A1,5), (stock2,B2,3))

・・・

2. モナドの利用

quill には IO モナドが用意されていたので、これを使って処理を組み立ててみます。

IO は run の代わりに runIO を使う事で取得でき、IO の結果は performIO で取得します。

probe の結果である Try[A]IO.fromTry を使う事で IO にできます。

また、クエリー query[A] では flatMap 等が使えるので for 内包表記で直接合成できましたが(selectStocks の箇所)、query[A].insert(・・・) は flatMap 等を使えなかったので runIO しています。(insertItemAndStock の箇所)

sample2/src/main/scala/sample/SampleApp.scala
package sample

import io.getquill.{H2JdbcContext, SnakeCase}

case class Item(itemId: String, name: String)
case class Stock(stockId: String, itemId: String, qty: Int)

object SampleApp extends App {
  lazy val ctx = new H2JdbcContext(SnakeCase, "ctx")

  import ctx._

  // CREATE TABLE
  val createTables = for {
    it <- probe("CREATE TABLE item(item_id VARCHAR(10) PRIMARY KEY, name VARCHAR(10))")
    st <- probe("CREATE TABLE stock(stock_id VARCHAR(10) PRIMARY KEY, item_id VARCHAR(10), qty INT)")
  } yield (it, st)

  // item と stock へ insert
  val insertItemAndStock = (itemId: String, name: String, stockId: String, qty: Int) => for {
    _ <- runIO( query[Item].insert(lift(Item(itemId, name))) )
    _ <- runIO( query[Stock].insert(lift(Stock(stockId, itemId, qty))) )
  } yield ()

  // stock と item の select(stock と該当する item をタプル化)
  val selectStocks = quote {
    for {
      s <- query[Stock]
      i <- query[Item] if i.itemId == s.itemId
    } yield (i, s)
  }

  // 処理の合成
  val proc = for {
    r1 <- IO.fromTry(createTables)
    _ <- insertItemAndStock("item1", "A1", "stock1", 5)
    _ <- insertItemAndStock("item2", "B2", "stock2", 3)
    r2 <- runIO(selectStocks)
  } yield (r1, r2)

  // 結果
  println( performIO(proc) )
  // トランザクションを適用する場合は以下のようにする
  //println( performIO(proc.transactional) )
}
実行結果
> cd sample2
> gradle run

・・・
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.

((false,false),List((Item(item1,A1),Stock(stock1,item1,5)), (Item(item2,B2),Stock(stock2,item2,3))))

・・・