play1 to play2

33
Play1 開開開開開開開 Play2 開開 株株株株 FLECT 株株 西

Upload: shunji-konishi

Post on 05-Dec-2014

2.846 views

Category:

Technology


9 download

DESCRIPTION

 

TRANSCRIPT

Page 1: Play1 to Play2

Play1開発者のためのPlay2講座

株式会社 FLECT小西俊司

Page 2: Play1 to Play2

FLECT の主力言語は Play1◦ しかし Play1 はもはやメンテされていない模様◦ 1.3 が出ると一瞬聞いたような気もするが。。。 (--

Play1 開発者が Play2 を使う場合に◦ Play1 でのやり方との対比で Play2 での方法を知る◦ 言語と Framework の両方を同時に学ぶのは辛いのでとりあえず

Scala が良く分かってなくてもなんとなく開発できる気にするのがゴール

◦ これどうするんだっけ?と思った時に見る資料 ( 主に俺が ) ただし Best Practice はまだまだ模索中なのでもっと良い

方法があるはずとも思う。◦ というか Play2 自体まだまだそんな感じ◦ 間違ってもここに書いてあることを鵜呑みにしてはいけない

この文書の目的

Page 3: Play1 to Play2

自力で頑張れ◦ List, Map, Option, パターンマッチ , case class あたりがわかればとりあ

えず使える 禁止技

◦ 途中で Return◦ Null との比較 (Option を使用する )

Scala でどう書けば良いのかわからん!と思った時にはとりあえず Java だと思って書けば OK

慣れるまで import でワイルドカードは使わない◦ Play( というか Scala 全般 ) で import はワイルドカードで書く文化でサン

プル等もほとんどそうなっているが、それだとクラス構成が全く分からない。

◦ 特に implicit 関数を把握しないまま使うのは危険だと思う◦ 列挙型や Implicit 関数などワイルドカードでインポートしないと使いづら

いものもあるが、それを見極めるためにも最初は自力で書く

Java to Scala

Page 4: Play1 to Play2

Play2 のインストールは zip を展開して PATH を通すだけ

しかし「 play(.bat) 」という実行スクリプトの名前が Play1 とバッティングする。。。◦ → 実行スクリプトを「 play2(.bat) 」とリネームすれば

問題なく共存して使える

環境設定

Page 5: Play1 to Play2

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 の編集も必要

Page 6: Play1 to Play2

多くのメソッドでリクエスト情報を暗黙引数として要求されるので常に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 をつけると暗黙の引数として利用でき

Page 7: Play1 to Play2

テンプレート@(name: String)(implicit lang: Lang)

@main(Messages("hello")) {<div>@Messages("hello") @name</div>}

これがないと Message の国際化がされない

使用するパラメータはすべて宣言する必要がある◦ 最初は Play1 に比べてかなり面倒な気がするがパラメータの不備がコン

パイルエラーで拾えるのがメリット マジックワードは「 @ 」のみ 「 @{…} 」で自由に Scala コードを記述可能 上の例で使用している機能

◦ 親テンプレートとして @main を呼び出している◦ @Messages で国際化されたメッセージリソースを出力◦ 引数 @name を出力

Page 8: Play1 to Play2

ワイルドカードは使えない 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 に比べて面倒

Page 9: Play1 to Play2

Ok 、 Redirect 、 BadRequest 、など Http ステータス毎にメソッドが用意されている

Content-Type は引数によって自動的に識別◦ Play1 の「 renderXXX 」に相当するメソッドはない◦ あるいは

のように「 as 」メソッドで明示 Play1 のようなトリックはない

◦ renderXXXX が実は Exception だとか◦ Controller のメソッドを呼ぶと勝手にリダイレクトされる

とか

Httpレスポンス

Ok(“””{“name”:”konishi”}”””).as(“application/json”)

Page 10: Play1 to Play2

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

Page 11: Play1 to Play2

個別に取得したい場合は以下のようにすると取れる◦ユーティリティメソッドを作っておかないとものすごく面倒

POSTパラメータの取得 (2)

val name: Option[String] = request.body.asFormUrlEncoded.flatMap(_.get("name").map(_.head));

Page 12: Play1 to Play2

Play1 とほとんど変わらない◦ application.conf の「 application.langs 」で使用する

言語を宣言◦ 言語毎に「 messages.xx 」を conf ディレクトリに作

成◦ リソースの参照は Messages クラスを使用

同一ファイル内に複数言語のメッセージを定義したい場合は Global.scala に Play1 で使用していた ResourceGen を組み込むことができる◦ https://github.com/shunjikonishi/sqltool/blob/mast

er/app/Global.scala

国際化

Page 13: Play1 to Play2

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 をリポジトリから取得できる

依存性管理

Page 14: Play1 to Play2

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;

Page 15: Play1 to Play2

CacheAPI は set,get,remove の 3 メソッドのみとなった

Memcached を使用する場合はプラグインが必要◦ https://github.com/mumoshu/play2-memcached◦ Play1 と同じく serializable なオブジェクトをキャッシュに保存可能

◦ Case class は serializable なのでそのままキャッシュに保存可能

キャッシュ

Page 16: Play1 to Play2

https://github.com/orefalo/play2-authenticitytoken

をいれると、 Play1 相当のことができるらしい◦ Memcached もそうだが Play1 にあった機能が足りて

ない場合は有志がプラグインとして作っている印象 Play1 の時に作った自前の CSRFモジュールをプ

ラグインとして移植しても良いけど微妙。

CSRF

Page 17: Play1 to Play2

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

Page 18: Play1 to Play2

Scala の XML リテラルがそのまま使用できる◦ もっとも最近では REST API はほとんど JSON なのでほ

とんど使うことはない◦以前に XML リテラルで XML を組み立てようとした際に、

手続き的な処理が必要になってどうするのが良いのか迷った記憶があるが詳細は忘れた。

XML

Page 19: Play1 to Play2

JavaScript や CSS は「 app/assets 」に置く このフォルダに置いたファイルでは

◦ CoffeeScript が JavaScript に変換される◦ LESS が css に変換される◦ js/css の minify版が生成される (xxx.min.js でアクセ

スできる ) イメージなどは「 public 」に置く。

◦ assets に置いても良いと思うが意味がないし、多分こっちの方が速い

assets

Page 20: Play1 to Play2

Play1 と同じく実体は Cookie セッション ID がない

◦ Memcache を使用する場合に困る◦ 自前の Filter で対応

セッション

//OAuth 認証の Token を session に持たせるOk(views.html.login(url)).withSession( "token" -> token.getOAuthToken())

Page 21: Play1 to Play2

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; …}

