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.ここでのハマりポイントは
エボリューション関係について
あと、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の開発効率と静的型チェックによる変な逸脱コードの回避などが相まって生産性は高いと思うので学習コストは投資回収可能なはずだと思います。