Subscribed unsubscribe Subscribe Subscribe

Slick ガイド

Scala

この記事は Play or Scala Advent Calendar 2012 の 4日目です

去年 ScalaQuery の記事を書いたのですが、今年はその後継、Slick です。

Slick とは?

Slick とは Typesafe stack に名を連ねる Typesafe 社お墨付きの ORM です。以前は ScalaQuery と呼ばれていたものが、Slick と名称を変更し、開発が続けられています。現在の最新は 0.11.2 です。
Slick は Scala 2.10 をベースに開発されていて、マクロなどの新機能も利用されています。現時点ではORMとしてデファクトスタンダードではなく、Squeryl と人気を二分している状態です。

Slick を使うメリット・デメリット

Slick のメリットはなんといってもかっこいいシンタックスだと思います。Scala のコレクションのように DB テーブルを扱うことができます。
WHERE や JOIN などの書きやすさには感動します。


非常に直感的なシンタックスで素晴らしいのですが、残念なことにTupleベースの実装なので、おなじみの「Tuple23問題」が発生します。
つまり23以上のカラムのテーブルがうまく扱えません。これを理由に採用を見送られることが多いです。
23以上になってしまった場合は、

  • テーブルを分割する
  • タプルをネストさせる
  • 必要なカラムだけをクラス定義に含める

などの(どれも面倒な)回避策があります。ただしこれらは SELECT についてであり、 INSERT はおそらく回避できません。少なくとも私は見つけられませんでした。
まあそこだけ別ライブラリ使うとか、 jdbc 直接叩くとかになるんだと思います。


2013/01/27 追記
一応 INSERT もできるらしいです。
https://github.com/slick/slick/commit/7f8668e49da34cf0cb52ac21a6f5b46d0b0fdb47


ScalaQuery と Slick の違い

ScalaQuery(0.10.0-M1まで) が 2.9 用で
Slick(0.10.0-M2から) が 2.10 用
と思えば良いです。
ScalaQuery は 0.10.0-M1 という中途半端なバージョンで終わっていますが、
Slick のコードを見ると、Scala 2.10 からの機能が多用されていますし、この先はないでしょう。


開発状況はなかなか活発ですが、Slick になってからで特に良いと思ったのは

です。順に説明します。

import を書くのが簡単になった

ScalaQuery ではこんな import を書いていました。

import org.scalaquery._
import session.{ Database, Session }
import ql._
import basic.BasicDriver.Implicit._
import basic.{ BasicTable => Table }


これが Slick では Driver#simple という import 用のオブジェクトが用意されているので、一行で済みます。

import scala.slick.driver.PostgresDriver.simple._
INSERT時に自動採番されたIDを返せるようになった

0.11.1 から追加された機能です。
returning を使って insert 用のオブジェクトを返す ins というメソッドを定義しています。

object Suppliers extends Table[Supplier]("SUPPLIER") {
  def id     = column[Int   ]("SUP_ID", O.PrimaryKey, O.AutoInc)
  def name   = column[String]("SUP_NAME")
  def city   = column[String]("CITY")
  def * = id ~ name ~ city <> (Supplier, Supplier.unapply _)
  def ins = name ~ city returning id
}

ins#insert, ins#insertAll を使うと insert した値が返ってきます。
(というかこんな機能も今までなかったのか)

 val Seq(sup1, sup2, sup3) = Suppliers.ins.insertAll(
      ("Acme, Inc.",      "Groundsville"),
      ("Superior Coffee", "Mendocino"),
      ("The High Ground", "Meadows")
    )

String interpolation API

Scala 2.10.0 より入る文字列埋め込み機能が使われています。
PlainSQL が簡潔に書けます。

def coffeeByName(name: String) = sql"select * from coffees where name = $name".as[Coffee]


あれ、これ SQL injection は大丈夫なの?という方、大丈夫なんです。
String interpolation の正体は単純な文字列埋め込みではなく、 StringContext というものに変換する仕組みなので。
テキトーですが、以前作ったスライドがあるのでそちらをご覧ください。
http://tototoshi.github.com/slides/rpscala-83-string-interpolation/


