作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Pooyan Khosravy的头像

By Pooyan Khosravy

Pooyan是一位数据科学家, 神经系统科学家, 软件工程师, 还有一位企业家致力于确保没有人患有未确诊的多动症

分享

安贝数据 (a.k.成员数据或余烬.是一个健壮地管理模型数据的库 灰烬.js 应用程序. 安贝数据的开发人员表示,它被设计成与底层持久性机制无关, 因此,它可以与HTTP上的JSON api一起工作,也可以与流式WebSockets或本地IndexedDB存储一起工作. 它提供了许多在服务器端可以找到的功能 对象关系映射(orm) 就像 ActiveRecord,但它是专门为浏览器中JavaScript的独特环境设计的.

而烬数据可能需要一些时间来理解, 一旦你这么做了, 你可能会发现投资是值得的. 它最终将使系统的开发、增强和维护变得更加容易.

当使用安贝数据模型表示API时, 适配器和序列化器, 每个关联都简单地变成一个字段名. 这封装了每个关联的内部细节, 从而将其余代码与关联本身的更改隔离开来. 其余的代码不会在意, 例如, 如果一个特定的关联是多态的,或者是许多关联图的结果.

此外, 您的代码库在很大程度上与后端更改隔离, 即使它们很重要, 因为所有的代码库期望的是模型上的字段和函数, 而不是模型的JSON、XML或YAML表示.

在本教程中, 我们将介绍安贝数据最显著的特性,并演示它如何帮助减少代码流失, 通过专注于一个 现实世界的例子.

An 附录 还讨论了一些更高级的灰烬数据主题和示例.

注意: 本文假定您对以下方面有基本的了解 灰烬.js. 如果你不熟悉烬.j,看看我们的热门节目 灰烬.js教程 作为介绍. 我们还有一个 全栈JavaScript指南 有俄文、葡萄牙文和西班牙文版本.

烬数据价值主张

烬数据库如何帮助满足客户需求的一个例子.

让我们从一个简单的例子开始.

假设我们有一个基本博客系统的工作代码库. 系统包含Posts和标签,它们之间是多对多的关系.

一切都很好,直到我们需要支持Pages. 该要求还指出, 因为可以在WordPress中标记页面, 我们也应该能够这样做.

所以现在,标签将不再只适用于帖子,它们也可能适用于页面. 因此,标签和帖子之间的简单关联将不再足够. 相反,我们需要一个多对多的单侧多态关系,如下所示:

  • 每个帖子都是可标记的,并且有许多标签
  • 每个页面都是可标记的,并且有许多标签
  • 每个标签都有许多多态标签

过渡到这个新的, 更复杂的, 一组关联可能在我们的代码中有重要的分支, 导致大量的流失. 因为我们不知道如何序列化多态关联到JSON, 我们可能会创建更多的API端点 得到/文章/:id /标记得到/页面/:id /标记. 然后, 我们将抛弃所有现有的JSON解析器函数,并为添加的新资源编写新的解析器. 啊. 乏味又痛苦.

现在让我们考虑一下如何使用灰烬数据来实现这一点.

在安贝数据中,要适应这一修改后的关联集,只需从:

应用程序.职位= DS.模型.扩展({
  标签:DS.hasM任何('tag', {异步:真})
});

应用程序.标签= DS.模型.扩展({
  职位:DS.belongsTo('帖子', {异步:真})
});

to:

应用程序.PostType = DS.模型.扩展({
  标签:DS.hasM任何('tag', {异步:真})
});

应用程序.Post = 应用程序.PostType.扩展({
});

应用程序.页面= 应用程序.PostType.扩展({
});

应用程序.标签= DS.模型.扩展({
  职位:DS.hasM任何('帖子Type',{多态:真正的,异步:真正的})
});

剩下的代码的混乱将是最小的,我们将能够重用大多数模板. 特别要注意的是 标签 Post上的关联名称保持不变. 此外,代码库的其余部分仅依赖于 标签 联想,而忽略了它的细节.

烬数据入门

在一头扎进 现实世界的例子,让我们回顾一些烬数据的基础知识.

路线和模型

In 灰烬.js, 路由器 负责显示模板、加载数据和设置应用程序状态. 路由器将当前的URL匹配到你定义的路由,所以a 路线 负责指定模板要显示的模型(灰烬希望这个模型是它的子类 灰烬.Object):

应用程序.Items路线 = 灰烬.路线.扩展({
  模型:功能(){
    //获取/项
    //检索所有项.
    返回这.梅尔(“订单.显示的).(“项目”);
  }
});

安贝数据提供 DS.模型 哪个是的子类 灰烬.Object 并增加了保存或更新单个记录或多个记录的功能,以方便使用.

要创建一个新的模型,我们创建的子类 DS.模型 (e.g., 应用程序.用户= DS.模型.扩展({})).

安贝数据期望一个定义良好的, 从服务器获取直观的JSON结构,并将新创建的记录序列化为相同的结构化JSON.

安贝数据还提供了一套数组类,比如 DS.RecordArray 用于使用模型. 它们的职责包括处理一对多或多对多关系, 处理数据的异步检索, 等等......。.

模型属性

定义基本模型属性 DS.attr; e.g.:

应用程序.用户= DS.模型.扩展({
  firstName: DS.attr('字符串'),
  姓:DS.attr(字符串)
});

仅由 DS.attr 是否包含在传递给服务器用于创建或更新记录的有效负载中.

DS.attr 接受四种数据类型: 字符串, 数量, 布尔日期.

RESTSerializer

默认情况下:

  • 安贝数据雇佣 RESTSerializer 用于从API响应创建对象(反序列化)和为API请求生成JSON(序列化).
  • RESTSerializer 创建的字段 DS.belongsTo 将字段命名为 用户 包含在来自服务器的JSON响应中. 该字段包含被引用记录的id.
  • RESTSerializer 添加一个 用户 字段转换为传递给API的有效负载,并带有关联订单的id.

例如,响应可能看起来像这样:

得到http://api.例子.com/订单s?id [] = 19&id [] = 28
{
  “订单”:[
    {
      "id":        "19",
      “createdAt”:“1401492647008”,
      “用户”:“1”
    },
    {
      "id":        "28",
      “createdAt”:“1401492647008”,
      “用户”:“1”
    }
  ]
}

创建的HTTP请求 RESTSerializer 保存订单可能是这样的:

文章http://api.例子.com/订单s
{
  “订单”:{
    “createdAt”:“1401492647008”,
    “用户”:“1”
  }
}

一对一的关系

例如,每个用户都有一个唯一的概要文件. 我们可以在烬数据中使用 DS.belongsTo 在用户和个人资料上:

应用程序.用户= DS.模型.扩展({
  简介:DS.belongsTo('profile', {异步:真})
});

应用程序.剖面= DS.模型.扩展({
  用户:DS.belongsTo('用户', {异步:真})
});

我们就可以得到 用户.get(概要) 或者用 用户.集(“概要”,aProfile).

RESTSerializer expects the ID of the associated 模型 to be provided for each 模型s; e.g.:

GET /用户
{
  “用户”:(
    {
      "id": "14",
      "profile": "1" /*与该用户关联的profile的ID */
    }
  ]
}

GET /配置文件
{
  “简介”:[
    {
      "id": "1",
      "用户": "14" /*与此配置文件关联的用户ID */
    }
  ]
}

类似地,它在请求负载中包含关联模型的ID:

POST /配置文件
{
  “简介”:{
    "用户": "17" /*与此配置文件关联的用户ID */
  }
}

一对多和多对一关系

假设我们有一个帖子有很多评论的模型. 在安贝数据中,我们可以用 DS.hasM任何('comment', {异步:真}) 有关邮政及 DS.belongsTo('帖子', {异步:真}) 评论:

应用程序.职位= DS.模型.扩展({
  内容:DS.attr('字符串'),
  评论:DS.hasM任何('comment', {异步:真})
});

应用程序.评论= DS.模型.扩展({
  信息:DS.attr('字符串'),
  职位:DS.belongsTo('帖子', {异步:真})
});

然后我们可以得到关联的项 帖子.Get ('评论', {异步:真}) 并添加一个新的关联 帖子.get(评论).然后(function(评论){返回注释.推Object (aComment);}).

然后,服务器将使用一个id数组来响应Post上的相应评论:

GET /职位
{
  “帖子”:(
    {
      "id":       "12",
      “内容”:“”,
      “评论”:["56", "58"]
    }
  ]
}

每个评论都有一个ID:

GET /评论?id [] = 56&id [] = 58
{
  “评论”:[
    {
      "id":      "56",
      “消息”:“”,
      “帖子”:“12”
    },
    {
      "id":      "58",
      “消息”:“”,
      “帖子”:“12”
    }
  ]
}

RESTSerializer 将相关帖子的id添加到评论中:

文章/评论
{
  "评论":{
    “消息”:“”,
    "帖子": "12" /*与此评论关联的帖子ID */
  }
}

不过请注意,默认情况下,RESTSerializer将 添加 DS.hasM任何 关联id到它序列化的对象, 由于这些关联是在“多”方面指定的(例如.e.,那些有 DS.belongsTo 协会). 因此,在我们的示例中,尽管Post有许多评论,但这些id将 被添加到Post对象中:

岗位/职位
{
  “帖子”:{
    "content": "" /*此处未添加关联的帖子id */
  }
}

“力” DS.hasM任何 id也要序列化,您可以使用 嵌入式记录混合.

多对多关系

假设在我们的模型中,一个Author可能有多个Post,而一个Post可能有多个Author.

为了在烬数据中表示这种关系,我们可以使用 DS.hasM任何('作者', {异步:真}) 有关邮政及 DS.hasM任何('帖子', {异步:真}) 作者:

应用程序.作者= DS.模型.扩展({
  名称:DS.attr('字符串'),
  职位:DS.hasM任何('帖子', {异步:真})
});

应用程序.职位= DS.模型.扩展({
  内容:DS.attr('字符串'),
  作者:DS.hasM任何('作者', {异步:真})
});

然后我们可以得到关联的项 作者.get(职位) 并添加一个新的关联 作者.get(职位).然后(function(帖子s){返回帖子s.推Object (aPost);}).

The server will then respond with an array of IDs for the corresponding objects; e.g.:

GET /作者
{
  “作者”:(
    {
      "id":    "1",
      “名称”:“”,
      "帖子s":["12"] /*与该作者相关的帖子的id */
    }
  ]
}

GET /职位
{
  “帖子”:(
    {
      "id":      "12",
      “内容”:“”,
      "作者s":["1"] /*与这篇文章相关的作者的id */
    }
  ]
}

因为这是一个多对多关系, RESTSerializer 添加一个n array of IDs of associated objects; e.g:

岗位/职位
{
  “帖子”:{
    “内容”:“”,
    "作者s":["1", "4"] /*与这篇文章相关的作者的id */
  }
}

一个真实的例子:改进现有的订购系统

在我们现有的订购系统中,每个用户有许多订单,每个订单有许多项目. 我们的系统有多个提供者(例如.e., 可以向其订购产品的供应商, 但是每个订单只能包含来自单个供应商的项目.

新需求#1:允许单个订单包含来自多个供应商的商品.

在现有系统中,提供者和订单之间存在一对多的关系. 一旦我们扩展一个订单,包括来自多个供应商的项目, 虽然, 这种简单的关系将不再适用.

具体地说, 如果供应商与整个订单相关联, 在增强的系统中,该订单很可能也包括从其他供应商订购的项目. 必须有个办法, 因此, 指示每个订单的哪个部分与每个提供者相关. 此外, 当供应商访问他们的订单时, 他们应该只能看到从他们那里订购的物品, 而不是客户从其他供应商处订购的任何其他项目.

One approach could be to introduce two new m任何-to-m任何 associations; one between Order 和 Item 和 a不her between Order 和 提供者.

然而, 为了让事情更简单, 我们在数据模型中引入了一个新结构,我们称之为" 提供者Order ".

起草的关系

增强的数据模型将需要适应以下关联:

  • 一对多 用户和订单之间的关系(每个用户都可以关联) 0到n 订单) a 一对多 用户和提供者之间的关系(每个用户可能与之关联) 0到n 供应商)

      应用程序.用户= DS.模型.扩展({
        firstName: DS.attr('字符串'),
        姓:DS.attr('字符串'),
        isAdmin: DS.attr(布尔),
    	
        订单:DS.hasM任何('订单', {异步:真}),
        提供者:DS.hasM任何('provider', {异步:真})
      });
    
  • 一对多 订单和提供者Orders之间的关系(每个订单由 1到n 提供者订单):

      应用程序.订单= DS.模型.扩展({
        createdAt: DS.attr(“日期”),
    	
        用户 :           DS.belongsTo('用户', {异步:真}),
        providerOrders: DS.hasM任何('providerOrders', {异步:真})
      });
    
  • 一对多 提供者和提供者Orders之间的关系(每个提供者都可以关联到) 0到n 提供者订单):

      应用程序.供应商= DS.模型.扩展({
        链接:DS.attr('字符串'),
    	
        管理:DS.belongsTo('用户', {异步:真}),
        订单:DS.belongsTo('providerOrder', {异步:真}),
        项目:DS.hasM任何('项目', {异步:真})
      });
    
  • 一对多 提供者Orders和Items之间的关系(每个提供者Order包含) 1到n 项):

      应用程序.提供者订单= DS.模型.扩展({
        // ['processed', 'in_delivery', 'delivered']之一
        状态:DS.attr('字符串'),
    	
        提供者:DS.belongsTo('provider', {异步:真}),
        秩序:DS.belongsTo('订单', {异步:真}),
        项目:DS.hasM任何('item', {异步:真})
      });
    	
      应用程序.项目= DS.模型.扩展({
        名称:DS.attr('字符串'),
        价格:DS.attr(的数量),
    	
        秩序:DS.belongsTo('订单', {异步:真}),
        提供者:DS.belongsTo('provider', {异步:真})
      });
    

