モデルのリレーションと永続化 – Ember.js入門(19)

2014.05.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

渡辺です。 久しぶりのEmber.js入門のエントリーです。

Ember.jsも気がつけば1.5.1までバージョンがあがり、細かい所まで手が届くようになってきた感があります。 未だにember-dataはBetaであることが気がかりですが、気にしないことにしましょう。

さて、前回はember-dataとREST APIによるモデルの永続化ということで、Emberアプリケーションとサーバサイドを繋げるRST APIについて基本的なことを解説しました。 今回は前回の内容を掘り下げて、モデルがリレーション(関連)を持つ場合の仕組みと実装方法を解説します。

モデルのリレーション

ember-dataではモデルのリレーションを定義することができます。

例えば、CategoryとEntryを1:N(ONE-TO-MANY)で関連付けるならば、次のようにモデルを定義します。

App.Category = DS.Model.extend({
  name: DS.attr('string'),
  entries: DS.hasMany('entry')
});
App.Entry = DS.Model.extend({
  title: DS.attr('string'),
  content: DS.attr('string'),
  category: DS.belongsTo('category')
});

詳細は、Modelの定義とリレーションを確認してください。

リレーションモデルを1つ持つレスポンスJSON

Entryモデルのようにモデルがリレーションモデルを1つ持つ場合、次のようにリレーションモデルのIDをレスポンスJSONに設定します。

{
  'entry': {
    id: 1,
    title: 'お知らせ1',
    content: 'お知らせです。',
    category: 1
  }
}

複数の結果を返す場合は、次のように配列オブジェクトを返します。

{
  'entries': [
  {
    id: 1,
    title: 'お知らせ1',
    content: 'お知らせです。',
    category: 1
  },
  {
    id: 2,
    title: 'お知らせ2',
    content: 'お知らせです。',
    category: 1
  }
  ]
}

リレーションの解決

ember-dataでは明示的にコードでリレーションの解決を行う必要はありません。 必要に応じて、自動的に必要なモデルを取得します。

例えば、次のようにエントリー一覧において、エントリーのタイトルとカテゴリの名前を表示しているとします。

<ul>
{{#each entries}}
  <li>{{title}} - {{category.name}}</li>
{{/each}}
</ul>

categoryのnameを必要とするため、id=1のcategoryが自動的に取得され、リレーションに設定されます。 言い換えれば、ember-dataを扱う場合、単一のIDを指定して、モデルを取得するAPIを用意しなければなりません。

この場合store.find('category', 1)に相当する処理が内部的に行われAPIコールが行われます。 そして、次のようなレスポンスが返ることで、カテゴリ名が表示されることになります。

{
  'category': {
    id: 1,
    name: 'NEWS'
  }
}

1:N問題対策

ember-dataでは必要に応じてAPIコールを行いリレーションされるモデルを取得します(遅延評価)。

このため、一覧のすべてのモデルに対し、それぞれのリレーションモデルを取得しにいく可能性があります。 最悪の場合、N件の一覧を取得したあと、N回のAPIコールが行われることになり、パフォーマンスの観点でも効率の観点でも好ましい状態ではありません。

この、いわゆる1:N問題への対策として、最初の一覧取得APIのレスポンスJSONにリレーションするモデルの情報を含めることができます。

エントリー一覧にカテゴリ一覧を含めるのであれば次のようなレスポンスJSONを返します。

{
  'entries': [
  {
    id: 1,
    title: 'お知らせ1',
    content: 'お知らせです。',
    category: 1
  },
  {
    id: 2,
    title: 'お知らせ2',
    content: 'お知らせです。',
    category: 1
  },
  {
    id: 3,
    title: '新商品X',
    content: '新商品が追加されました。',
    category: 1
  }
  ],
  'categories': [
  {
    id: 1,
    name: 'NEWS'
  },
  {
    id: 2,
    name: '新商品'
  }
  ]
}

このように各エントリーに含まれるカテゴリを、追加情報として付与します。 カテゴリは全て返してしまっても構いませんし、エントリーに含まれるカテゴリだけを含めても構いません。

このようなレスポンスJSONを返すことで、リレーションの解決をクライアントサイドのみで行わせることができます。 APIリクエストは1回しか行われません。

hasManyのリレーション

モデルがhasManyのリレーションを持つ場合は、配列としてJSONレスポンスを返します。

{
  'categories': [
  {
    id: 1,
    name: 'NEWS',
    entries: [1, 2]
  }
  ],
  'entries': [
  {
    id: 1,
    title: 'お知らせ1',
    content: 'お知らせです。'
  },
  {
    id: 2,
    title: 'お知らせ2',
    content: 'お知らせです。'
  }
  ]
}

双方向のリレーションを行う場合は、双方の参照を返します。

{
  'categories': [
  {
    id: 1,
    name: 'NEWS',
    entries: [1, 2]
  }
  ],
  'entries': [
  {
    id: 1,
    title: 'お知らせ1',
    content: 'お知らせです。',
    category: 1
  },
  {
    id: 2,
    title: 'お知らせ2',
    content: 'お知らせです。',
    category: 1
  }
  ]
}

推奨されるAPI設計

このようにリレーション対象のモデルをレスポンスJSONに乗せるかどうかで、パフォーマンスに大きな影響を与えます。 特に多くのオブジェクトを取得する場合は顕著になってくるでしょう。

しかし、常に関連モデルを必要とするかは別の問題となってきます。 APIのパラメータで関連モデルを含めるかどうかを指定できるようにするAPI設計を推奨します。

例えば、categoriesを含めるかどうかをincludeCategoriesというパラメータで指定するならば、クライアントでは次のようにしてパラメータを設定します。

this.store.find('entry', {
    includeCategories: true
});

サーバ側ではクエリパラメータincludeCategoriesの有無でリレーションモデルをレスポンスJSONに含めるかをハンドリングしましょう。