Ember Data:ember-data 库的综合教程
已发表: 2022-03-11Ember 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 数据价值主张
让我们从一个简单的例子开始。
假设我们有一个基本博客系统的工作代码库。 该系统包含帖子和标签,它们之间具有多对多的关系。
在我们得到支持 Pages 的要求之前,一切都很好。 该要求还指出,由于可以在 WordPress 中标记页面,因此我们也应该能够这样做。
所以现在,标签将不再只适用于帖子,它们也可能适用于页面。 结果,我们在标签和帖子之间的简单关联将不再适用。 相反,我们需要一个多对多的单边多态关系,例如:
- 每个帖子都是可标记的,并且有许多标签
- 每个页面都是可标记的,并且有许多标签
- 每个 Tag 都有许多多态 Taggables
过渡到这种新的、更复杂的关联集可能会对整个代码产生重大影响,从而导致大量流失。 由于我们不知道如何将多态关联序列化为 JSON,我们可能只会创建更多 API 端点,例如GET /posts/:id/tags
和GET /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
接受四种数据类型: string
、 number
、 boolean
和date
。
RESTSerializer
默认:
- Ember Data 使用
RESTSerializer
从 API 响应创建对象(反序列化)并为 API 请求生成 JSON(序列化)。 -
RESTSerializer
期望DS.belongsTo
创建的字段在来自服务器的 JSON 响应中包含一个名为user
的字段。 该字段包含引用记录的 id。 -
RESTSerializer
将user
字段添加到传递给 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 是抽象类型。 因此,我们有两种选择来解决这个问题:
- 要求所有provider关联同一个item类型(即声明一个特定类型的item与provider关联)
- 将 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 有push
、 pushPayload
和update
。 您始终可以手动将新的/更新的记录推送到 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 中有两种处理实时数据的方法:
- 为您的实时通信通道编写一个适配器并使用它来代替 RESTAdapter。
- 只要有可用的记录,就将它们推送到主存储区。
第一个选项的缺点是它有点类似于重新发明轮子。 对于第二个选项,我们需要访问主商店,它在所有路线上都可用route#store
。
包起来
在本文中,我们向您介绍了 Ember Data 的关键构造和范例,展示了它可以为您作为开发人员提供的价值。 Ember Data 提供了更灵活、更精简的开发工作流程,最大限度地减少了代码改动以应对可能会产生重大影响的更改。
您在项目中使用 Ember Data 所做的前期投资(时间和学习曲线)无疑将证明是值得的,因为您的系统不可避免地会发展并且需要扩展、修改和增强。
附录:高级 Ember 数据主题
本附录介绍了一些更高级的 Ember Data 主题,包括:
- Ember 的模块化设计
- 侧载
- 链接
- 主动模型串行器和适配器
- 嵌入式记录混合
- 关联修饰符(异步、反向和多态)
- GET 请求中的“ids”参数
模块化设计
Ember Data 在引擎盖下采用模块化设计。 关键组件包括:
-
Adapters
负责处理通信,目前只有 REST over HTTP。 -
Serializers
管理从 JSON 或相反的模型创建。 -
Store
缓存创建的记录。 -
Container
将所有这些粘合在一起。
这种设计的好处包括:
- 数据的反序列化和存储独立于使用的通信通道和请求的资源。
- ActiveModelSerializer 或 EmbeddedRecordsMixin 等工作配置是开箱即用的。
- 数据源(例如,LocalStorage、CouchDB 实现等)可以通过更改适配器来换入和换出。
- 尽管对配置有很多约定,但可以配置所有内容并与社区共享您的配置/实现。
侧载
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 请求,不会影响关联。
主动模型串行器和适配器
可以说, ActiveModelSerializer
和ActiveModelAdapter
在实践中比RESTSerializer
和RESTAdapter
使用得更多。 特别是,当后端使用 Ruby on Rails 和ActiveModel::Serializers
gem 时,最好的选择是使用ActiveModelSerializer
和ActiveModelAdapter
,因为它们开箱即用地支持ActiveModel::Serializers
。
不过幸运的是, ActiveModelSerializer
/ ActiveModelAdapter
和RESTSerializer
/ RESTAdapter
之间的使用差异非常有限; 即:
-
ActiveModelSerializer
将使用snake_case 字段名称,而RESTSerializer
需要 camelCased 字段名称。 -
ActiveModelAdapter
向snake_case API 方法发出请求,而RESTSerializer
向camelCased API 方法发出请求。 -
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.EmbeddedRecordsMixin
是DS.ActiveModelSerializer
的扩展,它允许配置关联如何被序列化或反序列化。 虽然还没有完成(特别是关于多态关联),但它仍然很有趣。
你可以选择:
- 不要序列化或反序列化关联。
- 序列化或反序列化与 id 或 ids 的关联。
- 序列化或反序列化与嵌入式模型的关联。
这在一对多关系中特别有用,默认情况下, 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
对象显式指定要序列化和/或反序列化的关联。 serialize
和deserialize
序列化的有效值是: - 'no'
:不包括序列化/反序列化数据中的关联 - 'id'
或'ids'
:在序列化/反序列化数据中包括关联 ID - 'records
':包括实际属性(即记录字段值)作为序列化/反序列化数据中的数组
关联修饰符(异步、反向和多态)
支持以下关联修饰符: polymorphic
、 inverse
和async
多态修饰符
在多态关联中,关联的一侧或两侧表示一类对象,而不是特定对象。
回想一下我们之前的博客示例,我们需要支持标记帖子和页面的能力。 为了支持这一点,我们得出了以下模型:
- 每个帖子都是可标记的,并且有许多标签
- 每个页面都是可标记的,并且有许多标签
- 每个 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 可以推断出关联的逆部分。
但是,如果模型中的对象相互之间有多个关联,Ember Data 无法自动导出每个关联的逆,因此需要使用invers
修饰符来指定。
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: [])
.