Play1とPlay2の比較 - コントローラ編

こんにちは。小西です。
前回の予告どおり今回はPlay1とPlay2のコントローラの比較をやってみたいと思います。

予想ですがPlay1を使い慣れた人がPlay2にやってきた場合最初にこのコントローラの違いにとまどい「Play1の方が便利じゃん。。。」と思うような気がします。
Play1の頃のやり方からの推測で「多分こうだろ」と勘で使おうとすると確実にはまります



<Play1のコントローラ>

まず僕がPlay1でコントローラを書く場合の典型的なコードを示します。

pubilc static void process(String s1, String s2, String s3, int n1, boolean b1) {
    if (s1 == null || s2 == null || s3 == null) {
        badRequest();
    }
    ResultObject obj = app.process(s1, s2, s3, n1, b1);
    render(obj);
}

 

特に説明の必要はないかと思いますがコントローラを作る際のパターンとしては以下になります。

  1. ガード条件として最低限の引数のエラーチェックをしてエラーの場合は問答無用でbadRequest
  2. 独立したApplicationオブジェクトに処理を委譲して結果を取得
  3. ビューに結果を渡してレンダリング(またはrenderJSONでJSONで結果を返す)

 

アプリケーションロジック自体はフレームワークに依存したくないのでほとんどの場合ApplicationオブジェクトはPlay非依存で別に作成します。(なのでPlayのモデルクラス群は実はほとんど使ったことがなく、この比較記事にもモデル編はありません。)

そのため、コントローラのコードはほぼ100%ワンパターンに上のようなコードになります。

Play1では引数の追加などの際にただ単純にコントローラの引数を追加するだけで良く、intやbooleanへの型変換も自動でやってくれるあたりが素晴らしいです。

また、routesで

*       /{controller}/{action}                  {controller}.{action}

を定義しておけばメソッドの追加の際にも他の設定ファイルを変更する必要がなくいきなりコントローラにメソッドを追加できまるのも楽で良いですね。

 

 

<Play2のコントローラ - 駄目な例>

さて、先のサンプルコードを次はPlay2で書き直してみます。
ろくにドキュメントも読まずに僕は最初こう書きました

 

def process(s1: String, s2: String, s3: String, n1: Int, b1: Boolean) = Action {
    if (s1 == null || s2 == null || s3 == null) {
        BadRequest
    } else {
        ResultObject obj = app.process(s1, s2, s3, n1, b1)
        Ok(view.html.process(obj))
    }
}

ほぼPlay1で書いたバージョンの逐語訳です。
これ、動くには動きますがかなり問題があります。

 

routesの問題
まず最初に単純にコントローラにメソッドを追加しただけではそのメソッドをHttpリクエストで実行できるようにはなりません。
Play2のroutesにはワイルドカード構文がないのでメソッド追加時には必ずroutesにもメソッド定義を追加する必要があります。

routesの構文はPlay1の時と同じく空白区切りで先頭からHTTPメソッド、URIパターン、実行するActionの順で指定しますが、HTTPメソッドでもワイルドカードは使えないのでGETとPOSTは区別して定義する必要があります。

GET /test/process controllers.Test.process

書式としては上のような感じでPlay1とほとんど変わりませんが、先のメソッド定義の場合このroutes定義ではまだエラーになります。
Play2ではroutesのメソッド定義は引数まで正確に定義する必要があるからです。

GET /test/process controllers.Test.process(s1: String, s2: String, s3: String, n1: Int, b1: Boolean)

これで一応先のメソッドはブラウザから実行できるようになります。
ブラウザのURLで「http:.../test/process?s1=aaa...」のようにパラメータをつけて実行すると正常に実行できていることが確認できます。

次のテストとして「/test/process」のようにパラメータを外して実行すると「400 BadRequest」が返ってきますが、これは実は自前のnullチェックが機能したからではありません。

Play1ではコントローラの引数に指定したパラメータが見つからない場合はnullまたは0,falseなどのデフォルト値でメソッドが実行されましたが、Play2ではメソッド実行前にBadRequestが返ります