别忘了我们的 路线 定义:

应用程序.Orders路线 = 灰烬.路线.扩展({
  模型:功能(){
    //获取/订单
    //检索所有订单.
    返回这.商店.找到(“订单”);
  }
});

现在每个提供者Order都有一个提供者,这是我们的主要目标. 项目从Order移动到提供者Order,假设一个提供者Order中的所有项目都属于单个提供者.

尽量减少代码混乱

不幸的是,这里有一些突破性的变化. 因此,让我们看看灰烬数据如何帮助我们减少代码库中产生的任何代码流失.

之前,我们用 项目.推Object(项). 现在我们需要首先找到合适的提供者Order并向其推送一个Item:

订单.get(“providerOrders”).然后(函数(provider订单) {
        返回providerOrders.findBy(“id”项目.得到(“供应商.id') )
                             .get(“项目”).然后(函数(物品){
                               返回物品.推Object(项);
                             });
     });

因为搅得太多了, 更多的是秩序的工作而不是控制器的工作, 我们最好把代码移到 订单# 推Item:

应用程序.订单= DS.模型.扩展({
  createdAt: DS.attr(“日期”),

  用户:DS.belongsTo('用户', {异步:真}),
  providerOrders: DS.hasM任何('providerOrders', {异步:真}),

  /**返回承诺*/
  推Item:函数(项){
    返回这.get(“providerOrders”).然后(函数(provider订单) {
      返回providerOrders.findBy(“id”项目.得到(“供应商.id') )
                           .get(“项目”).然后(函数(物品){
                             返回物品.推Object(项);
                           });
    });
  }
});

现在我们可以像这样直接在订单上添加项目 订单.推Item(项).

以及列出每个订单的项目:

应用程序.订单= DS.模型.扩展({
  /* ... */

  /**返回承诺*/
  项目:函数(){
    返回这.get(“restaurantOrders”).然后(函数(restaurant订单) {
      var arrayOfPromisesContainingItems = restaurantOrders.mapBy(“项目”)

      返回arrayOfPromisesContainingItems.然后(函数(项目){
        返回物品.reduce(memo, index, element){
          返回的备忘录.推Object(元素);
        },灰烬.A([]));
      });
    });
  }.产权(“restaurantOrders.@each.项目”)
  /* ... */
});

应用程序.Items路线 = 灰烬.路线.扩展({
  模型:功能(){
    //多个GET / id[]查询参数.
    //返回一个承诺.
    返回这.梅尔(“订单.显示的).(“项目”);
  }
});

多态的关系

现在让我们向我们的系统引入一个额外的增强请求,使事情进一步复杂化:

新需求#2:支持多种类型的提供者.

对于我们的简单示例,假设定义了两种类型的提供者(“Shop”和“Book商店”):

应用程序.商店= 应用程序.提供者.扩展({
  状态:DS.attr(字符串)
});

应用程序.书店= 应用程序.提供者.扩展({
  名称:DS.attr(字符串)
});

这里是烬数据的支持 多态的关系 会派上用场的. 安贝数据支持一对一、一对多和多对多的多态关系. 这只需添加属性即可完成 多态:真 符合协会的规定. 例如:

应用程序.供应商= DS.模型.扩展({
  providerOrders: DS.hasM任何('providerOrder', {异步:真})
});

应用程序.提供者订单= DS.模型.扩展({
  提供者:DS.belongsTo('provider',{多态:真正的, 异步: 真正的})
});

上面的 多态 标志表明有各种类型的提供者可以与提供者Order相关联(在我们的示例中是, 商店或书店).

当关系是多态的时,服务器响应应该同时指示ID 返回对象(对象)的类型 RESTSerializer does 这 by default); e.g.:

GET / providerOrders
{
  “providerOrders”:[{
    “状态”:“in_delivery”,
    “提供者”:1、
    “providerType”:“商店”
  }]
}

满足新要求

我们需要多态的提供者和项目来满足需求. 因为提供者Order连接了提供者s和Items, 我们可以将它的关联改为多态关联:

应用程序.提供者订单= DS.模型.扩展({
  提供者:DS.belongsTo('provider',{多态:真正的, 异步: 真正的}),
  项目:DS.hasM任何('item',{多态:真正的,异步:真正的})
});

还有一个问题, 虽然:提供者与Items有非多态关联,但Item是一个抽象类型. 因此,我们有两个选择来解决这个问题:

  1. 要求所有提供程序与相同的项类型相关联(例如.e.,声明与提供者关联的特定类型的Item)
  2. 声明 项目 提供者上的关联是多态的

在本例中,我们需要使用选项#2并声明 项目 提供者上的多态关联:

应用程序.供应商= DS.模型.扩展({
  /* ... */
  项目:DS.hasM任何('项目',{多态:真正的, 异步: 真正的})
});

注意,这并没有引入 任何 code churn; all associations simply work just the way they did prior to 这 change. 烬数据最好的力量!

烬数据 真的 为我所有的数据建模?

当然也有例外, 但我认为ActiveRecord约定是一种标准而灵活的结构化和建模数据的方式, 所以让我向你展示ActiveRecord惯例如何映射到灰烬数据:

has_m任何:用户s通过::所有权或表示中间模型

这将参考一个支点 模型 称为所有权以查找关联的用户. 如果主轴 模型 基本上是一个枢轴 table,您可以避免在安贝数据中创建中间模型,并表示与的关系 DS.hasM任何 在双方.

但是,如果你需要在前端中使用pivot关系, 建立一个所有权模型,包括 DS.belongsTo('用户', {异步:真})DS.belongsTo('provider', {异步:真}),n 添加 a property on both Users 和 提供者s that maps through to the association using Ownership; e.g.:

应用程序.用户= DS.模型.扩展({
  / /省略
  所有权:DS.hasM任何(“所有权”),

  /**返回承诺*/
  供应商:函数(){
    返回这.get(所有权).然后(函数(所有权){
      返回所有权.mapBy(“供应商”);
    });
  }.产权(所有权.@each.提供者”)
});

应用程序.所有权= DS.模型.扩展({
  //['已批准','待定']之一
  状态:DS.attr('字符串'),
  用户:DS.belongsTo('用户', {异步:真}),
  提供者:DS.belongsTo('provider', {异步:真})
});

应用程序.供应商= DS.模型.扩展({
  / /省略
  所有权:DS.hasM任何('owner ', {异步:真}),

  /**返回承诺*/
  用户:函数(){
    返回这.get(所有权).然后(函数(所有权){
      返回所有权.mapBy(“用户”);
    });
  }.产权(所有权.@each.用户”)
});

Has_m任何:映射,as: locatable

在我们的ActiveRecord对象中,我们有一个典型的多态关系:

class User < ActiveRecord::Base
  Has_m任何:映射,as: locatable
  Has_m任何:locations,通过::mappings
结束

class Mapping < ActiveRecord::Base
  Belongs_to:可定位的,多态的:真正的
  belongs_to:位置
结束

class Location < ActiveRecord::Base
  has_m任何:映射
  has_m任何:用户s,通过::映射,source::locatable, source_type: 'User'
  has_m任何:providers,通过::映射,source::locatable, source_type: '提供者'

  def定位
    用户+提供商
  结束
结束

这是一个多(多态)对多(正常的非多态)关系. 在烬数据中,我们可以用多态来表达这一点 DS.hasM任何('locatable',{多态:真正的, 异步: 真正的}) 静态的 DS.hasM任何('location', {异步:真}):

应用程序.定位 = DS.模型.扩展({
  地点:DS.hasM任何('location', {异步:真})
});

应用程序.用户=应用程序.定位.扩展({
  用户名:DS.attr(字符串)
});

应用程序.位置= DS.模型.扩展({
  定位:DS.hasM任何('locatable',{多态:真正的, 异步: 真正的})
});

对于定位s,比如User,服务器应该返回关联位置的id:

GET /用户
{
  “用户”:(
    {
      "id": "1",
      “用户名”:“Pooyan”,
      “位置”(“1”):
    }
  ]
}

对于Location,服务器应该返回对象数组中定位的ID和类型:

GET /位置
{
  “位置”:(
    {
      "id": "1",
      “定位”:(
        {"id": "1", "type": "用户"},
        {"id": "2", "type": "provider"}
      ]
    }
  ]
}

同样,你也可以用静态多对多关系按类型表示关系:

应用程序.用户=应用程序.定位.扩展({
  用户名:DS.attr('字符串'),
  地点:DS.hasM任何('location', {异步:真})
});

应用程序.提供商=应用程序.定位.扩展({
  链接:DS.attr('字符串'),
  地点:DS.hasM任何('location, {异步:真})
});

应用程序.位置= DS.模型.扩展({
  用户:DS.hasM任何('用户', {异步:真}),
  提供者:DS.hasm任何 ('provider', {异步:真})
});

那么实时数据呢?

烬数据 , 推Payload更新. 你总是可以手动将新的/更新的记录推送到安贝数据的本地缓存(称为商店),它将处理所有其余的.

应用程序.应用程序lication路线 = 灰烬.路线.扩展({
  激活:函数(){
    套接字.(“recordUp日期d”,函数(响应){
      Var类型=响应.类型;
      无负载=响应.有效载荷;
      这.商店.推Payload(类型、载荷);
    });
  }
});

我个人只在有效负载非常小的事件中使用套接字. 典型的事件是' recordu更新d ',其有效负载为 {"type": "shop", "id": "14"} 然后在应用程序lication路线中,我会检查那条记录是否在本地缓存(商店)中如果是,我会重新取回它.

应用程序.应用程序lication路线 = 灰烬.路线.扩展({
  激活:函数(){
    套接字.(“recordUp日期d”,函数(响应){
      Var类型=响应.类型;
      Var id = response.id;

      如果(这.商店.hasRecordForId(类型,id)){
        这.商店.找到(类型、id);
      }
    });
  }
});

通过这种方式,我们可以发送记录更新事件到所有客户端,而不会产生不可接受的开销.

安贝数据基本上有两种处理实时数据的方法:

  1. 为您的实时通信通道编写一个适配器,并使用它而不是RESTAdapter.
  2. 只要有记录,就把它们推送到主存储.

第一个选择的缺点是,它有点类似于重新发明轮子. 对于第二个选项,我们需要访问主存储,它在所有路由上都可用 路线#商店.

总结

在本文中, 我们已经向您介绍了烬数据的关键结构和范式, 展示它可以为开发人员提供的价值. 烬数据提供了一个更灵活和精简的开发工作流程, 最大限度地减少代码混乱,以响应那些影响很大的更改.

前期投资(时间和学习曲线),你在使用烬数据为您的项目将毫无疑问证明是值得的,因为你的系统不可避免地发展和需要扩展, 修改, 和增强.


附录:高级烬数据主题

本附录介绍了一些更高级的烬数据主题,包括:

模块化设计

烬数据在引擎盖下有一个模块化的设计. 主要组成部分包括:

  1. 适配器 负责处理通信,目前只有HTTP上的REST.
  2. 序列化器 从JSON管理模型的创建,或者相反.
  3. 商店 缓存创建的记录.
  4. 容器 把这些都粘在一起.

这种设计的好处包括:

  1. 数据的反序列化和存储工作独立于所使用的通信通道和所请求的资源.
  2. 配置工作, 例如Active模型Serializer或EmbeddedRecordsMixin, 是开箱即用的.
  3. 数据来源(e).g., LocalStorage, CouchDB实现等.)可以通过更换适配器来交换.
  4. 尽管在配置上有很多约定, 可以配置所有内容并与社区共享您的配置/实现.

侧面加载

安贝数据 supports “sidseloading” of data; i.e., 指示应该检索的辅助数据(以及请求的主数据),以便帮助合并多个相关的HTTP请求.

一个常见的用例是加载关联的模型. 例如,每个商店都有许多杂货,所以我们可以包括所有相关的杂货 /商店 回应:

GET /商店
{
  “商店”:(
    {
      "id": "14",
      "杂货":["98","99","112"]
    }
  ]
}

当杂货协会被访问时,烬数据将发出:

GET /杂货?id [] = 98&id [] = 99&id [] = 112
{
  “食品”:(
    {" id ": " 98 "、“提供者”:“14”、“类型”:“店”},
    {" id ": " 99 "、“提供者”:“14”、“类型”:“店”},
    {"id": "112", "provider": "14", "type": "shop"}
  ]
}

但是,如果我们返回关联的杂货in /商店 端点,烬数据将不需要发出另一个请求:

GET /商店
{
  “商店”:(
    {
      "id": "14",
      "杂货":["98","99","112"]
    }
  ],

  “食品”:(
    {" id ": " 98 "、“提供者”:“14”、“类型”:“店”},
    {" id ": " 99 "、“提供者”:“14”、“类型”:“店”},
    {"id": "112", "provider": "14", "type": "shop"}
  ]
}

安贝数据接受链接代替关联id. 当指定为链路的关联被访问时, 安贝数据将向该链接发出GET请求,以获取相关的记录.

例如,我们可以返回一个杂货协会的链接:

GET /商店
{
  “商店”:(
    {
      "id": "14",
      “链接”:{
        :“杂货/商店/ 14 /杂货”
      }
    }
  ]
}

然后烬数据将发出请求到 /商店/ 14 /杂货

GET /商店/ 14 /杂货
{
  “食品”:(
    {" id ": " 98 "、“提供者”:“14”、“类型”:“店”},
    {" id ": " 99 "、“提供者”:“14”、“类型”:“店”},
    {"id": "112", "provider": "14", "type": "shop"}
  ]
}

Keep in mind that you still need to represent the association in the data; links just suggest a new HTTP request 和 won’t affect associations.

活动模型序列化器和适配器

可以说, Active模型SerializerActive模型Adapter 在实践中使用的比 RESTSerializerRESTAdapter. 特别是当后端使用Ruby on Rails和 Active模型:序列化器 宝石,最好的选择是使用 Active模型SerializerActive模型Adapter,因为他们支持 Active模型:序列化器 开箱即用.

不过,幸运的是,两者的用法不同 Active模型Serializer/Active模型AdapterRESTSerializer/RESTAdapter are quite limited; namely:

  1. Active模型Serializer 将使用snake_case字段名而 RESTSerializer 需要camelcase字段名.
  2. Active模型Adapter 向snake_case API方法发出请求 RESTSerializer camelcase API方法的问题.
  3. Active模型Serializer 期望关联相关的字段名以结尾 _id or _idRESTSerializer 期望关联相关的字段名与关联字段名相同.

不管适配器和序列化器的选择是什么,安贝数据模型都是完全相同的. 只有JSON表示和API端点是不同的.

以我们最后的提供者Order为例:

应用程序.应用程序licationSerializer = DS.Active模型Serializer.扩展({
});

应用程序.提供者订单= DS.模型.扩展({
  // ['processed', 'in_delivery', 'delivered']之一
  状态:DS.attr('字符串'),

  提供者:DS.belongsTo('provider',{多态:真正的, 异步: 真正的}),
  秩序:DS.belongsTo('订单', {异步:真}),
  项目:DS.hasM任何('item',{多态:真正的,异步:真正的})
});

使用活动模型序列化器和适配器,服务器应该期望:

Post / provider_订单s
{
  “provider_订单”:(
    “状态”:“”,
    "provider": {"id": "13", "type": "shop"}
    “订单_id”:“68”,
  ]
}

,应该这样回答:

GET / provider_订单s
{
  “provider_订单s”:(
    "id":       "1",
    “状态”:“”,
    "provider": {"id": "13", "type": "shop"}
    “订单_id”:“68”,
    “物品”:(
      {"id": "57", "type": "grocery"},
      {"id": "89", "type": "grocery"}
    ]
  ]
}

嵌入式记录混合

DS.EmbeddedRecordsMixinDS.Active模型Serializer 这允许配置如何序列化或反序列化关联. 虽然还没有完成(特别是关于多态关联), 尽管如此,这还是很有趣.

您可以选择:

  1. 不序列化或反序列化关联.
  2. 序列化或反序列化与id或多个id的关联.
  3. 序列化或反序列化与嵌入模型的关联.

这在一对多关系中特别有用,默认情况下, DS.hasM任何 关联的id不会添加到序列化的对象中. 以有许多商品的购物车为例. 在本例中,在已知Items的情况下创建Cart. 然而, 当你保存购物车的时候, 安贝数据不会自动将相关项目的id放在请求有效负载上.

使用 DS.EmbeddedRecordsMixin,但是,可以告诉安贝数据序列化Cart上的项目id,如下所示:

应用程序.CartSerializer = DS.Active模型Serializer
                       .扩展(DS.EmbeddedRecordsMixin)
                       .延长{
                         attrs: {
                           //谢谢!
                           项:{序列化:“id”,反序列化:“id”}
                         }
                       });

应用程序.手推车= DS.模型.扩展({
  项目:DS.hasM任何('item', {异步:真})
});

应用程序.项目= DS.模型.扩展({
  购物车:DS.belongsTo('item', {异步:真})
});

如上面的例子所示, EmbeddedRecordsMixin允许显式规范要序列化和/或反序列化的关联 attrs object. 的有效值 序列化反序列化 是: - 'no':序列化/反序列化数据中不包含关联 - 'id' or “id”:在序列化/反序列化数据中包含关联ID - 的记录':包括实际属性(例如.e.(记录字段值)作为序列化/反序列化数据中的数组

关联修饰符(异步、反向和多态)

支持以下关联修饰符: 多态, , 异步

多态的修饰符

在多态关联中, 关联的一方或双方代表一个对象类, 而不是一个特定的对象.

回忆起我们的 前面的例子 对于一个博客,我们需要支持标记文章和页面的能力. 为了支持这一点,我们得出了以下模型:

  • 每个帖子都是可标记的,并且有许多标签
  • 每个页面都是可标记的,并且有许多标签
  • 每个标签都有许多多态标签

按照这个模型,a 多态 修饰符可用于声明标签与任何类型的“可标记”(可以是Post或Page)相关。, 如下:

// 机动空挡是可以被标记的东西.e.,它有标签)
应用程序.机动空挡 = DS.模型.扩展({
  标签:DS.hasM任何(标签)
});

//页面是一种可标记的类型
应用程序.页面= 应用程序.机动空挡.扩展({});

// Post是一种可标记的类型
应用程序.Post = 应用程序.机动空挡.扩展

应用程序.标签= DS.模型.扩展({
  //此关联的“另一方”(i.e.即“可标记”)是多态的
  机动空挡:DS.belongsTo(' tagable ',{多态:真正的})
});

逆修饰符

通常关联是双向的. 例如, “帖子有很多评论”将是协会的一个方向, 而“评论属于一个帖子”将是另一个(如.e.(“逆”)这种联系的方向.

在关联中没有歧义的情况下, 只需要指定一个方向,因为安贝数据可以推断出关联的反向部分.

然而, 在模型中的对象彼此之间有多个关联的情况下, 安贝数据不能自动导出每个关联的逆,因此需要使用 invers 修饰符.

例如,考虑这样一个情况:

  • 每个页面可能有许多用户作为合作者
  • 每个页面可能有许多用户作为维护者
  • 每个用户可以有多个收藏页面
  • 每个用户可能有多个关注页面

这需要在我们的模型中指定如下:

应用程序.用户= DS.模型.扩展({
  favoritePages: DS.hasM任何('page',{逆:'favoritors}),
  followedPages: DS.hasM任何('page',{逆:'follower '}),
  collaboratePages: DS.hasM任何('page',{逆:'合作者'}),
  maintainedPages: DS.hasM任何('page',{逆:'维护者'})
});

应用程序.页= DS.模型.扩展({
  最广泛:DS.hasM任何('用户',{逆:' favoritpages '}),
  追随者:DS.hasM任何('用户', {逆: ' followwedpages '}),
  合作者:DS.hasM任何('用户', {逆: 'collaboratedPages}),
  维护人员:DS.hasM任何('用户', {逆: 'maintainedPages'})
});

异步修饰符

当需要根据相关关联检索数据时, 关联的数据可能已经加载,也可能尚未加载. 否则,同步关联将抛出错误,因为关联数据尚未加载.

{异步:真} 指示对关联数据的请求应异步处理. 因此,请求立即返回一个承诺,一旦检索到相关数据并且可用,就调用提供的回调.

异步 is ,获取关联对象的操作如下:

帖子.get(评论).推Object(评论);

异步 is 真正的,获取关联对象将按如下方式完成(注意指定的回调函数):

帖子.get(评论).然后(函数(评论){
  评论.推Object(评论);
})

注意: 的当前默认值 异步 is ,但在烬数据1.默认值为0 真正的.

GET请求中的' id '参数

默认情况下,安贝数据期望资源不嵌套. 以有很多评论的帖子为例. 在REST的典型解释中,API端点可能看起来像这样:

GET /用户
/用户/:id /职位
GET /用户/ id /文章/:id /评论

然而, 安贝数据 expects API 结束points to be flat, 不 nested; e.g.:

GET /用户 with ?id[]=4作为查询参数.
GET /帖子 with ?id [] = 18&id[]=27作为查询参数.
GET /评论 with ?id [] = 74&id [] = 78&id [] = 114&id[]=117作为查询参数.

在上面的例子中, id 是数组的名称, [] 表示该查询参数为数组,且 = 表示新的ID正在被压入阵列.

现在烬数据可以避免下载资源,它已经有或推迟下载他们到最后一刻.

此外,要将数组参数列入白名单,请使用 StrongParameters宝石,您可以将其声明为 参数个数.要求(店).许可证(item_id: []).

就这一主题咨询作者或专家.
预约电话

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.