Slick リファレンス

というわけで 本題の Slick リファレンスです。

以前ブログに書いてたものとダブるところも多いです。

http://d.hatena.ne.jp/tototoshi/searchdiary?word=scalaquery

build.sbt

CrossVersion については sbt 0.12.x から導入された binary version について - tototoshiの日記 を見て下さい。

scalaVersion := "2.10.0-RC1"

libraryDependencies ++= Seq(
  "com.typesafe" %% "slick" % "0.11.2" cross CrossVersion.full,
  "postgresql" % "postgresql" % "9.1-901.jdbc4",
)

import

PostgreSQL の場合

import scala.slick.driver.PostgresDriver.simple._
import scala.language.postfixOps

import scala.language.postfixOps は obj method のような書き方をするときに必要です。
2.10 からこれがないと警告が出ます。うざ...

テーブル定義

テーブルは Table[(カラムの型)]("テーブル名") カラムは column[カラムの型]("カラム名") という書き方をします。カラムにはオプションを指定することもできて、標準で PrimaryKey や AutoInc などが用意されています。それ意外のオプション、例えば文字の長さやユニーク制約は O DBType "..." で指定できます。

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

object Instruments extends Table[Instrument]("instruments") {
  def id = column[Int]("id", O PrimaryKey)
  def name = column[String]("name", O DBType "varchar(10)")
  def * = id ~ name <> (Instrument.apply _ , Instrument.unapply _)
}

case class Member(id: Int, firstName: String, lastName: String, instrumentId: Int)

object Members extends Table[Member]("members") {
  def id = column[Int]("id", O PrimaryKey, O AutoInc)
  // 文字サイズの指定や unique 制約は O DBType を使う。
  def firstName = column[String]("first_name", O DBType "varchar(20)")
  def lastName = column[String]("last_name", O DBType "varchar(20)")
  def instrumentId = column[Int]("instrument_id")
  // case class へのマッピング
  def * = id ~ firstName ~ lastName ~ instrumentId <> (Member.apply _, Member.unapply _)
  // AutoIncrement なカラムを除外
  // returing の後に続けてどんな値を返すか指定できる
  // 例えば * なら Member オブジェクトを返すし、id のみでよければ id を指定する
  def ins = firstName ~ lastName ~ instrumentId returning *
  // 外部キーの設定もできる
  def instrument = foreignKey("fk_instrument", instrumentId, Instruments)(_.id)
}

Slickのサンプルでは、テーブルを Tuple にマッピングする例が多いですが、class、特に case class などにマッピングするのが扱いやすいでしょう。class へのマッピングは <> で指定します。

ins というのは insert 用のオブジェクトで、 insert 時の自動採番に対応するためのものです。

データベースへの接続

データベースへの接続は Database オブジェクトを経由して行われます。
一番シンプルなやりかたは forURL で jdbc URL を指定する方法です。

val db = Database.forURL("jdbc:postgresql://localhost:5432/beatles", driver = "org.postgresql.Driver", user = "xxx", password = "xxx")

コネクションプールを使用したい場合は適宜 createConnection をオーバーライドします。

val db = new Database {
    override def createConnection(): java.sql.Connection = /* ここでコネクションプールからコネクションを取得 */
}

接続のオープン、クローズは Database#withSession, withTransaction が面倒を見てくれます。

db withSession { /* この中でいろいろやる */}
db withTransaction { /* この中でいろいろやる */}

DBへのクエリは withSession、withTransaction のなかでのみ実行できます。

Table 作成

ddl.create を使います。

db.withSession { implicit session: Session =>
  Instruments.ddl.create
  Members.ddl.create
}

生成されたテーブル定義は以下のようになります。
外部キーなどもちゃんと生成されていますね。

beatles=# \d members
                                    Table "public.members"
    Column     |         Type          |                      Modifiers
