Playの初歩とSlick

PlayFrameworkerのisobeです。Scala面白いです。

Play使い始めてしばらくDAO周りでAnormを使ってたのですがノーテーションが冗長すぎて辛いのでSlick(旧ScalaQuery)に乗り換えました。今最新のPlay2.2を使ってるのですが次期2.3では標準ORM(というかDAO)がAnormからSlickに置き変わる予定だそうです。(ってことをSlick使い始めてから知りました)

ライブラリの設定周り

Play2.2ではplay new直後は入らないのでjarをlib/にぶち込むか、以下のようにbuild.sbtにmvnリポジトリを指定してplay updateで自動でダウンロードされるようにします。ちなみにScala使い始めてbuild.sbtとBuild.scalaの違いに混乱したりするのですがこのエントリが超分かりやすくて、つまりsbtのDSLはBuild.scala(Scala上でsbtをビルドタスクライブラリ的に使う方法)の簡易版ということのようです。

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache,
  "mysql" % "mysql-connector-java" % "5.1.18",
  "com.typesafe.slick" %% "slick" % "1.0.1",
  "com.typesafe.play" %% "play-slick" % "0.5.0.8",
  "commons-dbcp" % "commons-dbcp" % "1.4")

slick自体はplayに依存しないscala用ライブラリなのでそのplayプラグインであるplay-slickも一緒にいれます。あと自分はMySQLを使うのでJDBCのドライバを入れてるのと、あとcommons-dbcpというのはコネクションプール用のライブラリです。コネクションプールは(Scalaじゃない)Javaのほうで定義されたインタフェースで、このインタフェースを実装するライブラリをSlickから簡単に使えます。コネクションプールライブラリはAmache CommonsのDBCPっていうのと、C3P0というこちらもフリー(LGPL)のライブラリの、2つがメジャーなようですが、最速をうたうBoneCPというのもあるみたいです。(このBoneCPのサイトにあるベンチマークを見るとBoneCP爆速ですが、私は主にPlayはUIで使うので枯れてて比較的高速なDBCPを使うことにしました。)

application.confはAnormと殆ど同じでモデルのパッケージをslick.defaultに指定することくらい

slick.default="models.database.*"

db.default.driver=com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost/test1?characterEncoding=UTF-8"
db.default.user=xxx
db.default.password="xxx"

コネクション周り

Slick(以外のをちゃんと調べてないですが)はセッションやトランザクションを記述するやり方が便利で、JDBCのドライバを直接指定するやり方とDataSource(を実装するコネクションプール)を介在させるやり方を以下のように簡単に切り替えられます。

import scala.slick.driver.MySQLDriver.simple._
import Database.threadLocalSession

import org.apache.commons.dbcp.BasicDataSource
import javax.sql.DataSource

object Dao {

// JDBC的指定

def test1() = Database.forURL("jdbc:mysql://localhost/test1?characterEncoding=UTF-8", driver = "org.mysql.jdbc.Driver", user = "xxx", password = "xxx") {
  // do something..
}


// コネクションプール指定

val DS: DataSource = {
  val ds = new BasicDataSource
  ds.setDriverClassName("com.mysql.jdbc.Driver")
  ds.setUsername("xxx")
  ds.setPassword("xxx")
  ds.setMaxActive(20);
  ds.setMaxIdle(10);
  ds.setInitialSize(10);
  ds.setUrl("jdbc:mysql://localhost/test1?characterEncoding=UTF-8")
  ds
}

DS.getConnection().close()

val DB = Database.forDataSource(DS)

def test2() = DB withSession {
  // do something..
}

}

O/Rマッピング

これでセッション/トランザクションの準備は出来て、次にO/RマッピングですがSlickはドキュメントにもある通りLifted,PlainSQL,Directの3種類のやり方があります。抽象度の高い順に並べるとDirect>Lifted>PlainSQLで、Directは最も記述量が少ないですがまだ実験的実装で将来的に正式サポートされた場合にはこの3本立てになるそうです。(今slickは1.0.1が最新ですが次期バージョンは2.0)

PlaySQLを使ったとしてもAnormよりもだいぶ簡易的な記述で済むのでなかなかよいですが、せっかくなので私はLiftedの記法を使います。

package models

import play.api.db._
import play.api.Play.current

import scala.slick.driver.MySQLDriver.simple._
import scala.slick.jdbc._ // --(*) これが超大事!!
import Database.threadLocalSession

import org.apache.commons.dbcp.BasicDataSource
import javax.sql.DataSource

object Dao {
 // (中略)
case class MyOrder (
    id: Long,
    order: Int,
    description: String,
)

object MyOrders extends Table[MyOrder ]("myorder") {
  def id = column[Long]("id",O.PrimaryKey, O.AutoInc)
  def order= column[Int]("order", O.NotNull)
  def description = column[String]("description ", O.NotNull)
  def * = order ~ description <> (MyOrders , MyOrders .unapply _)

