Ember Data:ember-data 庫的綜合教程

已發表: 2022-03-11

Ember Data(又名 ember-data 或 ember.data)是一個用於在 Ember.js 應用程序中穩健地管理模型數據的庫。 Ember Data 的開發人員表示,它的設計與底層持久性機制無關,因此它與 HTTP 上的 JSON API 和流式 WebSocket 或本地 IndexedDB 存儲一樣好。 它提供了許多您可以在服務器端對象關係映射 (ORM)(如 ActiveRecord)中找到的工具,但它是專為瀏覽器中 JavaScript 的獨特環境而設計的。

雖然 Ember Data 可能需要一些時間來了解,但一旦你這樣做了,你可能會發現它非常值得投資。 它最終將使您的系統的開發、增強和維護變得更加容易。

當使用 Ember Data 模型、適配器和序列化器表示 API 時,每個關聯都只是一個字段名稱。 這封裝了每個關聯的內部細節,從而將其餘代碼與關聯本身的更改隔離開來。 例如,如果一個特定的關聯是多態的,或者是許多關聯的映射的結果,那麼您的其餘代碼將不關心。

此外,您的代碼庫在很大程度上不受後端更改的影響,即使它們很重要,因為您的代碼庫所期望的只是模型上的字段和函數,而不是模型的 JSON、XML 或 YAML 表示。

在本教程中,我們將介紹 Ember Data 最顯著的特性,並通過關註一個真實世界的示例來演示它如何幫助最大限度地減少代碼流失。

還提供了一個附錄,討論了一些更高級的 Ember Data 主題和示例。

注意:本文假定您對 Ember.js 有一定的基本了解。 如果您不熟悉 Ember.js,請查看我們流行的 Ember.js 教程以獲取介紹。 我們還提供俄語、葡萄牙語和西班牙語的全棧 JavaScript 指南。

Ember 數據價值主張

Ember 數據庫如何幫助滿足客戶需求的示例。

讓我們從一個簡單的例子開始。

假設我們有一個基本博客系統的工作代碼庫。 該系統包含帖子和標籤,它們之間具有多對多的關係。

在我們得到支持 Pages 的要求之前,一切都很好。 該要求還指出,由於可以在 WordPress 中標記頁面,因此我們也應該能夠這樣做。

所以現在,標籤將不再只適用於帖子,它們也可能適用於頁面。 結果,我們在標籤和帖子之間的簡單關聯將不再適用。 相反,我們需要一個多對多的單邊多態關係,例如:

  • 每個帖子都是可標記的,並且有許多標籤
  • 每個頁面都是可標記的,並且有許多標籤
  • 每個 Tag 都有許多多態 Taggables

過渡到這種新的、更複雜的關聯集可能會對整個代碼產生重大影響,從而導致大量流失。 由於我們不知道如何將多態關聯序列化為 JSON,我們可能只會創建更多 API 端點,例如GET /posts/:id/tagsGET /pages/:id/tags 。 然後,我們將丟棄所有現有的 JSON 解析器函數,並為添加的新資源編寫新函數。 啊。 乏味而痛苦。

現在讓我們考慮如何使用 Ember Data 來解決這個問題。

在 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 數據入門

在深入研究現實世界的示例之前,讓我們回顧一下 Ember Data 的一些基礎知識。

路線和模型

在 Ember.js 中,路由器負責顯示模板、加載數據以及設置應用程序狀態。 路由器將當前 URL 與您定義的路由匹配,因此 Route 負責指定模板要顯示的模型(Ember 期望此模型是Ember.Object的子類):

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

Ember Data 提供了DS.Model ,它是Ember.Object的子類,並添加了保存或更新單個記錄或多個記錄等功能,以方便使用。

要創建新模型,我們創建DS.Model的子類(例如, App.User = DS.Model.extend({}) )。

Ember Data 期望來自服務器的定義明確、直觀的 JSON 結構,並將新創建的記錄序列化為相同的結構化 JSON。

Ember Data 還提供了一套數組類,如DS.RecordArray用於處理模型。 它們的職責包括處理一對多或多對多關係、處理數據的異步檢索等。

模型屬性

基本模型屬性使用DS.attr定義; 例如:

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

只有DS.attr創建的字段才會包含在傳遞給服務器以創建或更新記錄的負載中。

DS.attr接受四種數據類型: stringnumberbooleandate

RESTSerializer

默認:

  • Ember Data 使用RESTSerializer從 API 響應創建對象(反序列化)並為 API 請求生成 JSON(序列化)。
  • RESTSerializer期望DS.belongsTo創建的字段在來自服務器的 JSON 響應中包含一個名為user的字段。 該字段包含引用記錄的 id。
  • RESTSerializeruser字段添加到傳遞給 API 的有效負載中,並帶有關聯訂單的 id。

例如,響應可能如下所示:

 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" } }

一對一關係

例如,假設每個用戶都有一個唯一的個人資料。 我們可以在用戶和配置文件上使用DS.belongsTo在 Ember Data 中表示這種關係:

 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 */ } }

一對多和多對一關係