---------------+-----------------------+------------------------------------------------------
 id            | integer               | not null default nextval('members_id_seq'::regclass)
 first_name    | character varying(20) | not null
 last_name     | character varying(20) | not null
 instrument_id | integer               | not null
Indexes:
    "members_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "fk_instrument" FOREIGN KEY (instrument_id) REFERENCES instruments(id)

beatles=# \d instruments
         Table "public.instruments"
 Column |         Type          | Modifiers
--------+-----------------------+-----------
 id     | integer               | not null
 name   | character varying(10) | not null
Indexes:
    "instruments_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "members" CONSTRAINT "fk_instrument" FOREIGN KEY (instrument_id) REFERENCES instruments(id)

INSERT

Table オブジェクトから直接 insert を呼ぶ方法もありますが、先ほど定義した ins を使うほうが便利だと思います。
戻り値として insert したオブジェクトが返ってきます。自動採番もサポートされます。

db.withSession { implicit session: Session =>
  val guitar = Instruments.insert(Instrument(1, "Guitar"))
  val base = Instruments.insert(Instrument(2, "Base"))
  val drums = Instruments.insert(Instrument(3, "Drums"))

  println(guitar) // Instrument(1,Guitar)
  println(base)   // Instrument(2,Base)
  println(drums)  // Instrument(3,Drums)
}

db withSession { implicit session: Session =>
  val john = Members.ins.insert("John", "Lennon", 1)
  val paul = Members.ins.insert("Paul", "McCartney", 2)
  val george = Members.ins.insert("George", "Hareisson", 1)
  val ringo = Members.ins.insert("Ringo", "Star", 3)

  println(john)   // Member(1,John,Lennon,1)
  println(paul)   // Member(2,Paul,McCartney,2)
  println(george) // Member(3,George,Hareisson,1)
  println(ringo)  // Member(4,Ringo,Star,3)
}

insertAll という batch insert 用のメソッドもあります。

SELECT

主に for を使う方法と where を使う方法の 2 種類があります。


書き方ちょっと多すぎるので、ここではほんの一例をあげます。他は
http://slick.typesafe.com のドキュメントや
https://github.com/slick/slick-examples のサンプルコードや
Slick 自体のテストコードを見てください。
かなりいろいろな書き方ができますが、Scala のコレクションと似たようなものと捉えればさほど難しくありません。

  db withSession { implicit session: Session =>
    // 単純な find
    val allQuery = for { m <- Members } yield m
    println(allQuery.list)
    // List(Member(1,John,Lennon,1), Member(2,Paul,McCartney,2), Member(4,Ringo,Star,3), Member(3,George,Harrisson,1))

    // where 句は for 式内の if で
    val johnQuery = for { m <- Members if m.firstName === "John" } yield m
    println(johnQuery.firstOption) // Some(Member(1,John,Lennon,1))

    val notJohnQuery = for {
      m <- Members.sortBy(_.firstName) // order by は sortBy。ScalaQuery の Query.orderBy は deprecated に。
      if m.firstName =!= "John"
    } yield m
    println(notJohnQuery.list)
    // List(Member(3,George,Harrisson,1), Member(2,Paul,McCartney,2), Member(4,Ringo,Star,3))

    // limit は take
    // 先に組み立てていたクエリをもとに別のクエリを作ることができる。
    println(notJohnQuery.take(2).list)
    // List(Member(3,George,Harrisson,1), Member(2,Paul,McCartney,2))

    // INNER JOIN がステキ
    val membersAndInstrumentsQuery = for {
      m <- Members
      i <- Instruments
      if m.instrumentId === i.id
    } yield (m, i)

    membersAndInstrumentsQuery.foreach(x => println(x))
    // (Member(1,John,Lennon,1),Instrument(1,Guitar))
    // (Member(2,Paul,McCartney,2),Instrument(2,Base))
    // (Member(4,Ringo,Star,3),Instrument(3,Drums))
    // (Member(3,George,Harrisson,1),Instrument(1,Guitar))

    // where の chain でも探せる
    Members.where(_.id === 3).where(_.firstName === "George").firstOption.foreach(println)
    // Member(3,George,Harrisson,1)
  }


