残り火データ:残り火データライブラリの包括的なチュートリアル

公開: 2022-03-11

Ember Data(別名ember-dataまたはember.data)は、Ember.jsアプリケーションでモデルデータを堅牢に管理するためのライブラリです。 Ember Dataの開発者は、基盤となる永続化メカニズムに依存しないように設計されているため、ストリーミングWebSocketやローカルのIndexedDBストレージと同様にHTTP経由のJSONAPIでも機能すると述べています。 ActiveRecordのようなサーバー側のオブジェクトリレーショナルマッピング(ORM)にある多くの機能を提供しますが、ブラウザーのJavaScriptの独自の環境向けに特別に設計されています。

Ember Dataの作成には時間がかかる場合がありますが、一度作成すると、投資する価値が十分にあることがわかります。 最終的には、システムの開発、拡張、および保守がはるかに簡単になります。

APIがEmberDataモデル、アダプター、およびシリアライザーを使用して表される場合、各関連付けは単にフィールド名になります。 これにより、各アソシエーションの内部詳細がカプセル化されるため、コードの残りの部分がアソシエーション自体の変更から保護されます。 コードの残りの部分は、たとえば、特定の関連付けが多態的であるか、または多くの関連付けのマップの結果であるかどうかを気にしません。

さらに、コードベースが期待するのはモデルのJSON、XML、またはYAML表現ではなく、モデル上のフィールドと関数であるため、コードベースは、重要な場合でも、バックエンドの変更から大部分が隔離されます。

このチュートリアルでは、Ember Dataの最も顕著な機能を紹介し、実際の例に焦点を当てて、コードチャーンを最小限に抑えるのにどのように役立つかを示します。

いくつかのより高度なEmberDataのトピックと例について説明する付録も提供されています。

注:この記事は、Ember.jsに関する基本的な知識があることを前提としています。 Ember.jsに慣れていない場合は、人気のあるEmber.jsチュートリアルで紹介を確認してください。 また、ロシア語、ポルトガル語、スペイン語で利用できるフルスタックのJavaScriptガイドもあります。

Emberデータの価値提案

Emberデータライブラリが顧客のニーズに対応するのにどのように役立つかの例。

簡単な例から始めましょう。

基本的なブログシステムのコードベースが機能しているとします。 システムには、相互に多対多の関係を持つ投稿とタグが含まれています。

Pagesをサポートするための要件が​​得られるまで、すべて問題ありません。 要件には、WordPressでページにタグを付けることが可能であるため、同様にタグを付けることができるはずであるとも記載されています。

そのため、タグは投稿だけに適用されるのではなく、ページにも適用される可能性があります。 その結果、タグと投稿の間の単純な関連付けは適切ではなくなります。 代わりに、次のような多対多の片側ポリモーフィック関係が必要になります。

  • 各投稿はタグ付け可能であり、多くのタグがあります
  • 各ページはタグ付け可能であり、多くのタグがあります
  • 各タグには、多くのポリモーフィックなタグ可能オブジェクトがあります

この新しい、より複雑な一連の関連付けに移行すると、コード全体に重大な影響が及ぶ可能性があり、その結果、多くのチャーンが発生します。 ポリモーフィックな関連付けをJSONにシリアル化する方法がわからないため、おそらくGET /posts/:id/tagsGET /pages/:id/tagsなどのAPIエンドポイントをさらに作成するだけです。 次に、既存のJSONパーサー関数をすべて破棄し、追加された新しいリソース用に新しい関数を記述します。 うーん。 退屈で苦痛。

次に、EmberDataを使用してこれにどのようにアプローチするかを考えてみましょう。

Ember Dataでは、この変更された一連の関連付けに対応するには、次の場所から移動するだけです。

 App.Post = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Tag = DS.Model.extend({ post: DS.belongsTo('post', {async: true}) });

に:

 App.PostType = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Post = App.PostType.extend({ }); App.Page = App.PostType.extend({ }); App.Tag = DS.Model.extend({ posts: DS.hasMany('postType', {polymorphic: true, async: true}) });

コードの残りの部分で生じるチャーンは最小限に抑えられ、ほとんどのテンプレートを再利用できます。 特に、Postのtagsの関連付け名は変更されないことに注意してください。 さらに、コードベースの残りの部分は、 tagsの関連付けの存在のみに依存しており、その詳細には気づいていません。

Emberデータ入門書

実際の例に飛び込む前に、EmberDataの基本を確認しましょう。