また、routesのHTTPメソッド定義をPOSTに変更して、パラメータを設定したPOSTリクエストを発行してもやはりBadRequestが返ってきます。
何故かと言うとroutesのメソッド定義で指定した引数にマップされるのはURLパラメータまたはURIパターン内で定義した変数だけでPOSTパラメータは対象外だからです。

 

 

ここまでに書いた内容ではPlay2に良いところはありません。
Play1に慣れた人ならば「いくらなんでもそれはないんじゃないの?」と思うことでしょう。
その通りです。

 

実際にはPlay2ではURIパターンを使用する場合以外はパラメータをこのように引数で指定することはありません。
ただ、routesのドキュメントにも例つきでこのやり方が載っているしPlay1を使ってた人ならば、ほとんどこの道を通ると思うんですよね。。。(--

 

余談ですが何故引数まで正確に必要かと言うとPlay2のroutingはreflectionではなく静的にメソッドをバインドしたクラスをジェネっているからだと思います。(つまりroutingのコストはPlay1よりもはるかに小さいと思われます)

<Play2のコントローラ - 正しい例>

Play2のコントローラでパラメータを処理する書き方は実際には以下のようになります。


def process = Action { implicit request =>
    val form = Form(
        tuple(
            "s1" -> text,
            "s2" -> nonEmptyText,
            "s3" -> nonEmptyText,
            "n1" -> number(min = 0, max = 100),
            "b1" -> boolean
        )
    )
    form.bindFromRequest.fold(
        e => BadRequest(e.errors.head.message),
        p => {
            val obj = app.process(p._1, p._2, p._3, p._4, p._5)
            Ok(views.html.process(obj))
        }
    )
}

細かい文法は置いておいて雰囲気はわかるでしょうか?

Formとしてパラメータを定義してそれをリクエストからバインドしています。

サンプル的にFormの中でnumberのminやmaxを定義していますがFormには簡単なガード条件を記述するだけのスペックがあります。(僕はあまり使いませんでしたがPlay1でもAnnotationやValidationで同じようなことができます。)

Form#foldメソッドは第1引数にエラーがあった場合に実行する関数、第2引数にエラーがない場合に実行する関数を指定できるのでここでガード条件の分岐を行うことができます。

routesでのパラメータ定義も不要になるので、これならば十分にシンプルでメソッドの仕様変更などの場合にもあまりストレスはなさそうです。

 

ここではパラメータのマッピングにTupleを使用していますが、それ以外にもcase classを使用してオブジェクトにマッピングできるので、自由度という点ではPlay2の方がPlay1よりも優れていると思います。

ただ、慣れの問題もあるにしても、どちらがわかりやすいかと言われればPlay1の方じゃないかとは思いますね。

 

 

<ちなみにFileアップロードを扱う場合は?>

FormではFileを扱うことはできないようです。

ファイルとその他のパラメータを両方扱う場合のコードがいまいち綺麗に書けないなーと思ってるんですが、今のところアップロードを扱う場合のコードは以下のように書いています。

def upload = Action(parse.multipartFormData) { implicit request =>
    val form = Form (
        tuple(
            "sheet" -> text,
            "download" -> boolean
        )
     ).bindFromRequest
    val file = request.body.file("file")
    if (file.isEmpty || form.hasErrors) {
        BadRequest
    } else {
        doUpload(file.get, form.get._1, form.get._2)
    }
}

Actionに謎の引数が増えてますがMultipartFormDataを扱う場合は使用するパーサーをそれ用に変更しなければなりません。

このあたりをちゃんと理解しようとするとソースのかなり深いところまで読む羽目になるのである程度の所で挫折しました。(--

 

いずれにせよPlay1のメソッドの引数をFileクラスで宣言すれば良かっただけというお手軽さとは比べるべくもない感じです。

ざっと見た感じだとFileもFormで扱えるようにすることはおそらく可能だと思うので、ここはPlay2の開発チームにもうちょっとがんばってほしいところです。

コメント(0)