SQL の LIMIT 句が take に対応していたりするのは納得感がありますね。ちなみに OFFSET は drop です。
組立てたクエリは他のクエリを組み立てるのに再利用することもできます。
SQL の弱点は non-compositional な文法だ。Slick は compositional だ。だそうですよ。

COUNT

2012/12/30 追記

COUNT のクエリは table.primary-key.count という風に組立てます。
Scala Slick 0.11.2 count with H2 - Stack Overflow

  db.withSession { implicit session: Session =>

    val q = for {
      m <- Member
    } yield {
      m.id.count
    }

    q.firstOption.foreach { x => println(x) } //=> 4

  }

... yield { m.count } ってやるんだと思ってたんですが、それだと変なクエリが発行され、実行時エラーになります。ずっとバグだと思ってました。コンパイルエラーにしてくれるとうれしいなあ。

UPDATE

for または where で絞り込み、map で対象のカラムを選び、それに update をかけます。
map を省略するとカラム全て、<> で テーブルをケースクラスにマッピングしている場合は case class が update の引数となります。


ジョージの名前を typo していたので修正します。

where を使うとこう

db.withSession { implicit session: Session =>
  Members.where(_.id === 3).map(_.lastName).update("Harrisson")
}

for を使うとこうです。

db withSession { implicit session: Session =>
  (for { m <- Members if m.id === 3 } yield m).map(_.lastName).update("Harrisson")
}

Scala の for 式が map などに変換されることを知っていれば

db withSession { implicit session: Session =>
  (for {
    m <- Members
    if m.id === 3
    lastName = m.lastName
  } yield lastName).update("Harrisson")
}

と書けることもわかると思います。(まあこれはちょっと読みづらいですが)


そろそろ Slick のクエリにも慣れてきたのではないでしょうか。
結構複雑に思えるかもしれませんが、Slick というより Scala のコレクションの操作方法がたくさんあるということです。


2013/02/03 追記
複数のカラムを同時にアップデートするやり方は以下のスレッドで紹介されています。
https://groups.google.com/d/topic/scalaquery/ML56aZAfy3g/discussion

DELETE

ここまでくると特に言うことはありません。
for, where でクエリを組み立てて delete です。

db withSession { implicit session: Session =>
  Members.where(_.firstName === "Yoko").delete
}

生成された SQL の確認

2012/12/08 追記

生成された SQL は #selectStatement で見ることができます。

val q = for (m <- Members; .....) yield m
println(q.selectStatement)

パフォーマンスが悪い、生成された SQL がダメで SQLException が飛んできて動かない(バグですね)、なんてときは実際に生成された SQL を確認してみましょう。

Plain SQL

StaticQuery を使うと、SQL 直書きも可能です。
今回はせっかくなので String interpolation API を試してみます。
その他の例は
https://github.com/slick/slick-examples/blob/0.11.2/src/main/scala/scala/slick/examples/jdbc/PlainSQL.scala
を参照してください。

import scala.slick.jdbc.{ GetResult, StaticQuery => Q }
import Q.interpolation // String interpolation API
// resultset から Member オブジェクトへの変換を定義する必要がある
implicit val getMember = GetResult(rs => Member(rs.nextInt, rs.nextString, rs.nextString, rs.nextInt))

db withSession { implicit session: Session =>
  val id = 2
  sql"select * from Members where id = ${id}".as[Member].firstOption
  // Option[Member] = Some(Member(2,Paul,McCartney,2))
}


String Interpolation を使わないとこうなります。

db withSession { implicit session: Session =>
  val id = 2
  Q.query[Int, Member]("select * from Members where id = ?").firstOption(id)
}

オマケ java.sql.* の代わりに JodaTime を使う

追記 2013/03/23
http://d.hatena.ne.jp/tototoshi/20130323/1364013170

まとめ

というわけで Slick の使い方でした。
特に Play2.0 で Anorm もうやだーって人は Play2.1 からは Slick を使うと良いと思いますよ。(他にもいろいろあるけどね)