Scalaのslick3.1を使ったメモ

概要

Scalaでpostgresqlを使う用事があったのでslickを利用して書いてみる。

これを書いてる時点の最新版である3.1.0-M2を使ってみたら、以前試したバージョン(1.0.1)とはだいぶ書き方が変わっていたので、基本的な使い方を実践してメモを残しておく。

利用バージョンは下記。相変わらずsbtではなくtypsafe activator + eclipseで開発している。

@createdAt: 2015/08/18
@versions scala 2.11, slick 3.1.0-M2, typesafe activator 1.3.5-minimal, postgresql 9.3.9

広告枠

準備

build.sbtにdepencencyを設定。

libraryDependencies ++= Seq(
    "org.scalatest" %% "scalatest" % "2.2.4" % "test",
    "com.jsuereth" %% "scala-arm" % "1.4",
    "com.typesafe.slick" %% "slick" % "3.1.0-M2",
    "com.typesafe.slick" %% "slick-codegen" % "3.1.0-M2",
    "com.typesafe" % "config" % "1.3.0",
    "postgresql" % "postgresql" % "9.1-901-1.jdbc4"
)

slick本体slick-codegen(データベースの内容からslickのコードを生成するヤツ)を入れている。またDBは今回はpostgresqlを使用するので、それ用のjdbcのjarも追加。

typesafe-configは設定を記述するライブラリで、DBの情報を記載する際に利用。

scala-armも入れてる。slickとは関連はないけど、closeableなインスタンスに対してauto close的なことをできる。

code generateしてみる

既に当該データベースにテーブルが作成されている場合、slick-codegenでDBのスキーマからモデルのコードを自動生成できる。

下記コードで生成できた。

package example

object CodeGen extends App {
  val slickDriver = "slick.driver.PostgresDriver"
  val jdbcDriver = "org.postgresql.Driver"
  val url = "jdbc:postgresql://localhost/example_database"
  val outputDir = "src/main/scala"
  val pkg = "example"
  val user = "scott"
  val password = "tiger"

  slick.codegen.SourceCodeGenerator.main(
    Array(slickDriver, jdbcDriver, url, outputDir, pkg, user, password))
}

上記コードを実行した場合、src/mai/scala/example の下にコードが生成される。

いろんなテーブルに対して実行してみたところ、一部スキーマで postgresql の default value のパースに失敗してエラーが出た。postgresqlのdefaultに記述される内容はけっこう面倒そうなのでそういうこともあるのだろう。一般的なスキーマであれば特に問題なく生成できた。

SQLを実行する

自動生成したモデルのコードは使わず、生のSQLを書いて実行してみる。

先にresource 配下に application.conf を置いてDBの設定を記述しておく。

pg_database = {
  url = "jdbc:postgresql://localhost/example_database?user=scott&password=tiger"
  driver = org.postgresql.Driver
  connectionPool = disabled
  keepAliveConnection = true
}

これを使って生のSQLを投げてみる。

事前に id(int) と name(string) という2つのカラムを持つ example というテーブルが生成されているものとする。

package example

import scala.concurrent.Await
import scala.concurrent.duration.Duration
import slick.driver.PostgresDriver.api._

object SelectExample extends App {
  val db = Database.forConfig("pg_database")
  val query = sql"SELECT id, name FROM examples".as[(Int, String)]
  val f = db.run(query)
  Await.result(f, Duration.Inf) foreach println
}

上記を実行するとクエリの結果が全行標準出力される。この場合の Await.result に対する戻り値は Vector[(Int, String)] になる。

Tuple[(Int, String)] で返ってくるのはイマイチなので、GetResult で bind して case class に入れてみる。

case class Example(id: Int, name: String)
import slick.jdbc.GetResult
implicit val getResult = GetResult(r => Example(r.nextInt, r.nextString))

val db = Database.forConfig("pg_database")
val query = sql"SELECT id, name FROM examples".as[Example]
Await.result(db.run(query), Duration.Inf) foreach println

これで Vector[Example] を結果として取得することができる。

生SQLでのSELECTはこの記述で概ねストレスなく書けそう。

INSERT もしてみる。

INSERT等の更新系クエリを流す際は、sql"..." ではなく sqlu"..." を使うらしい。下記例では scala-arm の managed で Database を最後に close する処理も入れている。