  def all(): List[MyOrders ] = DB withTransaction MyOrders.all()
  
  def findById(id1: Long): Option[MyOrders ] = DB withSession {
    Query(MyOrders ).where(_.id===id1).firstOption
  }
}

// テーブル初期化。エボリューションをちゃんと使うべき
DB withSession {
  if (MTable.getTables.list().size < 1) {
    MyOrders.ddl.create
    // 複数あるときは (A.ddl ++ B.ddl).create という風にする
  }
}

}

テーブル定義用のオブジェクトのほうが最後に複数形のsがついてるけど、case classと同じ名前(この場合はMyOrder)でコンパニオンオブジェクトにしても支障はないようです。上記コードではDaoオブジェクトにくるんでるがそれぞれファイルを分けてパッケージ直下においてももちろんOK.ここでのハマりポイントは

  • import scala.slick.jdbc._ しないとfindByIdの中で使ってるwhereやfirstOptionが使えないので注意
    • QueryからInvokerへのimplicit conversionが定義されてる必要があるため
    • ちなみにfindByIdメソッドの中で書いてるQuery(MyOrders)のところで(implicit conversion頼みで)単にMyOrderと書くサンプルコードがあるがScalaバージョンによってはなのか、型推論に失敗するようなのでQuery(MyOrder)と書くのが無難
  • import Database.threadLocalSessionしておくと、DB withSessionブロックの内部がセッションを引数とするラムダ式(session =>みたいな)を書かなくてよくなる(importしてる目的自体は表記短縮ではなくコネクションをスレッドローカルに持つため)
  • eclipseを使ってるとScalaIDEプラグイン型推論能力不足のためかコード補完する際に可能なものが列挙されないときがある
    • これに5時間くらいハマった。SlickのAPIドキュメントを見ながら気にせず正しいメソッド呼び出しを書けばコンパイル自体はScalaコンパイラが行うのでeclipse上で正しい文章としてエラーが抑制される(逆に言うとeclipse上でエラーとなったらScalaコードとして正しくない。当然だけど。)

エボリューション関係について

あと、Slickを使う場合のエボリューションスクリプトについてですが、1.sql,2.sqlとかの管理は基本的プログラマがやる必要があります。ただしテーブルcreateのSQL文は上記のコードのようにSlickで書いたクラス/オブジェクトのDDLから自動生成できるので、この人の書いてるスクリプト自動生成プログラムを置いとくと便利かもしれません。
とりあえずは上記のコードのようにevolutionは使わず、処理系がDaoオブジェクトを読み込んだときにddl.createしてテーブル生成するやり方がまずは簡単でいいかもしれません(ただ個別のDBに依存しないためなのかcreate if not existsが使えないので作成済みテーブル数を見て初回だけ生成するようにするとかやっているのでevolutionをちゃんと使ったほうが良い)。

追記)
上のように書きましたが、その上で掲載してるようにapplication.confにslick.defaultを指定してればplay-slickプラグインがエボリューションスクリプトを自動生成してくれるようです。ただしリフレクションを使ってクラスを探すので上のようにDaoオブジェクトで包む形だと上手く自動生成してくれません。そこで私は以下のようにしました。

  • caseクラスとコンパニオンオブジェクトを1クラスごとのXxx.scalaファイルとしてmodelsパッケージに置く
    • コンパニオンオブジェクトにはfindByIdみたいなSlickを使ったDBアクセス用のメソッドを書いてコントローラがSlickに依存しないようにしておく
  • slick用のLiftedのコードは複数形の名前でXxxs.scalaという風なファイルに書いてmodels.databaseパッケージに置く
  • application.confのslick.defaultには"models.database.*"を指定する
  • コネクションプール周りのコードはデフォルトパッケージのGlobal.scalaの中でmodelsのパッケージオブジェクトとして書いておく

こうすると色々スッキリしました。
追記ここまで)

これでコントローラからは

object Application extends Controller {
  def index = Action {
    val id = get_some_id()
    val mo = Dao.MyOrder.findById(id)
    // use mo object...
    // (以下略)
  }
}

という風にDBアクセスが出来ます。

というわけで

PlayFramework2.2へのSlick1.0.1の導入について書きました。あとPlay2を使ってて必要になる認証周りのライブラリでSecureSocialというのを使っているので次回エントリではそれについて書きます。

Playはちょっと面倒なことが多いけどRails的な利便性とIDEの開発効率と静的型チェックによる変な逸脱コードの回避などが相まって生産性は高いと思うので学習コストは投資回収可能なはずだと思います。