ルートとモデル

Ember.jsでは、ルーターはテンプレートの表示、データの読み込み、その他のアプリケーションの状態の設定を担当します。 ルーターは現在のURLを定義したルートと照合するため、ルートはテンプレートが表示するモデルを指定する責任があります(EmberはこのモデルがEmber.Objectのサブクラスであると想定しています)。

 App.ItemsRoute = Ember.Route.extend({ model: function(){ // GET /items // Retrieves all items. return this.modelFor('orders.show').get('items'); } });

Ember Dataは、 Ember.Object DS.Model提供し、便利な単一レコードまたは複数レコードの保存や更新などの機能を追加します。

新しいモデルを作成するには、 DS.Modelのサブクラスを作成します(例: App.User = DS.Model.extend({}) )。

Ember Dataは、サーバーからの明確に定義された直感的なJSON構造を期待し、新しく作成されたレコードを同じ構造化されたJSONにシリアル化します。

Ember Dataは、モデルを操作するためのDS.RecordArrayのような配列クラスのスイートも提供します。 これらには、1対多または多対多の関係の処理、データの非同期取得の処理などの責任があります。

モデル属性

基本的なモデル属性は、 DS.attrを使用して定義されます。 例えば:

 App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string') });

DS.attrによって作成されたフィールドのみが、レコードを作成または更新するためにサーバーに渡されるペイロードに含まれます。

DS.attrは、 stringnumberbooleandateの4つのデータ型を受け入れます。

RESTSerializer

デフォルト:

  • Ember Dataは、API応答からオブジェクトを作成するため(逆シリアル化)およびAPIリクエスト用のJSONを生成するため(シリアル化)にRESTSerializerを採用しています。
  • RESTSerializerは、 DS.belongsToによって作成されたフィールドに、サーバーからのJSON応答にuserという名前のフィールドが含まれていることを想定しています。 そのフィールドには、参照されるレコードのIDが含まれます。
  • RESTSerializerは、関連付けられた注文のIDを使用してAPIに渡されるペイロードにuserフィールドを追加します。

たとえば、応答は次のようになります。

 GET http://api.example.com/orders?ids[]=19&ids[]=28 { "orders": [ { "id": "19", "createdAt": "1401492647008", "user": "1" }, { "id": "28", "createdAt": "1401492647008", "user": "1" } ] }

また、注文を保存するためにRESTSerializerによって作成されたHTTPリクエストは、次のようになります。

 POST http://api.example.com/orders { "order": { "createdAt": "1401492647008", "user": "1" } }

1対1の関係

たとえば、各ユーザーが一意のプロファイルを持っているとします。 ユーザーとプロファイルの両方でDS.belongsToを使用して、EmberDataでこの関係を表すことができます。

 App.User = DS.Model.extend({ profile: DS.belongsTo('profile', {async: true}) }); App.Profile = DS.Model.extend({ user: DS.belongsTo('user', {async: true}) });

次に、 user.get('profile')との関連付けを取得するか、 user.set('profile', aProfile)設定します。

RESTSerializerは、関連付けられたモデルのIDが各モデルに提供されることを想定しています。 例えば:

 GET /users { "users": [ { "id": "14", "profile": "1" /* ID of profile associated with this user */ } ] } GET /profiles { "profiles": [ { "id": "1", "user": "14" /* ID of user associated with this profile */ } ] }

同様に、関連するモデルのIDがリクエストペイロードに含まれます。

 POST /profiles { "profile": { "user": "17" /* ID of user associated with this profile */ } }

1対多および多対1の関係

投稿に多くのコメントがあるモデルがあるとします。 Ember Dataでは、この関係をDS.hasMany('comment', {async: true})およびコメントのDS.belongsTo('post', {async: true})で表すことができます。

 App.Post = DS.Model.extend({ content: DS.attr('string'), comments: DS.hasMany('comment', {async: true}) }); App.Comment = DS.Model.extend({ message: DS.attr('string'), post: DS.belongsTo('post', {async: true}) });

次に、 post.get('comments', {async: true})で関連付けられたアイテムを取得し、 post.get('comments').then(function(comments){ return comments.pushObject(aComment);})で新しい関連付けを追加できます。 post.get('comments').then(function(comments){ return comments.pushObject(aComment);})

サーバーは、投稿に対する対応するコメントのIDの配列で応答します。

 GET /posts { "posts": [ { "id": "12", "content": "", "comments": ["56", "58"] } ] }

…そして各コメントのID付き:

 GET /comments?ids[]=56&ids[]=58 { "comments": [ { "id": "56", "message": "", "post": "12" }, { "id": "58", "message": "", "post": "12" } ] }

RESTSerializerは、関連付けられた投稿のIDをコメントに追加します。

 POST /comments { "comment": { "message": "", "post": "12" /* ID of post associated with this comment */ } }

ただし、デフォルトでは、RESTSerializerはDS.hasManyに関連付けられたIDを、シリアル化するオブジェクトに追加しません。これらの関連付けは「多」側で指定されるためです(つまり、 DS.belongsTo関連付けを持つもの)。 したがって、この例では、Postには多くのコメントがありますが、それらのIDはPostオブジェクトに追加されません

 POST /posts { "post": { "content": "" /* no associated post IDs added here */ } }

DS.hasMany IDもシリアル化するように「強制」するには、EmbeddedRecordsMixinを使用できます。

多対多の関係

私たちのモデルでは、著者が複数の投稿を持っている可能性があり、投稿が複数の著者を持っている可能性があるとしましょう。

Ember Dataでこの関係を表すには、PostでDS.hasMany('author', {async: true})を使用し、作成者でDS.hasMany('post', {async: true})を使用できます。

 App.Author = DS.Model.extend({ name: DS.attr('string'), posts: DS.hasMany('post', {async: true}) }); App.Post = DS.Model.extend({ content: DS.attr('string'), authors: DS.hasMany('author', {async: true}) });

次に、 author.get('posts')に関連付けられたアイテムを取得し、 author.get('posts').then(function(posts){ return posts.pushObject(aPost);})に新しい関連付けを追加できます。

サーバーは、対応するオブジェクトのIDの配列で応答します。 例えば:

 GET /authors { "authors": [ { "id": "1", "name": "", "posts": ["12"] /* IDs of posts associated with this author */ } ] } GET /posts { "posts": [ { "id": "12", "content": "", "authors": ["1"] /* IDs of authors associated with this post */ } ] }

これは多対多の関係であるため、RESTSerializerは関連するオブジェクトのIDの配列を追加します。 例えば:

 POST /posts { "post": { "content": "", "authors": ["1", "4"] /* IDs of authors associated with this post */ } }

実際の例:既存の注文システムの強化

既存の注文システムでは、各ユーザーには多くの注文があり、各注文には多くのアイテムがあります。 私たちのシステムには、製品を注文できる複数のプロバイダー(つまり、ベンダー)がありますが、各注文には単一のプロバイダーからのアイテムのみを含めることができます。

新しい要件#1:単一の注文で複数のプロバイダーからのアイテムを含めることができるようにします。

既存のシステムでは、プロバイダーと注文の間には1対多の関係があります。 ただし、複数のプロバイダーからのアイテムを含めるように注文を拡張すると、この単純な関係はもはや適切ではなくなります。

具体的には、プロバイダーが注文全体に関連付けられている場合、拡張システムでは、その注文には他のプロバイダーから注文されたアイテムも含まれる可能性があります。 したがって、各注文のどの部分が各プロバイダーに関連しているかを示す方法が必要です。 さらに、プロバイダーが注文にアクセスするときは、顧客が他のプロバイダーに注文した可能性のある他のアイテムではなく、注文されたアイテムのみを表示する必要があります。

1つのアプローチは、2つの新しい多対多の関連付けを導入することです。 1つは注文とアイテムの間、もう1つは注文とプロバイダーの間です。

ただし、物事を簡単にするために、「ProviderOrder」と呼ばれる新しい構造をデータモデルに導入します。

製図関係

拡張データモデルは、次の関連付けに対応する必要があります。

  • ユーザーと注文の間の1対多の関係(各ユーザーは0からnの注文に関連付けられる場合があります)およびユーザーとプロバイダーの間の1対多の関係(各ユーザーは0からnのプロバイダーに関連付けられる場合があります)

     App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), isAdmin: DS.attr('boolean'), orders: DS.hasMany('order', {async: true}), providers: DS.hasMany('provider', {async: true}) });
  • OrdersとProviderOrdersの間の1対多の関係(各Ordersは1からnのProviderOrdersで構成されます):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • ProvidersとProviderOrdersの間の1対多の関係(各Providerは0からnのProviderOrdersに関連付けられている場合があります):

     App.Provider = DS.Model.extend({ link: DS.attr('string'), admin: DS.belongsTo('user', {async: true}), orders: DS.belongsTo('providerOrder', {async: true}), items: DS.hasMany('items', {async: true}) });
  • ProviderOrdersとItemsの間の1対多の関係(各ProviderOrderは1からnのアイテムで構成されます):

     App.ProviderOrder = DS.Model.extend({ // One of ['processed', 'in_delivery', 'delivered'] status: DS.attr('string'), provider: DS.belongsTo('provider', {async: true}), order: DS.belongsTo('order', {async: true}), items: DS.hasMany('item', {async: true}) }); App.Item = DS.Model.extend({ name: DS.attr('string'), price: DS.attr('number'), order: DS.belongsTo('order', {async: true}), provider: DS.belongsTo('provider', {async: true}) });

そして、ルートの定義を忘れないでください。

 App.OrdersRoute = Ember.Route.extend({ model: function(){ // GET /orders // Retrieves all orders. return this.store.find('order'); } });

現在、各ProviderOrderには1つのプロバイダーがあり、これが私たちの主な目標でした。 アイテムはOrderからProviderOrderに移動され、1つのProviderOrder内のすべてのアイテムが単一のプロバイダーに属すると想定されます。

コードチャーンの最小化

残念ながら、ここにはいくつかの重大な変更があります。 それでは、Ember Dataが、コードベースで発生するコードチャーンを最小限に抑えるのにどのように役立つかを見てみましょう。

以前は、 items.pushObject(item)を使用してアイテムをプッシュしていました。 次に、最初に適切なProviderOrderを見つけて、それにアイテムをプッシュする必要があります。

 order.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); });

これは多くの解約率であり、コントローラーよりもOrderの仕事が多いため、このコードをOrder#pushItemに移動するとよいでしょう。

 App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}), /** returns a promise */ pushItem: function(item){ return this.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); }); } });