for (db <- managed(Database.forConfig("pg_database"))) {
  val id = 27
  val name = "name27"
  val query = sqlu"INSERT INTO example (id, name) VALUES (${id}, ${name})"
  Await.result(db.run(query, Duration.Inf)
}

SELECTの場合でも使える記述だが、上記の ${id} のように変数名をそのまま埋め込め、また文字列であれば勝手にサニタイズ的なことも行われる。便利。

DELETEもしておく。

for (db <- managed(Database.forConfig("pg_database"))) {
  val id = 27
  val query = sqlu"DELETE FROM ssps WHERE id=${id}"
  Await.result( db.run(query), Duration.Inf)
}

モデルを作る

slick-codegenによる自動生成ではなく、自前でモデルクラスを書いてみる。classと一緒にTableQueryを作っておくと、filter とかScalaっぽい記述でDBの検索が手軽に書ける。

package example

import scala.concurrent.Await
import scala.concurrent.duration.Duration

import slick.driver.PostgresDriver.api._

case class Example(id: Int, name: String)

/** Model */
class Examples(tag: Tag) extends Table[Example](tag, "examples") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def * = (id, name) <> (Example.tupled, Example.unapply)
}

/** TableQuery */
object Examples extends TableQuery(new Examples(_))

/** SELECTの実行 */
object SelectExample extends App {
  val db = Database.forConfig("pg_database")
  val f = db.run(Examples.filter { _.id === 1 }.result)
  Await.result(f, Duration.Inf) foreach println
}

モデルからのINSERT/UPDATE/DELETE

INSERT, 複数行INSERTを実行する。その後、結果をSELECTして確認する。

import scala.concurrent.Await
import scala.concurrent.duration.Duration

import slick.driver.PostgresDriver.api._

case class Example(id: Int, name: String)

/** Model */
class Examples(tag: Tag) extends Table[Example](tag, "examples") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def * = (id, name) <> (Example.tupled, Example.unapply)
}

/** TableQuery */
object Examples extends TableQuery(new Examples(_))

object Main extends App {
  val db = Database.forConfig("pg_database")

  // INSERT
  val insert1 = db.run(Examples += Example(1, "name1"))
  val intertResult1 = Await.result(insert1, Duration("10s"))
  println(intertResult1)

  // 複数行INSERT
  val insert2 = db.run(Examples ++= Seq(Example(2, "name2"), Example(3, "name3")))
  val intertResult2 = Await.result(insert2, Duration("10s"))
  println(intertResult2)

  // 結果確認(id=1だけ)
  val select1 = db.run(Examples.filter { _.id === 1 }.result.head)
  val selectResult1 = Await.result(select1, Duration("10s"))
  println(selectResult1)
    #=> Example(1,name1)

  // 結果確認(id in (1, 2, 3))
  val select2 = db.run(Examples.filter { _.id.inSet(List(1, 2, 3)) }.result)
  val selectResult2 = Await.result(select2, Duration("10s"))
  println(selectResult2)
    #=> Vector(Example(1,name1), Example(2,name2), Example(3,name3))
}

UPDATEする。

  val db = Database.forConfig("pg_database")

  // id = 1の結果を
  val update = Examples.filter { _.id === 1 }.map(_.name).update("updated name1")
  Await.result(db.run(update), Duration("10s"))

  val select = db.run(Examples.filter { _.id === 1 }.result.head)
  val selectResult = Await.result(select, Duration("10s"))
  println(selectResult)
    #=> Example(1,updated name1)

nameが書き換えられていることがわかる。

DELETEも同じように実行できる。

  val db = Database.forConfig("pg_database")

  // id = 1の結果をdelete
  val delete = Examples.filter { _.id === 1 }
  Await.result(db.run(delete.delete), Duration("10s"))

  val select = db.run(Examples.filter { _.id === 1 }.result)
  val selectResult = Await.result(select, Duration("10s"))
  println(selectResult)
    #=> Vector()

UPSERT(MERGE INTO)する

TableQueryにinsertOrUpdateが用意されているのでこれを使う。PrimaryKeyに対してSELECTを実行し、結果の有無でINSERTとUPDATEを切り分ける。またDBMS側でupsert的なものが用意されている場合はそちらを使う。

  val upsert = Examples.insertOrUpdate(Example(10, "name10"))
  Await.result(db.run(upsert), Duration("10s"))

ちなみにPrimaryKeyがないテーブル(DBにはなくても良いけど、SlickのModel側には書かれていないといけない)に対して実行するとエラーになった。見たところWHERE句が空の状態でクエリが生成されてそのまま発効してしまっているようだ。