假設我們有一個模型,其中一個帖子有很多評論。 在 Ember Data 中,我們可以在 Post 上用DS.hasMany('comment', {async: true})和在 Comment 上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 來響應 Post 上的相應評論:

 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將關聯 Post 的 id 添加到 Comment 中:

 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 也被序列化,您可以使用 Embedded Records Mixin。

多對多關係

假設在我們的模型中,一個作者可能有多個帖子,一個帖子可能有多個作者。

為了在 Ember Data 中表示這種關係,我們可以對 Post 使用DS.hasMany('author', {async: true})對 Author 使用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:使單個訂單能夠包含來自多個供應商的商品。

在現有系統中,Providers 和 Orders 之間是一對多的關係。 但是,一旦我們擴展訂單以包含來自多個供應商的商品,這種簡單的關係將不再適用。

具體來說,如果提供者與整個訂單相關聯,則在增強系統中,該訂單很可能也包括從其他提供者訂購的項目。 因此,需要有一種方法來指示每個訂單的哪個部分與每個供應商相關。 此外,當供應商訪問他們的訂單時,他們應該只能看到從他們那裡訂購的商品,而不是客戶可能從其他供應商那裡訂購的任何其他商品。

一種方法是引入兩個新的多對多關聯; 一個在 Order 和 Item 之間,另一個在 Order 和 Provider 之間。

然而,為了簡單起見,我們在數據模型中引入了一個新的結構,我們稱之為“ProviderOrder”。

起草關係

增強的數據模型將需要適應以下關聯:

  • 用戶和訂單之間的一對多關係(每個用戶可能關聯0 到 n 個訂單)用戶和提供商之間的一對多關係(每個用戶可能關聯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 之間的一對多關係(每個 Order 由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是一對多的關係(每個 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 之間的一對多關係(每個 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}) });

我們不要忘記我們的Route定義:

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

現在每個 ProviderOrder 都有一個 Provider,這是我們的主要目標。 項目從 Order 移動到 ProviderOrder,假設一個 ProviderOrder 中的所有項目都屬於一個 Provider。

最小化代碼流失

不幸的是,這裡有一些重大變化。 因此,讓我們看看 Ember Data 如何幫助我們最大限度地減少代碼庫中產生的任何代碼流失。

以前,我們使用items.pushObject(item)推送項目。 現在我們需要先找到合適的 ProviderOrder 並推送一個 Item 給它:

 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:支持多種類型的Provider。

對於我們的簡單示例,假設定義了兩種類型的 Provider(“Shop”和“Bookstore”):

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

這就是 Ember Data 對多態關係的支持將派上用場的地方。 Ember Data 支持一對一、一對多和多對多的多態關係。 這可以簡單地通過將屬性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標誌表明有多種類型的 Provider 可以與 ProviderOrder 相關聯(在我們的例子中,是 Shop 或 Bookstore)。

當關係是多態的時,服務器響應應該同時指明返回對象的 ID類型( RESTSerializer默認這樣做); 例如:

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

滿足新要求

我們需要多態的 Providers 和 Items 來滿足要求。 由於 ProviderOrder 將 Providers 與 Items 連接起來,我們可以將其關聯更改為多態關聯:

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

但是還有一個問題:Provider 與 Items 有非多態關聯,但 Item 是抽像類型。 因此,我們有兩種選擇來解決這個問題:

  1. 要求所有provider關聯同一個item類型(即聲明一個特定類型的item與provider關聯)
  2. 將 Provider 上的items關聯聲明為多態

在我們的例子中,我們需要使用選項 #2 並將 Provider 上的items關聯聲明為多態:

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

請注意,這不會引入任何代碼攪動; 所有關聯都只是按照他們在此更改之前所做的方式工作。 Ember Data 的強大功能!

Ember Data真的可以為我的所有數據建模嗎?

當然也有例外,但我認為 ActiveRecord 約定是一種標準且靈活的數據結構化和建模方式,所以讓我向您展示 ActiveRecord 約定如何映射到 Ember 數據:

has_many :users through: :ownerships 或代表中間模型

這將參考一個名為 Ownership 的樞軸模型來查找關聯的用戶。 如果數據透視模型基本上是數據透視,可以避免在 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}) });

對於 Locatables,比如 User,服務器應該返回相關位置的 ID:

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

對於位置,服務器應在對像數組中返回可定位的 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 有pushpushPayloadupdate 。 您始終可以手動將新的/更新的記錄推送到 Ember Data 的本地緩存(稱為存儲)中,其餘的將由它處理。

 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 我將檢查該記錄是否在本地緩存(存儲)中,如果它是我'會重新取回它。

 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 中有兩種處理實時數據的方法:

  1. 為您的實時通信通道編寫一個適配器並使用它來代替 RESTAdapter。
  2. 只要有可用的記錄,就將它們推送到主存儲區。

第一個選項的缺點是它有點類似於重新發明輪子。 對於第二個選項,我們需要訪問主商店,它在所有路線上都可用route#store

包起來

