Play Framework 2.0.2 + AngularJS + Squeryl + lift-jsonでTODOアプリのサンプルを書いてみた

Play Framework 2.0 + AngularJS + Squeryl + lift-json を使って、AngularJSのTODOアプリのサンプルのバックエンドにPlay Framework2.0を使うような感じで、TODOアプリのサンプルを書いてみた。

tarhashi/Play-AngularJS-Sample

細かいことはソース見てください。コメント全然入ってないけど…。

Squerylのセットアップ

Squerylを使えるようにするために、以下のようなapp/GlobalSettings.scalaを作成する。今はドライバの初期化をH2 DBの場合しか書いてないが、実戦で使う場合にはPostgresなりMySQLなりを使えるようにする。

import org.squeryl.adapters.H2Adapter
import org.squeryl.internals.DatabaseAdapter
import org.squeryl.{Session, SessionFactory}
import play.api.db.DB
import play.api.Application
import play.api.GlobalSettings

object Global extends GlobalSettings {
  
  override def onStart(app: Application) {
    SessionFactory.concreteFactory = app.configuration.getString("db.default.driver") match {
      case Some("org.h2.Driver") => Some*1
      case _ => sys.error("Database driver must be org.h2.Driver")
    }
  }
  
  def getSession(adapter: DatabaseAdapter, app: Application) = Session.create(DB.getConnection()(app), adapter)
}

lift-json

どうもPlay標準のJSONモジュールは面倒な感じなので、@tototoshiさんのPlay20からlift-jsonを使えるモジュールを使ってみた。まあ、今回は大したことやってないので何使ってもよさそうでしたが。

Play20 で lift-json を使う

javascriptRouter

Play Framework2.0にはjavascriptRouterという、ajax通信するときにjavascriptからroutesの定義をタイプセーフに使うための仕組みがあるので、これを使ってみる。コントローラにjavascriptRouterを出力するためのメソッドを作成。

  def javascriptRoutes() = Action { implicit request =>
    import play.api.Routes
    Ok(
      Routes.javascriptRouter("jsRouter", Some("jQuery.ajax"))(
          routes.javascript.Application.tasks,
          routes.javascript.Application.newTask,
          routes.javascript.Application.doneTask,
          routes.javascript.Application.undoneTask
      )
    ).as("text/javascript")
  }  

routesに以下を追加。

GET     /javascript-routes          controllers.Application.javascriptRoutes 
 

そして、テンプレートに以下を追加。

<script src="@routes.Application.javascriptRoutes" type="text/javascript"></script>

で、こんな感じで使う。

jsRouter.controllers.Application.doneTask(id).ajax({
  data: {
    //...
  },
  success: function(data) {
    // ...
  },
  error: function() {
    // ...
  }
});

jsRouterをAngularJSで使う時の注意点

jsRouterを使ってデータの取得や更新をやろうとして、最初正常に反映されなくてハマった。最初、以下のようなコードで変更を反映しようとした。

function TodoCtrl($scope) {
  // ...

  $scope.addTodo = function() {
    jsRouter.controllers.Application.newTask().ajax({
      data: { label: $scope.todoText },
      success: function(data) {
        $scope.todos.push(data);
        $scope.todoText = '';
      },
      error: function() {
        alert("error:addTask");
      }
    });
  };

  // ...
  
}

通常AngularJSでajaxするときは$httpという専用のやつを使うのが推奨されているのだけど、jsRouterを使うとjQuery.ajaxを使って通信することになる。で、非同期で通信するとコールバック関数からの値の変更がAngularJSに検出されないようで、画面に変更が反映されない。

これを解決するための1つ目の方法はasync=falseとして同期通信にすること。でも全部を同期通信にしてしまうのはあまりにいけてない。で、もう1つの方法が、変更を通知する関数を使うこと。$scope.$applyという関数を使ってsuccessの処理を以下のように書き換える。

      success: function(data) {
        $scope.$apply(function(){
          $scope.todos.push(data);
          $scope.todoText = '';
        });
      },

感想

AngularJSっていうか、Javascript MVCフレームワークは使ったことなかったんだけど、なかなか便利。パフォーマンスについてどうなのか気になる所だけど、何かで使ってみたい。画面遷移を最小限にして、サーバサイドの処理のほとんどをJSONを返すAPIを実装していく形にできるのも良い。

AngularJSと組み合わせるときにajax通信をjavascriptRouterを使うべきか、AngularJS標準のでやるかは微妙なところ。AngularJSと使う以外の用途だったら普通に使えそう。

追記

Play2.1が出たので、2.1対応をやってみた。

Play2.0で書いたサンプルを2.1対応にしてみる

*1:) => getSession(new H2Adapter, app