Ember Data を Rails と一緒に使う


Ember Data は、Beta なこともあり、まだあまり情報がないので整理しておきます。Guides にもあまり書かれていないので、Discussion Forum や Stackoverflow を参考にしました。

## ActiveModelAdapter

RESTAdapter という一見デフォルト風の Adapter がありますが、Rails の snakecase な変数名から JavaScript な camelcase に変換してくれる ActiveModelAdapter が使いやすいと思います。

Rails 側では Active Model Serializer を使って JSON を出力します。

## Rails の routing

衝撃的なことに、AcriveModelAdapter は（RESTAdapter も）ネストした URL に対応していません。Adapter に手を入れれば対応させることはできるかもしれませんが、ひとまずそのままいきます。

```config/routes.rb
resources :posts, except: [:new, :edit], defaults: { format: 'json' }
resources :comments, except: [:new, :edit], defaults: { format: 'json' }
```

## ActiveModel::Serializers

[ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers) をインストールして使います。

```Gemfile
gem 'active_model_serializer'
```

```shell
bundle install
./bin/rails g serializer post
./bin/rails g serializer comment
```

Serializer を用意しておくだけで、controller で `respond_to` した際に、自動的に Serializer を使ってくれます。

### 子の中身も親のレスポンスに含める場合

関連を id の配列として出力するため `embed :ids` と書きます。どの model にも書くので、親クラスを作ると便利です。

```app/serializers/applicaton_serializer.rb
class ApplicationSerializer < ActiveModel::Serializer
  # Compatible with ember-data.
  embed :ids, include: true
end
```

```app/serializers/post_serializer.rb
class PostSerializer < ApplicationSerializer
  attributes :id, :title, :body
  has_many :comments
end
```

```app/serializers/comment_serializer.rb
class CommentSerializer < ApplicationSerializer
  attributes :id, :post_id, :body
end
```

`embed :ids, include: true` にすると、Post の JSON を返す時、以下のように comments の中身も一緒に含めてくれます。

```/posts.json
{
  "comments": [
    {
      "id": 123,
      "body": "Nice post!"
    }
  ],
  "posts": [
    {
      "id": 345,
      "title": "Hello, World!",
      "body": "I'm so happy to meet you, World!",
      "comment_ids": [123]
    }
  ]
}
```

この際、Ember 側の model の hasMany を `async: false` にします。

```models/post.js
App.Post = DS.Model.extend({
  title: DS.attr('string'),
  body: DS.attr('string'),
  comments: DS.hasMany('comment', { async: false })
});
```

### 子の中身を親のレスポンスに含めない場合

Comment は別の JSON でリクエストさせたい場合は、`embed :ids, include: false` にして、Post のレスポンスに Comment の中身を含めないようにします。false がデフォルトです。

```app/serializers/applicaton_serializer.rb
class ApplicationSerializer < ActiveModel::Serializer
  # Compatible with ember-data.
  embed :ids
end
```

Ember 側は `async: true` にしておきます。[こっちの作りが、今後 Ember Data のデフォルトになる可能性が高そうです。](https://github.com/emberjs/data/issues/1443#issuecomment-26607013)

```models/post.js
App.Post = DS.Model.extend({
  title: DS.attr('string'),
  body: DS.attr('string'),
  comments: DS.hasMany('comment', { async: true })
});
```

こうしておくと、Post が読み込まれた後、Comment は別途 Post ごとに `/comments?ids[]=1&ids[]=2` のような URL で問い合わせられます。実際は URL ecnode されていて `/comments?ids%5b%5d=1&ids%5b%5d=2` となっています。

Rails の controller でフィルタリング処理を実装しておかないと、毎回全部のコメントを返してしまい無駄になります。それでも動くようでしたが。

```ruby
class CommentController < ApplicationController
  respond_to :json

  def index
    if params[:ids].is_a?(Enumerable)
      @comments = Comment.where(id: params[:ids])
    else
      @comments = Comment.all
    end
    respond_with @comments
  end
end
```

## 子の保存

子を保存しても、クライアント側では自動的に親に追加されないので、自分で追加します。

```controllers/comment_new_controller.js
App.CommentNewController = Ember.ObjectController.extend({
  // PostController を呼び出すのに必要。
  needs: 'post',
  actions: {
    createComment: function() {
      // PostController の model を取得。
      var post = this.get('controllers.post.content');
      
      var comment = this.store.createRecord('comment', {
        body: this.get('body'),
        post: post
      };

      comment.save().then(function() {
        post.get('comments').pushObject(comment);
      });
    }
  }
});
```

Rails 側も resources をネストさせていないので、ちょっと変わった感じになります。

```ruby
class CommentController < ApplicationController
  respond_to :json

  before_action :set_post, only: [:create]

  def create
    @comment = @post.comments.create(comment_params)
    respond_with @comment
  end

  private

  def set_post
    @post = Post.find(comment_params[:post_id])
  end

  def comment_params
    params.require(:comment).permit(:body, :post_id)
  end
end
```

という感じで、なんとかひととおり動くようになりました。

Ember の他の部分に比べ、Ember Data はまだまだこれからな感じですね。

## 参考
hasMany の async: true って何？
http://discuss.emberjs.com/t/what-is-an-async-relationship-async-true-vs-async-false/4107/10