在本文中,我們向您介紹了 Ember Data 的關鍵構造和範例,展示了它可以為您作為開發人員提供的價值。 Ember Data 提供了更靈活、更精簡的開發工作流程,最大限度地減少了代碼改動以應對可能會產生重大影響的更改。

您在項目中使用 Ember Data 所做的前期投資(時間和學習曲線)無疑將證明是值得的,因為您的系統不可避免地會發展並且需要擴展、修改和增強。


附錄:高級 Ember 數據主題

本附錄介紹了一些更高級的 Ember Data 主題,包括:

  • Ember 的模塊化設計
  • 側載
  • 鏈接
  • 主動模型串行器和適配器
  • 嵌入式記錄混合
  • 關聯修飾符(異步、反向和多態)
  • GET 請求中的“ids”參數

模塊化設計

Ember Data 在引擎蓋下採用模塊化設計。 關鍵組件包括:

  1. Adapters負責處理通信,目前只有 REST over HTTP。
  2. Serializers管理從 JSON 或相反的模型創建。
  3. Store緩存創建的記錄。
  4. Container將所有這些粘合在一起。

這種設計的好處包括:

  1. 數據的反序列化和存儲獨立於使用的通信通道和請求的資源。
  2. ActiveModelSerializer 或 EmbeddedRecordsMixin 等工作配置是開箱即用的。
  3. 數據源(例如,LocalStorage、CouchDB 實現等)可以通過更改適配器來換入和換出。
  4. 儘管對配置有很多約定,但可以配置所有內容並與社區共享您的配置/實現。

側載

Ember Data 支持數據的“側載”; 即,指示應檢索的輔助數據(連同請求的主要數據),以幫助合併多個相關的 HTTP 請求。

一個常見的用例是側載相關模型。 例如,每個 Shop 都有很多雜貨,所以我們可以在/shops響應中包含所有相關的雜貨:

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

當訪問雜貨關聯時,Ember Data 將發出:

 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端點中返回關聯的 Groceries,Ember Data 將不需要發出另一個請求:

 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" } } ] }

然後 Ember Data 會向/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使用得更多。 特別是,當後端使用 Ruby on Rails 和ActiveModel::Serializers gem 時,最好的選擇是使用ActiveModelSerializerActiveModelAdapter ,因為它們開箱即用地支持ActiveModel::Serializers

不過幸運的是, ActiveModelSerializer / ActiveModelAdapterRESTSerializer / RESTAdapter之間的使用差異非常有限; 即:

  1. ActiveModelSerializer將使用snake_case 字段名稱,而RESTSerializer需要 camelCased 字段名稱。
  2. ActiveModelAdapter向snake_case API 方法發出請求,而RESTSerializer向camelCased API 方法發出請求。
  3. ActiveModelSerializer期望與關聯相關的字段名稱以_id_ids結尾,而RESTSerializer期望與關聯相關的字段名稱與關聯字段相同。

無論適配器和序列化器如何選擇,Ember Data 模型都將完全相同。 只有 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"} ] ] }

嵌入式記錄混合

DS.EmbeddedRecordsMixinDS.ActiveModelSerializer的擴展,它允許配置關聯如何被序列化或反序列化。 雖然還沒有完成(特別是關於多態關聯),但它仍然很有趣。

你可以選擇:

  1. 不要序列化或反序列化關聯。
  2. 序列化或反序列化與 id 或 ids 的關聯。
  3. 序列化或反序列化與嵌入式模型的關聯。

這在一對多關係中特別有用,默認情況下, DS.hasMany關聯的 ID 不會添加到序列化的對像中。 以包含許多商品的購物車為例。 在此示例中,在 Items 已知的情況下創建了購物車。 但是,當您保存購物車時,Ember Data 不會自動將關聯項目的 ID 放在請求負載上。

但是,使用DS.EmbeddedRecordsMixin可以告訴 Ember Data 序列化 Cart 上的項目 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對象顯式指定要序列化和/或反序列化的關聯。 serializedeserialize序列化的有效值是: - 'no' :不包括序列化/反序列化數據中的關聯 - 'id''ids' :在序列化/反序列化數據中包括關聯 ID - 'records ':包括實際屬性(即記錄字段值)作為序列化/反序列化數據中的數組

關聯修飾符(異步、反向和多態)

支持以下關聯修飾符: polymorphicinverseasync

多態修飾符

在多態關聯中,關聯的一側或兩側表示一類對象,而不是特定對象。

回想一下我們之前的博客示例,我們需要支持標記帖子和頁面的能力。 為了支持這一點,我們得出了以下模型:

  • 每個帖子都是可標記的,並且有許多標籤
  • 每個頁面都是可標記的,並且有許多標籤
  • 每個 Tag 都有許多多態 Taggables

遵循該模型, 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}) });

逆修飾符

通常關聯是雙向的。 例如,“帖子有很多評論”將是關聯的一個方向,而“評論屬於帖子”將是該關聯的另一個(即“反向”)方向。

在關聯中沒有歧義的情況下,只需要指定一個方向,因為 Ember Data 可以推斷出關聯的逆部分。

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: []) .