play1 to play2
Post on 05-Dec-2014
2.846 Views
Preview:
DESCRIPTION
TRANSCRIPT
Play1開発者のためのPlay2講座
株式会社 FLECT小西俊司
FLECT の主力言語は Play1◦ しかし Play1 はもはやメンテされていない模様◦ 1.3 が出ると一瞬聞いたような気もするが。。。 (--
Play1 開発者が Play2 を使う場合に◦ Play1 でのやり方との対比で Play2 での方法を知る◦ 言語と Framework の両方を同時に学ぶのは辛いのでとりあえず
Scala が良く分かってなくてもなんとなく開発できる気にするのがゴール
◦ これどうするんだっけ?と思った時に見る資料 ( 主に俺が ) ただし Best Practice はまだまだ模索中なのでもっと良い
方法があるはずとも思う。◦ というか Play2 自体まだまだそんな感じ◦ 間違ってもここに書いてあることを鵜呑みにしてはいけない
この文書の目的
自力で頑張れ◦ List, Map, Option, パターンマッチ , case class あたりがわかればとりあ
えず使える 禁止技
◦ 途中で Return◦ Null との比較 (Option を使用する )
Scala でどう書けば良いのかわからん!と思った時にはとりあえず Java だと思って書けば OK
慣れるまで import でワイルドカードは使わない◦ Play( というか Scala 全般 ) で import はワイルドカードで書く文化でサン
プル等もほとんどそうなっているが、それだとクラス構成が全く分からない。
◦ 特に implicit 関数を把握しないまま使うのは危険だと思う◦ 列挙型や Implicit 関数などワイルドカードでインポートしないと使いづら
いものもあるが、それを見極めるためにも最初は自力で書く
Java to Scala
Play2 のインストールは zip を展開して PATH を通すだけ
しかし「 play(.bat) 」という実行スクリプトの名前が Play1 とバッティングする。。。◦ → 実行スクリプトを「 play2(.bat) 」とリネームすれば
問題なく共存して使える
環境設定
Hello world!package controllers
import play.api.mvc.Controllerimport play.api.mvc.Action
object Application extends Controller { def index = Action { Ok(views.html.index("Your new application is ready.")) } def hello = Action { Ok("Hello world") } }
デフォルトで定義されているActionOk は「 200 OK 」のレスポンスviews.xxxx は views フォルダにあるテンプレートファイルへの参照でr enderTemplate相当
renderText 相当
Play1 と違ってワイルドカードルーティングはないので routes の編集も必要
多くのメソッドでリクエスト情報を暗黙引数として要求されるので常にimplicit キーワード付きで参照するようにする
リクエストを参照可能にするpackage controllers
import play.api.mvc.Controllerimport play.api.mvc.Action
object Application extends Controller { def index = Action { implicit request => Ok(views.html.index("Your new application is ready.")) } def hello = Action {implicit request => Ok("Hello world") } } Implicit をつけると暗黙の引数として利用でき
る
テンプレート@(name: String)(implicit lang: Lang)
@main(Messages("hello")) {<div>@Messages("hello") @name</div>}
これがないと Message の国際化がされない
使用するパラメータはすべて宣言する必要がある◦ 最初は Play1 に比べてかなり面倒な気がするがパラメータの不備がコン
パイルエラーで拾えるのがメリット マジックワードは「 @ 」のみ 「 @{…} 」で自由に Scala コードを記述可能 上の例で使用している機能
◦ 親テンプレートとして @main を呼び出している◦ @Messages で国際化されたメッセージリソースを出力◦ 引数 @name を出力
ワイルドカードは使えない Action の参照には引数を含める必要がある PATH の一部を Action の引数として渡せる GET パラメータは Action の引数として渡せるが POST パラメータは渡せない パラメータには省略時のデフォルトが定義できる
ルーティング# Routes# This file defines all application routes (Higher priority routes first)# ~~~~
# Home pageGET / controllers.Application.index
GET /hello controllers.Application.helloGET /hello2/:name controllers.Application.hello2(name)GET /hello2 controllers.Application.hello2(name ?= "guest")
# Map static resources from the /public folder to the /assets URL pathGET /assets/*file controllers.Assets.at(path="/public", file)
Play1 に比べて面倒
Ok 、 Redirect 、 BadRequest 、など Http ステータス毎にメソッドが用意されている
Content-Type は引数によって自動的に識別◦ Play1 の「 renderXXX 」に相当するメソッドはない◦ あるいは
のように「 as 」メソッドで明示 Play1 のようなトリックはない
◦ renderXXXX が実は Exception だとか◦ Controller のメソッドを呼ぶと勝手にリダイレクトされる
とか
Httpレスポンス
Ok(“””{“name”:”konishi”}”””).as(“application/json”)
Form を使用する◦ Case class を作ってそこにマッピングするのが楽
POSTパラメータの取得
private val queryForm = Form(mapping( "id" -> optional(text), "kind" -> of[QueryKind], "name" -> text, "group" -> default(text, ""), "sql" -> text, "desc" -> optional(text), "setting" -> optional(text) )(QueryInfo.apply)(QueryInfo.unapply));
https://github.com/shunjikonishi/sqltool/blob/master/app/controllers/QueryTool.scala
個別に取得したい場合は以下のようにすると取れる◦ユーティリティメソッドを作っておかないとものすごく面倒
POSTパラメータの取得 (2)
val name: Option[String] = request.body.asFormUrlEncoded.flatMap(_.get("name").map(_.head));
Play1 とほとんど変わらない◦ application.conf の「 application.langs 」で使用する
言語を宣言◦ 言語毎に「 messages.xx 」を conf ディレクトリに作
成◦ リソースの参照は Messages クラスを使用
同一ファイル内に複数言語のメッセージを定義したい場合は Global.scala に Play1 で使用していた ResourceGen を組み込むことができる◦ https://github.com/shunjikonishi/sqltool/blob/mast
er/app/Global.scala
国際化
Play1 の「 conf/dependencies.yml 」相当のファイルは「 project/Build.scala 」
JDBC ドライバはデフォルトで入っていないのでPostgreSQL などの JDBC ドライバを使用する場合は自分で組み込む必要がある
https://github.com/shunjikonishi/sqltool/blob/master/project/Build.scala
Heroku で使う場合、 Play1 では ivysetting.xml が自前で用意できなかったためリポジトリの追加ができなかったが Play2 では問題なくできる◦ flectCommon などの内部 jar をリポジトリから取得できる
依存性管理
Application.conf にデータベース定義を追加 Play1 では DB はひとつしか定義できなかったが複
数定義可能になった ORMapper はない 代わりに Anorm が組み込まれている
◦ 多分 SelectBuilder との相性は良い◦ https://github.com/shunjikonishi/sqltool/blob/master
/app/models/QueryManager.scala
データベース
// グループ名の一部を抜き出す処理SQL(""" SELECT distinct groupname FROM sqltool_sql WHERE groupname LIKE {gl} ORDER BY groupname """ ).on( "gl" -> (parent + "/%") ).apply.map{ row => val g = row[String]("groupname").substring(parent.length + 1); g.split("/")(0); }.toSet.toList;
CacheAPI は set,get,remove の 3 メソッドのみとなった
Memcached を使用する場合はプラグインが必要◦ https://github.com/mumoshu/play2-memcached◦ Play1 と同じく serializable なオブジェクトをキャッシュに保存可能
◦ Case class は serializable なのでそのままキャッシュに保存可能
キャッシュ
https://github.com/orefalo/play2-authenticitytoken
をいれると、 Play1 相当のことができるらしい◦ Memcached もそうだが Play1 にあった機能が足りて
ない場合は有志がプラグインとして作っている印象 Play1 の時に作った自前の CSRFモジュールをプ
ラグインとして移植しても良いけど微妙。
CSRF
JSON ライブラリが gson から jackson( の Scalaラッパーである jerkson) に変わった
Case class を JSON に parse/format するだけならJSON.format を implicit で定義するだけで良い。
加工が必要な場合は自分で Format を定義◦ JSON のキーを変数名とは別にしたい◦ Case class 中のあるフィールドは JSON に含めたくない
https://github.com/shunjikonishi/sqltool/blob/master/app/models/QueryInfo.scala
https://github.com/shunjikonishi/sqltool/blob/master/app/models/SQLToolImplicits.scala
JSON
Scala の XML リテラルがそのまま使用できる◦ もっとも最近では REST API はほとんど JSON なのでほ
とんど使うことはない◦以前に XML リテラルで XML を組み立てようとした際に、
手続き的な処理が必要になってどうするのが良いのか迷った記憶があるが詳細は忘れた。
XML
JavaScript や CSS は「 app/assets 」に置く このフォルダに置いたファイルでは
◦ CoffeeScript が JavaScript に変換される◦ LESS が css に変換される◦ js/css の minify版が生成される (xxx.min.js でアクセ
スできる ) イメージなどは「 public 」に置く。
◦ assets に置いても良いと思うが意味がないし、多分こっちの方が速い
assets
Play1 と同じく実体は Cookie セッション ID がない
◦ Memcache を使用する場合に困る◦ 自前の Filter で対応
セッション
//OAuth 認証の Token を session に持たせるOk(views.html.login(url)).withSession( "token" -> token.getOAuthToken())
Play1 と同じく次のリクエストまで有効な Flash が使用できる◦ 仕組みも Play1 と同じ◦ https://github.com/shunjikonishi/sqltool/blob/master/a
pp/controllers/QueryTool.scala
フラッシュ
// ファイル インポート後にインポート件数を付けてメイン画面にリダイレクトRedirect("/main").flashing( "Import-Insert" -> insertCount.toString, "Import-Update" -> updateCount.toString);
取得側のコード◦ Implicit request が必要◦ https://github.com/shunjikonishi/sqltool/blob/master/app/controlle
rs/Application.scala
def main = Action { implicit request => val importInsert = flash.get("Import-Insert").getOrElse("0").toInt; val importUpdate = flash.get("Import-Update").getOrElse("0").toInt; …}
Request.body.asMultipartFormData からアップロードされたファイルを java.io.File として取り出せる◦ Play1 は引数に File を宣言するだけだったのですごく面
倒になった◦ https://github.com/shunjikonishi/sqltool/blob/mast
er/app/controllers/QueryTool.scala
ファイルアップロード
def importSql = Action { implicit request => request.body.asMultipartFormData match { case Some(mdf) => mdf.file("file") match { case Some(file) => ... case None => BadRequest; } case None => BadRequest; }}
なんか冗長なのでもっときれいな書き方があると思う
Ok.sendFile メソッドを使う◦ Play1 では添付ファイルのダウンロードと削除を同時に
やろうとするとすごく面倒だったのが楽になった◦ https://github.com/shunjikonishi/sqltool/blob/mast
er/app/controllers/QueryTool.scala
ファイルダウンロード
def exportSql = Action { implicit request => val file = File.createTempFile("temp", ".sql"); try { man.exportTo(file); Ok.sendFile(file, fileName={ f=> "export.sql"}, onClose={ () => file.delete()}); } catch { case e: Exception => e.printStackTrace; Ok(e.toString); }}
Play1 と同じように WS クラスで外部WebService にアクセス可能
Play1 とほぼ同じような使い方だが同期 API がなくなって非同期 API のみなので同期で使用したい場合は自分で Future から結果を取り出す必要がある
WebService
Play1 の Plugin のように全リクエストで共通的に行う処理は Filter クラスとして実装◦ SessionIDFilter( リクエストにセッション ID を付加す
る )◦ https://
github.com/shunjikonishi/sqltool/blob/master/app/jp/co/flect/play2/filters/SessionIdFilter.scala
◦ AccessControlFilter(Basic認証と IP制限 )◦ https://
github.com/shunjikonishi/sqltool/blob/master/app/jp/co/flect/play2/filters/AccessControlFilter.scala
◦便宜的に SQLTool内で作成しているが必要に応じてPlugin 化する
ServletFilter的なモノ
Global クラスの宣言に WithFilters で組み込み https://github.com/shunjikonishi/sqltool/blob
/master/app/Global.scala
Filterの組み込み
object Global extends WithFilters(SessionIdFilter, AccessControlFilter) { …}
Play1 の @throws や @before のような処理を行う場合は Action の合成を使用する◦ https://github.com/shunjikonishi/sqltool/blob/mast
er/app/controllers/GoogleTool.scala
Controller内での Filter的なモノ
//GoogleSpreadsheet の設定が有効でない場合は InternalServerError を返すdef filterAction(f: Request[AnyContent] => Result): Action[AnyContent] = Action { request => if (GoogleSpreadsheetManager.enabled) { f(request); } else { InternalServerError("Google account is not setuped."); }}
def showSheet(bookName: String, sheetName: String) = filterAction { implicit request => ...}
Akka を直接使う◦ https://github.com/shunjikonishi/sqlsync/blob/mas
ter/app/Global.scala
Job
//Heroku の WebDyno を眠らせないために自分自身をポーリングval appname = sys.env.get("HEROKU_APPLICATION_NAME");if (appname.nonEmpty) { Akka.system.scheduler.schedule(0 seconds, 10 minutes) { WS.url("http://" + appname.get + ".herokuapp.com/assets/ping.txt").get() }}
ログライブラリは log4j から Logback+SLF4J に変更になった
play.api.Logger を直接使うこともできるが、個別に Logger インスタンスを生成して使用することもできるようになった
ログレベルの設定は application.conf内で個別に行える
ログ
logger.root=ERRORlogger.play=INFOlogger.application=DEBUGlogger.jp.co.flect=INFO#logger.com.google=DEBUGlogger.Schedule=INFO
Play1 にあった AppID はない ので、開発/本番環境で設定を切り替えたい場合は
application.conf 自体を切り替える◦起動コマンド
play run –Dconfig.resource=prod.conf Conf ファイル内では別の conf ファイルをインク
ルードでき、キーをオーバーライドすることが可能
Application.confの切り替え
include "application.conf"
key.to.override=blah
Heroku のスケジューラから Play2 を起動したい場合◦重いので可能であれば Java クラス単体にするなどした方が良い
target/start スクリプトに起動オプションを渡して起動
スケジュール
target/start –Dsqltool.mode=schedule
ローカルで動かす場合はあらかじめ「 play stage 」コマンドを叩いて lib をビルドする必要がある
Windows の場合は .bat は生成されないので自分でバッチファイルを書く必要がある
コマンドが長いので rake で起動するようにしているhttps://github.com/shunjikonishi/sqltool/blob/master/sqltool.bat
https://github.com/shunjikonishi/sqltool/blob/master/Rakefile
スケジュール起動時の処理実装 Global.onStart で起動オプションによって処理を分岐
◦終了時には問答無用で System.exit している ( 多分問題ないはず )
◦https://github.com/shunjikonishi/sqltool/blob/master/app/Global.scala
val mode = sys.props.get("sqltool.mode").getOrElse("web");mode match { case "schedule" => Schedule.main(); System.exit(0); case “setup” => … System.exit(0); case _ =>}
Play1 でできていたことは頑張れば全部できる Play1 では標準機能でできていたことがプラグイ
ンが必要になって面倒になった気がするが、言い換えれば組み合わせの自由度が上がったということでもある◦ もっとも発展途上の印象もぬぐえない
コード量はおそらく Play1 の半分以下になる◦ case class と List があるだけでも Java の生産性をはる
かに凌駕していると思う
多分使える
まとめ
top related