これで、 order.pushItem(item)のように、注文時に直接アイテムを追加できます。

そして、各注文のアイテムをリストするために:

 App.Order = DS.Model.extend({ /* ... */ /** returns a promise */ items: function(){ return this.get('restaurantOrders').then(function(restaurantOrders){ var arrayOfPromisesContainingItems = restaurantOrders.mapBy('items') return arrayOfPromisesContainingItems.then(function(items){ return items.reduce(function flattenByReduce(memo, index, element){ return memo.pushObject(element); }, Ember.A([])); }); }); }.property('[email protected]') /* ... */ }); App.ItemsRoute = Ember.Route.extend({ model: function(){ // Multiple GET /items with ids[] query parameter. // Returns a promise. return this.modelFor('orders.show').get('items'); } });

ポリモーフィックな関係

次に、システムに追加の拡張リクエストを導入して、事態をさらに複雑にします。

新しい要件#2:複数のタイプのプロバイダーをサポートします。

簡単な例として、2種類のプロバイダー(「ショップ」と「書店」)が定義されているとします。

 App.Shop = App.Provider.extend({ status: DS.attr('string') }); App.BookStore = App.Provider.extend({ name: DS.attr('string') });

ここで、EmberDataによるポリモーフィックな関係のサポートが役立ちます。 Ember Dataは、1対1、1対多、および多対多のポリモーフィック関係をサポートします。 これは、属性polymorphic: trueを関連付けの仕様に追加するだけで実行できます。 例えば:

 App.Provider = DS.Model.extend({ providerOrders: DS.hasMany('providerOrder', {async: true}) }); App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}) });