Page 22: Play1 to Play2

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; }}

なんか冗長なのでもっときれいな書き方があると思う

Page 23: Play1 to Play2

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); }}

Page 24: Play1 to Play2

Play1 と同じように WS クラスで外部WebService にアクセス可能

Play1 とほぼ同じような使い方だが同期 API がなくなって非同期 API のみなので同期で使用したい場合は自分で Future から結果を取り出す必要がある

WebService

Page 25: Play1 to Play2

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的なモノ

Page 26: Play1 to Play2

Global クラスの宣言に WithFilters で組み込み https://github.com/shunjikonishi/sqltool/blob

/master/app/Global.scala

Filterの組み込み

object Global extends WithFilters(SessionIdFilter, AccessControlFilter) { …}

Page 27: Play1 to Play2

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 => ...}

Page 28: Play1 to Play2

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() }}

Page 29: Play1 to Play2

ログライブラリは 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

Page 30: Play1 to Play2

Play1 にあった AppID はない ので、開発/本番環境で設定を切り替えたい場合は

application.conf 自体を切り替える◦起動コマンド

play run –Dconfig.resource=prod.conf Conf ファイル内では別の conf ファイルをインク

ルードでき、キーをオーバーライドすることが可能

Application.confの切り替え

include "application.conf"

key.to.override=blah

Page 31: Play1 to Play2

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

Page 32: Play1 to Play2

スケジュール起動時の処理実装 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 _ =>}

Page 33: Play1 to Play2

Play1 でできていたことは頑張れば全部できる Play1 では標準機能でできていたことがプラグイ

ンが必要になって面倒になった気がするが、言い換えれば組み合わせの自由度が上がったということでもある◦ もっとも発展途上の印象もぬぐえない

コード量はおそらく Play1 の半分以下になる◦ case class と List があるだけでも Java の生産性をはる

かに凌駕していると思う

多分使える

まとめ