play-slickを使う

先日play-slickを使ってみたのでそのメモ。

play-slickとは

その名の通り、playframeworkでslickを使うためのプラグインです。

play-slickを使うと

  • playframeworkのデフォルトのDB設定をそのまま使い回せる
  • slickのスキーマ変更に合わせて自動でevolution用のmigrationファイルを作成してくれる
  • セッション周りの扱いが楽になる

といったメリットがあります。

ちなみにplay-slickはplayframework 2.3に統合される予定とのこと。

環境

今回試した環境です。

  • playframework 2.2.1
  • scala 2.10
  • slick 2.0.0
  • play-slick 0.6.0.1
  • mariadb 10.0.8

セットアップ

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を簡単に実装するための仕組みがあり、公式サンプルに実装例があります。

ただこのサンプルだと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
}