上記のpolymorphicフラグは、ProviderOrder(この場合はShopまたはBookstore)に関連付けることができるさまざまなタイプのプロバイダーがあることを示しています。

関係が多態的である場合、サーバーの応答は、返されたオブジェクトのIDタイプの両方を示す必要があります( RESTSerializerはデフォルトでこれを行います)。 例えば:

 GET /providerOrders { "providerOrders": [{ "status": "in_delivery", "provider": 1, "providerType": "shop" }] }

新しい要件を満たす

要件を満たすには、ポリモーフィックプロバイダーとアイテムが必要です。 ProviderOrderはプロバイダーとアイテムを接続するため、その関連付けをポリモーフィックな関連付けに変更できます。

 App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });

ただし、残りの問題があります。プロバイダーはアイテムに対して非多態的な関連付けを持っていますが、アイテムは抽象型です。 したがって、これに対処するための2つのオプションがあります。

  1. すべてのプロバイダーが同じアイテムタイプに関連付けられていることを要求します(つまり、プロバイダーとの関連付けのために特定のタイプのアイテムを宣言します)
  2. プロバイダーでのitemsの関連付けをポリモーフィックとして宣言します

この場合、オプション#2を使用して、プロバイダーでのitemsの関連付けをポリモーフィックとして宣言する必要があります。

 App.Provider = DS.Model.extend({ /* ... */ items: DS.hasMany('items', {polymorphic: true, async: true}) });

