play-slickを使う
先日play-slickを使ってみたのでそのメモ。
play-slickとは
その名の通り、playframeworkでslickを使うためのプラグインです。
play-slickを使うと
- playframeworkのデフォルトのDB設定をそのまま使い回せる
- slickのスキーマ変更に合わせて自動でevolution用のmigrationファイルを作成してくれる
- セッション周りの扱いが楽になる
といったメリットがあります。
ちなみにplay-slickはplayframework 2.3に統合される予定とのこと。
環境
今回試した環境です。
セットアップ
playframeworkのセットアップは省略。
プラグインの追加
まずbuild.sbt
に利用するプラグインを追記していきます。
// build.sbt libraryDependencies ++= Seq( // other plugins // ... "com.typesafe.slick" %% "slick" % "2.0.0", "org.slf4j" % "slf4j-nop" % "1.6.4", "com.typesafe.play" % "play-slick_2.10" % "0.6.0.1" "org.jumpmind.symmetric.jdbc" % "mariadb-java-client" % "1.1.1", )
ここで注意が必要なのはplay-slickのバージョンで、playframeworkとslickのバージョンによって使えるバージョンが異なるようです。
playframework | slick | play-slick |
---|---|---|
2.3.x | 2.0.x | 0.7.x |
2.2.x | 2.0.x | 0.6.x |
2.2.x | 1.0.x | 0.5.x |
2.1.x | 1.0.x | 0.4.x |
(2014-07-27編集: 更新された公式ドキュメントに合わせて修正しました)
DBの設定
通常のplayframeworkアプリケーションと同様に、application.conf
にDBの接続情報を追記します。
mariadb用の設定となっているところは適宜変更してください。
# conf/application.conf db.default.driver=org.mariadb.jdbc.Driver db.default.url="jdbc:mariadb://localhost/dbname" db.default.user=dbuser db.default.password=dbpass slick.default="models.*"
ここでslick.default
というパラメタを指定していますが、これはevlolution用にmigrationファイルを作成する対象のクラスを指定するために利用されるそうです。
今回はmodels
パッケージ以下にslickの用クラスを定義するためそのように指定しています。
modelの実装
idと名前を持つユーザ用のテーブルとそれを操作するクラスを考えたときに、基本形はこんな形になると思います。
// app/models/user.scala package models import play.api.db.slick.Config.driver.simple._ case class User(id: Option[Int], name: String) class Users(tag: Tag) extends Table[User](tag, "users") { def id = column[Int]("id", O.PrimaryKey, O.AutoInc) def name = column[String]("name", O.NotNull) def * = (id.?, name) <> (User.tupled, User.unapply _) } object Users extends DAO { def findById(id: Int)(implicit s: Session): Option[User] = { Users filter { _.id === id } firstOption } def insert(user: User)(implicit s: Session) { Users += user } }
まずユーザ情報をUser
ケースクラスとして実装し、usersテーブルの表現としてTable[User]
を継承するUsers
クラスを実装しています。これはslickのLifted Embeddingの書式そのままです。
今回はここでUsers
クラスのコンパニオンオブジェクトとしてDAOを定義しています。
controllerはこのDAOを利用することでロジックをcontrollerに持ち込ませなくて済みます。
このコンパニオンオブジェクトが継承しているDAO
は以下のように定義しています。
// app/models/DAO.scala import scala.slick.lifted.TableQuery private[models] trait DAO { val Users = TableQuery[Users] val Posts = TableQuery[Posts] ... }
こうすることで他のテーブルを跨ぐクエリ発行するメソッドを実装できるようになります。
controllerの実装
// app/Application.scala package controllers import models._ import play.api.db.slick._ import play.api.mvc._ object Application extends Controller { def index = DBAction { implicit rs => Ok(views.html.index(Users.findAll)) } }
controllerではDBAction
を使うことによりセッションの生成を自動で行ってくれます。
そのためDAO側ではimplicit session: Session
を受け取るよう定義する必要があります。
まとめ
以上のようにセッション周りを考えずにDBを扱うことができるので、ロジックに集中して実装できると思います。
おまけ: slick-playとCake Pattern
play-slickにはProfile
というcake patternを簡単に実装するための仕組みがあり、公式サンプルに実装例があります。
- https://github.com/freekh/play-slick/blob/master/src/main/scala/play/api/db/slick/Profile.scala
- https://github.com/freekh/play-slick/tree/master/samples/play-slick-cake-sample
ただこのサンプルだとcontrollerで直接slickのクエリを利用していてあまりイケてない感じだったのでDAOを使う方式に書き直してみました。
が、DAO
を継承するクラスが増えるたびにインスタンスを作らなければならなくなり、微妙な感じに。
良いやり方があったら教えてください!
# conf/application.conf slick.default="models.current.dao.*"
// app/models/user.scala package models import play.api.db.slick.{Profile, Session} case class User(id: Int, name: String) trait UserComponent { this: Profile => import profile.simple._ class Users(tag: Tag) extends Table[User](tag, "users") { def id = column[Int]("id", O.PrimaryKey) def name = column[String]("name", O.NotNull) def * = (id, name) <> (User.tupled, User.unapply _) } } class UsersDAO(implicit dao: DAO) { import dao._ import profile.simple._ def findByName(id: Int)(implicit s: Session): Option[User] = { Users filter { _.id === id } firstOption } def insert(user: User)(implicit s: Session) { Users += user } }
// app/models/DAO package models import scala.slick.driver.JdbcProfile import scala.slick.lifted.TableQuery import play.api.db.slick.{Profile, DB} class DAO(override val profile: JdbcProfile) extends UserComponent with Profile { val Users = TableQuery[Users] } object current { implicit val dao = new DAO(DB(play.api.Play.current).driver) val Users = new UsersDAO }