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 }
dispatch(reboot)でoauthを扱う
rebootと呼ばれる新しいdispatchにはoauth exchangeというoauth 1.0aを簡単に扱うための仕組みがあるのですが、公式ドキュメントに解説が載っていないのでメモ。
メインとなるのはExchange
トレイトで、自分型としてSomeHttp
、SomeConsumer
、SomeCallback
、SomeEndpoints
の各トレイトがミックスインされています。
これらの各トレイトを継承したオブジェクトを作ることで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
ここで出てくるConsumerKey
はAsync 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) }
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
を出すとこんな感じ。
hunk headerにはクラス名が出てしまい、なんのメソッドへの修正かわかりません。
after
以下のような感じで、 ファイルをそれぞれ~/.gitattributes
と~/.gitconfig
として保存します。
すると、あら不思議!
hunk headerにちゃんとメソッド名が出ています!ヤッター!
注意点
見ての通り適当な正規表現なので、修正行の直前に深いネストの関数などが定義されてたり、コメントの中に予約語が入ってたりすると、そっちのメソッド名が出てしまいます。
まあその辺はビルトインの他言語用gitattributesも同じくゆるふわな感じなのでご了承ください。
感想とか
本当に見やすくなったんだろうか…?