これはコードチャーンを導入しないことに注意してください。 すべてのアソシエーションは、この変更前と同じように機能します。 最高のEmberDataのパワー!

Ember Dataは本当にすべてのデータをモデル化できますか?

もちろん例外もありますが、ActiveRecordの規則をデータの構造化とモデリングの標準的で柔軟な方法と考えているので、ActiveRecordの規則がEmberDataにどのようにマッピングされるかを示します。

has_many:users through::ownersshipsまたはRepresentingIntermediateModels

これは、所有権と呼ばれるピボットモデルを参照して、関連するユーザーを見つけます。 ピボットモデルが基本的にピボットテーブルである場合、Ember Dataで中間モデルを作成することを避け、両側でDS.hasManyとの関係を表すことができます。

ただし、フロントエンド内でそのピボット関係が必要な場合は、 DS.belongsTo('user', {async: true})DS.belongsTo('provider', {async: true})を含む所有権モデルを設定します。次に、所有権を使用して関連付けにマップするプロパティをユーザーとプロバイダーの両方に追加します。 例えば:

 App.User = DS.Model.extend({ // Omitted ownerships: DS.hasMany('ownership'), /** returns a promise */ providers: function(){ return this.get('ownerships').then(function(ownerships){ return ownerships.mapBy('provider'); }); }.property('[email protected]') }); App.Ownership = DS.Model.extend({ // One of ['approved', 'pending'] status: DS.attr('string'), user: DS.belongsTo('user', {async: true}), provider: DS.belongsTo('provider', {async: true}) }); App.Provider = DS.Model.extend({ // Omitted ownerships: DS.hasMany('ownership', {async: true}), /** returns a promise */ users: function(){ return this.get('ownerships').then(function(ownerships){ return ownerships.mapBy('user'); }); }.property('[email protected]') });

has_many:mappings、as:locatable

ActiveRecordオブジェクトには、典型的なポリモーフィックな関係があります。

 class User < ActiveRecord::Base has_many :mappings, as: locatable has_many :locations, through: :mappings end class Mapping < ActiveRecord::Base belongs_to :locatable, polymorphic: true belongs_to :location end class Location < ActiveRecord::Base has_many :mappings has_many :users, through: :mappings, source: :locatable, source_type: 'User' has_many :providers, through: :mappings, source: :locatable, source_type: 'Provider' def locatables users + providers end end

これは、多(ポリモーフィック)から多(通常の非ポリモーフィック)の関係です。 Ember Dataでは、これをポリモーフィックDS.hasMany('locatable', {polymorphic: true, async: true})と静的DS.hasMany('location', {async: true})ことができます。

 App.Locatable = DS.Model.extend({ locations: DS.hasMany('location', {async: true}) }); App.User = App.Locatable.extend({ userName: DS.attr('string') }); App.Location = DS.Model.extend({ locatables: DS.hasMany('locatable', {polymorphic: true, async: true}) });

Userなどのロケータブルの場合、サーバーは関連付けられたロケーションのIDを返す必要があります。

 GET /users { "users": [ { "id": "1", "userName": "Pooyan", "locations": ["1"] } ] }

Locationの場合、サーバーはオブジェクトの配列でLocatableのIDとタイプの両方を返す必要があります。

 GET /locations { "locations": [ { "id": "1", "locatables": [ {"id": "1", "type": "user"}, {"id": "2", "type": "provider"} ] } ] }

また、静的な多対多の関係を使用して、タイプごとに関係を表すことができます。

 App.User = App.Locatable.extend({ userName: DS.attr('string'), locations: DS.hasMany('location', {async: true}) }); App.Provider = App.Locatable.extend({ link: DS.attr('string'), locations: DS.hasMany('location, {async: true} }); App.Location = DS.Model.extend({ users: DS.hasMany('user', {async: true}), providers: DS.hasMnay('provider', {async: true}) });

そして、リアルタイムデータはどうですか?

Ember Dataには、 pushpushPayload 、およびupdateがあります。 新しい/更新されたレコードをいつでも手動でEmberDataのローカルキャッシュ(ストアと呼ばれる)にプッシュでき、残りはすべて処理されます。

 App.ApplicationRoute = Ember.Route.extend({ activate: function(){ socket.on('recordUpdated', function(response){ var type = response.type; var payload = response.payload; this.store.pushPayload(type, payload); }); } });

私は個人的に、ペイロードが非常に小さいイベントにのみソケットを使用します。 典型的なイベントは、ペイロードが{"type": "shop", "id": "14"}の'recordUpdated'です。次に、ApplicationRouteで、そのレコードがローカルキャッシュ(ストア)にあるかどうか、およびIであるかどうかを確認します。再フェッチするだけです。

 App.ApplicationRoute = Ember.Route.extend({ activate: function(){ socket.on('recordUpdated', function(response){ var type = response.type; var id = response.id; if( this.store.hasRecordForId(type, id) ){ this.store.find(type, id); } }); } });

このようにして、許容できないオーバーヘッドなしに、レコード更新イベントをすべてのクライアントに送信できます。

Ember Dataには、リアルタイムデータを処理するための基本的に2つのアプローチがあります。

  1. リアルタイム通信チャネル用のアダプターを作成し、RESTAdapterの代わりに使用します。
  2. レコードが利用可能になるたびに、レコードをメインストアにプッシュします。

最初のオプションの欠点は、車輪の再発明にいくらか似ていることです。 2番目のオプションでは、メインストアにアクセスする必要があります。メインストアは、すべてのルートでroute#storeとして使用できます。

要約

この記事では、Ember Dataの主要な構成とパラダイムを紹介し、開発者として提供できる価値を示します。 Ember Dataは、より柔軟で合理化された開発ワークフローを提供し、影響の大きい変更に対応するコードチャーンを最小限に抑えます。

プロジェクトにEmberDataを使用するために行う先行投資(時間と学習曲線)は、システムが必然的に進化し、拡張、変更、および拡張する必要があるため、間違いなく価値があります。


付録:高度なEmberデータトピック

この付録では、次のようないくつかのより高度なEmberDataトピックを紹介します。

  • エンバーのモジュラーデザイン
  • サイドローディング
  • リンク
  • アクティブモデルのシリアライザーとアダプター
  • Embedded Records Mixin
  • アソシエーション修飾子(非同期、逆、および多態性)
  • GETリクエストの「ids」パラメータ

モジュール設計

Ember Dataは、内部でモジュラー設計になっています。 主なコンポーネントは次のとおりです。

  1. Adaptersは通信の処理を担当しますが、現在はRESToverHTTPのみです。
  2. Serializersライザーは、JSONまたはその逆からのモデルの作成を管理します。
  3. Storeされたレコードをキャッシュに保存します。
  4. Containerはこれらすべてを接着します。

この設計の利点は次のとおりです。

  1. データの逆シリアル化と保存は、使用される通信チャネルと要求されるリソースとは無関係に機能します。
  2. ActiveModelSerializerやEmbeddedRecordsMixinなどの動作構成は、すぐに使用できます。
  3. アダプターを変更することで、データソース(LocalStorage、CouchDB実装など)をスワップインおよびスワップアウトできます。
  4. 設定より規約が多いにもかかわらず、すべてを設定し、設定/実装をコミュニティと共有することができます。

サイドローディング

Ember Dataは、データの「サイドローディング」をサポートしています。 つまり、関連する複数のHTTPリクエストを統合するために、(リクエストされたプライマリデータとともに)取得する必要のある補助データを示します。

一般的な使用例は、関連するモデルのサイドローディングです。 たとえば、各ショップには多くの食料品があるため、関連するすべての食料品を/shopsの応答に含めることができます。

 GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ] }

食料品協会にアクセスすると、EmberDataは以下を発行します。

 GET /groceries?ids[]=98&ids[]=99&ids[]=112 { "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }

ただし、代わりに/shopsエンドポイントで関連する食料品を返す場合、EmberDataは別のリクエストを発行する必要はありません。

 GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ], "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }

リンク

Ember Dataは、アソシエーションIDの代わりにリンクを受け入れます。 リンクとして指定された関連付けにアクセスすると、Ember Dataは、関連付けられたレコードを取得するために、そのリンクに対してGETリクエストを発行します。

たとえば、食料品協会のリンクを返すことができます。

 GET /shops { "shops": [ { "id": "14", "links": { "groceries": "/shops/14/groceries" } } ] }

そして、EmberDataは/shops/14/groceriesにリクエストを発行します

GET /shops/14/groceries { "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }

データで関連付けを表す必要があることに注意してください。 リンクは新しいHTTPリクエストを提案するだけで、関連付けには影響しません。

アクティブモデルのシリアライザーとアダプター

間違いなく、 ActiveModelSerializerActiveModelAdapterは、 RESTSerializerRESTAdapterよりも実際に使用されています。 特に、バックエンドがRubyonRailsとActiveModel::Serializers gemを使用する場合、 ActiveModelSerializerActiveModelAdapterを使用するのが最適なオプションです。これらは、そのままActiveModel::Serializersをサポートするためです。

ただし、幸いなことに、 ActiveModelSerializer / ActiveModelAdapterRESTSerializer / RESTAdapterの使用法の違いは非常に限られています。 すなわち:

  1. ActiveModelSerializerはsnake_caseフィールド名を使用しますが、 RESTSerializerはcamelCasedフィールド名を必要とします。
  2. ActiveModelAdapterはsnake_caseAPIメソッドにリクエストを発行し、 RESTSerializerはcamelCasedAPIメソッドにリクエストを発行します。
  3. ActiveModelSerializerは、関連付けに関連するフィールド名が_idまたは_idsで終わることを想定していますが、 RESTSerializerは、関連付けに関連するフィールド名が関連付けフィールドと同じであることを想定しています。

アダプターとシリアライザーの選択に関係なく、EmberDataモデルはまったく同じになります。 JSON表現とAPIエンドポイントのみが異なります。

例として、最終的なProviderOrderを取り上げます。

 App.ApplicationSerializer = DS.ActiveModelSerializer.extend({ }); App.ProviderOrder = DS.Model.extend({ // One of ['processed', 'in_delivery', 'delivered'] status: DS.attr('string'), provider: DS.belongsTo('provider', {polymorphic: true, async: true}), order: DS.belongsTo('order', {async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });

Active Model SerializerとAdapterを使用すると、サーバーは次のことを期待する必要があります。

 Post /provider_orders { "provider_order": [ "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", ] }

…そして次のように応答する必要があります:

 GET /provider_orders { "provider_orders": [ "id": "1", "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", "items": [ {"id": "57", "type": "grocery"}, {"id": "89", "type": "grocery"} ] ] }

Embedded Records Mixin

DS.EmbeddedRecordsMixinは、 DS.ActiveModelSerializerの拡張機能であり、関連付けをシリアル化または逆シリアル化する方法を構成できます。 まだ完全ではありませんが(特にポリモーフィックな関連付けに関して)、それでも興味深いものです。

選んでいいですよ:

  1. アソシエーションをシリアル化または逆シリアル化しないでください。
  2. 1つまたは複数のIDとの関連付けをシリアル化または逆シリアル化します。
  3. 組み込みモデルとの関連付けをシリアル化または逆シリアル化します。

これは、デフォルトでDS.hasManyに関連付けられたIDがシリアル化されるオブジェクトに追加されない1対多の関係で特に役立ちます。 例として、多くのアイテムが入っているショッピングカートを取り上げます。 この例では、アイテムがわかっている間にカートが作成されています。 ただし、カートを保存する場合、EmberDataは関連するアイテムのIDをリクエストペイロードに自動的に配置しません。

ただし、 DS.EmbeddedRecordsMixinを使用すると、次のようにEmberDataにカートのアイテムIDをシリアル化するように指示できます。

 App.CartSerializer = DS.ActiveModelSerializer .extend(DS.EmbeddedRecordsMixin) .extend{ attrs: { // thanks EmbeddedRecordsMixin! items: {serialize: 'ids', deserialize: 'ids'} } }); App.Cart = DS.Model.extend({ items: DS.hasMany('item', {async: true}) }); App.Item = DS.Model.extend({ cart: DS.belongsTo('item', {async: true}) });

上記の例に示されているように、EmbeddedRecordsMixinでは、 attrsオブジェクトを介してシリアル化および/または逆シリアル化するアソシエーションを明示的に指定できます。 シリアルserializeおよびdeserialize化の有効な値は次のとおりです。- 'no' :シリアル化/逆シリアル化されたデータに関連付けを含めない-'id 'id'または'ids' :シリアル化/逆シリアル化されたデータに関連付けられたIDを含める- 'records ':実際のプロパティを含める(つまり、フィールド値を記録する)シリアル化/逆シリアル化されたデータの配列として

アソシエーション修飾子(非同期、逆、および多態性)

次の関連付け修飾子がサポートされています: polymorphicinverse 、およびasync

多形修飾子

ポリモーフィックアソシエーションでは、アソシエーションの片側または両側が、特定のオブジェクトではなく、オブジェクトのクラスを表します。

投稿とページの両方にタグを付ける機能をサポートする必要があったブログの以前の例を思い出してください。 これをサポートするために、次のモデルに到達しました。

  • 各投稿はタグ付け可能であり、多くのタグがあります
  • 各ページはタグ付け可能であり、多くのタグがあります
  • 各タグには、多くのポリモーフィックなタグ可能オブジェクトがあります

そのモデルに従って、次のように、 polymorphic修飾子を使用して、タグが任意のタイプの「タグ付け可能」(投稿またはページのいずれか)に関連していることを宣言できます。

 // A Taggable is something that can be tagged (ie, that has tags) App.Taggable = DS.Model.extend({ tags: DS.hasMany('tag') }); // A Page is a type of Taggable App.Page = App.Taggable.extend({}); // A Post is a type of Taggable App.Post = App.Taggable.extend App.Tag = DS.Model.extend({ // the "other side" of this association (ie, the 'taggable') is polymorphic taggable: DS.belongsTo('taggable', {polymorphic: true}) });

逆修飾子

通常、関連付けは双方向です。 たとえば、「投稿には多くのコメントがあります」は関連付けの1つの方向であり、「コメントは投稿に属します」はその関連付けのもう1つの方向(つまり「逆」)になります。

アソシエーションにあいまいさがない場合、Ember Dataはアソシエーションの逆の部分を推測できるため、1つの方向のみを指定する必要があります。

However, in cases where objects in your model have multiple associations with one another, Ember Data cannot derive the inverse of each association automatically and it therefore needs to be specified using the invers modifier.

Consider, for example, a case where:

  • Each Page may have many Users as Collaborators
  • Each Page may have many Users as Maintainers
  • Each User may have many Favorite Pages
  • Each User may have many Followed Pages

This would need to be specified as follows in our model:

 App.User = DS.Model.extend({ favoritePages: DS.hasMany('page', {inverse: 'favoritors}), followedPages: DS.hasMany('page', {inverse: 'followers'}), collaboratePages: DS.hasMany('page', {inverse: 'collaborators'}), maintainedPages: DS.hasMany('page', {inverse: 'maintainers'}) }); App.Page = DS.Model.extend({ favoritors: DS.hasMany('user', {inverse: 'favoritePages'}), followers: DS.hasMany('user', {inverse: 'followedPages'}), collaborators: DS.hasMany('user', {inverse: 'collaboratedPages}), maintainers: DS.hasMany('user', {inverse: 'maintainedPages'}) });

Async Modifier

When data needs to be retrieved based on relevant associations, that associated data may or may not already have been loaded. If not, a synchronous association will throw an error since the associated data has not been loaded.

{async: true} indicates that the request for the associated data should be handled asynchronously. The request therefore immediately returns a promise and the supplied callback is invoked once the associated data has been retrieved and is available.

When async is false , getting associated objects would be done as follows:

 post.get('comments').pushObject(comment);

When async is true , getting associated objects would be done as follows (note the callback function specified):

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

Note well: The current default value of async is false , but in Ember Data 1.0 the default will be true .

'ids' Parameter in GET Requests

By default, Ember Data expects that resources aren't nested. Take Posts which have many Comments as an example. In a typical interpretation of REST, API endpoints might look like these:

 GET /users GET /users/:id/posts GET /users/:id/posts/:id/comments

However, Ember Data expects API endpoints to be flat, and not nested; 例えば:

 GET /users with ?ids[]=4 as query parameters. GET /posts with ?ids[]=18&ids[]=27 as query parameters. GET /comments with ?ids[]=74&ids[]=78&ids[]=114&ids[]=117 as query parameters.

In the above example, ids is the name of the array, [] indicates that this query parameter is an array, and = indicates that a new ID is being pushed onto the array.

Now Ember Data can avoid downloading resources it already has or defer downloading them to very last moment.

Also, to white list an array parameter using the StrongParameters gem, you can declare it as params.require(:shop).permit(item_ids: []) .