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
}

dispatch(reboot)でoauthを扱う

rebootと呼ばれる新しいdispatchにはoauth exchangeというoauth 1.0aを簡単に扱うための仕組みがあるのですが、公式ドキュメントに解説が載っていないのでメモ。

メインとなるのはExchangeトレイトで、自分型としてSomeHttpSomeConsumerSomeCallbackSomeEndpointsの各トレイトがミックスインされています。

これらの各トレイトを継承したオブジェクトを作ることでaccess tokenの取得までが簡単に行えます。

oauth exchangeの実装

以下はDropbox Core APIの場合の実装例。

import com.ning.http.client.oauth._
import dispatch._
import dispatch.oauth._

trait DropboxHttp extends SomeHttp {
  def http: HttpExecutor = Http
}

trait DropboxConsumer extends SomeConsumer {
  def consumer: ConsumerKey = new ConsumerKey("ほげ", "ふが")
}

trait DropboxCallback extends SomeCallback {
  def callback: String = "oob"
}

trait DropboxEndpoints extends SomeEndpoints {
  def requestToken: String = "https://api.dropbox.com/1/oauth/request_token"
  def accessToken: String = "https://www.dropbox.com/1/oauth/authorize"
  def authorize: String = "https://api.dropbox.com/1/oauth/access_token"
}

object DropboxExchange extends Exchange
  with DropboxHttp with DropboxConsumer with DropboxCallback with DropboxEndpoints

ここで出てくるConsumerKeyAsync Http Clientのものです。

ちなみにcallbackに指定された指定された値はrequest token取得時に渡される仕様(oauth 1.0a)なので、authorizeへのリクエスト時に渡したい場合(oauth 1.0)はオーバーライドする必要がありそう。とここまで書いて、authorize時にoauth_callbackを指定するdropbox core apiを例に出したのは失敗だなと気づきました。

oauth exchangeの利用

以下がaccess tokenの取得例。

import com.ning.http.client.oauth._
import dispatch._
import dispatch.Defaults._
import scala.concurrent._

val res1 = DropboxExchange.fetchRequestToken
val requestToken = res1 map {
 case Right(t) => t
 case Left(m)  => sys.error(m)
}

val verifier = requestToken map { t =>
  blocking {
    val url = DropboxExchange.signedAuthorize(t)
    println(url)
    println("please press enter after authorize")
    readLine()
  }
}

val res2 = requestToken flatMap { t =>
  verifier flatMap { v =>
    DropboxExchange.fetchAccessToken(t, v)
  }
}

val accessToken = res2 map {
  case Right(token)  => token
  case Left(message) => sys.error(message)
}

access tokenを利用してAPIを叩く例。

val accountInfo = :/("api.dropbox.com").secure / "1" / "account" / "info"
val signedAccountInfo = accountInfo <@ (DropboxExchange.consumer, accessToken())
val res3 = Http(signedAccountInfo OK as.String)
res3 onSuccess {
  case a => println(a) 
}

ちょっとfutureを扱うコードが汚くなっていますが、ちゃんと書けばそれっぽく見えると思います(?)。

oauth 2.0

oauth 2.0を実装したPRが出ているのですが、現時点より6ヶ月前で更新が止まっています。

scala用のgitattributesを定義してdiffを見やすくする

ちょっと前に話題になってましたね、gitattributes。

調べてみましたが案の定scalaは非対応だったので、自分で定義してみます。

before

普通にdiffを出すとこんな感じ。

f:id:akiomik:20131220003306p:plain

hunk headerにはクラス名が出てしまい、なんのメソッドへの修正かわかりません。

after

以下のような感じで、 ファイルをそれぞれ~/.gitattributes~/.gitconfigとして保存します。

gitattributes for scala

すると、あら不思議!

f:id:akiomik:20131220003323p:plain

hunk headerにちゃんとメソッド名が出ています!ヤッター!

注意点

見ての通り適当な正規表現なので、修正行の直前に深いネストの関数などが定義されてたり、コメントの中に予約語が入ってたりすると、そっちのメソッド名が出てしまいます。

まあその辺はビルトインの他言語用gitattributesも同じくゆるふわな感じなのでご了承ください。

感想とか

本当に見やすくなったんだろうか…?