実践入門 Ember.js

第7回 データの永続化(Ember Data)

この記事を読むのに必要な時間:およそ 11 分

REST APIを扱う

ここまででストアに読み込んだデータを扱うことができるようになりました。ここからはREST API経由でデータの取得とデータの保存を解説します。

今までデータを準備していたApp.ApplicationRouteは不要になるので,クラスごと削除してください。

そして,PostsRoutemodel()メソッドではall()メソッドの代わりにfind()メソッドを利用します。

App.PostsRoute = Ember.Route.extend({
  model: function() {
    return this.store.find('post');
  }
});

これで,必要に応じてデータを取得するように設定されました。

しかし,まだデータを保存するサーバを準備していません。サーバの実装も解説したいところですが,それは「実践入門 Ember.js」の枠をはるかに超えてしまうため今回は割愛させてください。動作するサンプルのAPIサーバを用意したので,今回の記事ではこのサーバを使うよう設定します※5⁠。

APIサーバのURLは次のとおりです。

※5
  • サンプルで利用しているサーバはRuby on Railsで作成しました。ソースコードはGitHubで公開してあるので,興味ある方は参照してください。

  • それでは,引き続きこのAPIサーバを使うための設定を解説します。

    DS.Adapter

    Ember Dataでデータの取得元を指定するためにはDS.Adapterを利用します。Ember DataにはDS.Adapterのサブクラスがいくつか用意されていて,デフォルトではREST APIをデータの取得元として扱うDS.RESTAdapterが利用されます※6⁠。

    ※6
    DS.RESTAdapter以外には,ローカルのデータを扱うDS.FixtureAdapterRubyのJSON生成ライブラリであるActiveModel::Serializersが生成するJSONを返すREST APIと親和性の高いDS.ActiveModelAdapterが提供されています。

    DS.RESTAdapterは,以下の規則に沿ってHTTPリクエストを発行します。

    メソッド URL
    store.find('post'); /posts
    store.find('post', 1); /posts/1
    store.find('post', {page: 2}); /posts?page=2

    この規則はカスタマイズ可能ですが,なるべくこの規則に沿ってAPIを実装するほうが手間が少なくなります。

    では先ほど紹介したAPIサーバを利用するための設定を行います。

    App.ApplicationAdapter = DS.RESTAdapter.extend({
      host: 'https://tricknotes-gihyo-ember-07.herokuapp.com',
      namespace: 'v1'
    });

    すべてのモデルに共通するアダプタはApplicationAdapterという名前で定義します。もしPostComment専用のアダプタを作成したい場合は,PostAdapterCommentAdapterといった「モデルのクラス名+Adapter」というクラス名のアダプタを定義してください。

    ではここで登場した設定項目を説明します。

    host

    APIサーバのスキームとホスト名を指定します。指定しない場合は現在のURLwindow.location.originが利用されます。

    namespace

    APIの名前空間を指定します。例えばnamespace: 'v1'が設定されていると,/posts/1というURLの代わりに/v1/posts/1に対してリクエストが発行されます。今回利用するAPIサーバではいろいろな形式のJSONを扱う例を紹介したいので,各セクション毎に別のnamespaceを用意しました。

    これで,サンプルサーバを利用する設定が完了しました。

    次はAPIサーバが返すべきJSONの形について考えてみましょう。

    DS.Serializer

    JSONに含まれるデータとモデルのマッピングにはDS.Serializerを使います。デフォルトでは,DS.RESTSerializerというREST APIに特化したシリアライザが利用されます。

    このDS.RESTSerializerをカスタマイズするとクライアントサイドで自由なマッピングを指定できますが,まずはカスタマイズをせずにこのシリアライザが期待する形のJSONを準備することにします。

    ブログ記事一覧で返すべきJSONは次のような形になります。

    /v1/posts.json

    {
      "posts": [
        {
          "id": 1,
          "title": "はじめての Ember.js",
          "body": "これから Ember.js を始めようという方向けの記事です。"
        },
        {
          "id": 2,
          "title": "公式サイトの歩き方",
          "body": "http://emberjs.com/ の解説です。"
        }
      ],
      "comments": [
        {
          "id": 1,
          "text": "はじめまして",
          "post": 1
        },
        {
          "id": 2,
          "text": "入門しました",
          "post": 1
        },
        {
          "id": 3,
          "text": "詳しい説明を知りたいときはまず参考にします。",
          "post": 2
        }
      ]
    }

    ポイントは次の2点です。

    • JSONのルートのキーはモデル名を使う(オブジェクトが複数あればモデル名の複数形を指定する)
    • 関連するデータがあればレスポンスに含める(今回の例では/postsへのリクエストに対してcommentsをレスポンスに含めている)

    これを満たすことで,DS.RESTSerializerがレスポンスのJSONをモデルにマッピングしてくれます。

    また,記事詳細で返すべきJSONの形は以下のとおりです。

    /v1/posts/1.json

    {
       "post": {
          "id": 1,
          "body": "これから Ember.js を始めようという方向けの記事です。",
          "title": "はじめての Ember.js"
       },
       "comments": [
          {
             "id": 1,
             "text": "はじめまして",
             "post": 1
          },
          {
             "id": 2,
             "text": "入門しました",
             "post": 1
          }
       ]
    }

    記事一覧のJSONとの違いは,全件の記事ではなく一件の記事を対象としたものになったことです。JSONのルートのキーも"posts"から"post"になっています。

    postとcommentsのような関連するリソースを扱う場合,サーバでこの形式のJSONを出力するのが困難な場合があります。例えば,次のような場合です。

    • JSON生成に利用するライブラリの都合上,"post"と"comments"を並列に並べるのが困難である
    • commentsの数が膨大で全部をpostのレスポンスに含めるとパフォーマンスが劣化する

    こういった場合に対応するため,ここからはこの形式以外のJSONを扱う方法を紹介します。

    子リソースを親リソースに埋め込む

    ここでは,次のようにpostsの中にcommentsが埋め込まれている場合のJSONの扱い方を解説します。

    /v2/posts.json

    {
      "posts": [
        {
          "id": 1,
          "body": "これから Ember.js を始めようという方向けの記事です。",
          "title": "はじめての Ember.js",
          "comments": [
            {
              "id": 1,
              "text": "はじめまして"
            },
            {
              "id": 2,
              "text": "入門しました"
            }
          ]
        },
        {
          "id": 2,
          "body": "http://emberjs.com/ の解説です。",
          "title": "公式サイトの歩き方",
          "comments": [
            {
              "id": 3,
              "text": "詳しい説明を知りたいときはまず参考にします。",
            }
          ]
        }
      ]
    }

    まずはアダプタの設定を変更しましょう。

    App.ApplicationAdapter = DS.RESTAdapter.extend({
      host: 'https://tricknotes-gihyo-ember-07.herokuapp.com',
      namespace: 'v2'
    });

    この形のJSONから子リソースを取得したい場合,DS.EmbeddedRecordsMixinを利用します。

    App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
      attrs: {
        comments: {embedded: 'always'}
      }
    });
    attrs

    attrsDS.JSONSerializerDS.RESTSerializerの親クラス)で提供されているプロパティです。ここではデータとモデルのプロパティの対応を設定できます。

    embedded

    DS.EmbeddedRecordsMixinを使うと,attrsでデータの取得時と送信時での子リソースの扱い方を指定きます(データ送信時の振る舞いについては後述)。ここで指定している{embedded: 'always'}というのは,⁠データ送信時・取得時ともに子リソースを埋め込む」ということを意味します。

    非同期で子リソースを取得する(ID参照)

    さて,次は子リソースを必要になったタイミングで取得する方法を解説します。JSONには子リソースのIDが含まれますが,実体は含まれていません。

    /v3/posts.json

    {
      "posts": [
        {
          "id": 1,
          "title": "はじめての Ember.js",
          "body": "これから Ember.js を始めようという方向けの記事です。",
          "comments": [
            1,
            2
          ]
        },
        {
          "id": 2,
          "title": "公式サイトの歩き方",
          "body": "http://emberjs.com/ の解説です。",
          "comments": [
            3
          ]
        }
      ]
    }

    まずはアダプタの設定を変更しましょう。

    App.ApplicationAdapter = DS.RESTAdapter.extend({
      host: 'https://tricknotes-gihyo-ember-07.herokuapp.com',
      namespace: 'v3'
    });

    「子リソースを親リソースの埋め込む」で作成したPostSerializerは不要なので削除します。

    そして,関連に非同期であることを示すフラグを設定します。

    App.Post = DS.Model.extend({
      // ...
    
      comments: DS.hasMany('comment', {async: true})
    });

    では,この状態で実際に動かしてみましょう。

    記事詳細を表示すると,少し遅れてコメントが表示されるのが確認できます。

    実際にどのタイミングでコメントが取得されているのかというと,postテンプレート中でコメントが参照されたタイミングです。

    {{#each comment in model.comments}}

    ではここで,開発者ツールを利用してこのときのHTTPリクエストを確認してみましょう。

    v3.1-comments

    画像

    コメントの数だけHTTPリクエストが発行されています。もしコメントが大量に存在する場合コメントの数だけHTTPリクエストが発行されるため,表示までに時間がかる場合があります。これを避けるためには,以下のオプションを設定して一度に必要なコメントすべてを取得するようにします。

    App.ApplicationAdapter = DS.RESTAdapter.extend({
      // ...
    
      coalesceFindRequests: true
    });

    v3.2-comments

    画像

    コメントを取得するURLに,idsというパラメータ付きでリクエストが発生するようになりました。このAPIサーバがこの形式に対応していればこちらの方がリクエスト数を減らせます。

    非同期で子リソースを取得する(URL参照)

    先ほどと同じく,子リソースが必要になったタイミングで取得するという方法ですが,先ほどのID指定の方法とは違って子リソースを表すURLを指定する方法を解説します。

    linksというプロパティを使うと,子リソースを取得するURLを指定できます。JSONは以下の形になります。

    /v4/posts.json

    {
      "posts": [
        {
          "body": "これから Ember.js を始めようという方向けの記事です。",
          "title": "はじめての Ember.js",
          "id": 1,
          "links": {
            "comments": "https://tricknotes-gihyo-ember-07.herokuapp.com/v4/posts/1/comments"
          }
        },
        {
          "body": "http://emberjs.com/ の解説です。",
          "title": "公式サイトの歩き方",
          "id": 2,
          "links": {
            "comments": "https://tricknotes-gihyo-ember-07.herokuapp.com/v4/posts/2/comments"
          }
        }
      ]
    }

    ではこのJSONを扱うためにアダプタを設定しましょう。

    App.ApplicationAdapter = DS.RESTAdapter.extend({
      host: 'https://tricknotes-gihyo-ember-07.herokuapp.com',
      namespace: 'v4'
    });

    この状態で,どのようなリクエストが発行されているか開発者ツールで確認してみましょう。

    v4-comments

    画像

    linksプロパティで指定したURLに対してリクエストが発行されています。

    子リソースを扱ういくつかの方法を紹介しましたが,どの方法が良い/悪いというのはありません。アプリケーション毎に適切だと考えられる方法を選択してください。

    著者プロフィール

    佐藤竜之介(さとうりゅうのすけ)

    株式会社えにしテック所属。JavaScriptとRubyが好きなウェブ系プログラマ。オープンソースソフトウェアに強い関心がありEmber.jsにも数多くのパッチを送っている。

    ブログ:http://tricknotes.hateblo.jp/
    Twitter:@tricknotes