个性化阅读
专注于IT技术分析

Ember Data:Ember数据库的综合教程

本文概述

Ember Data(又称ember-data或ember.data)是一个库, 用于在Ember.js应用程序中可靠地管理模型数据。 Ember Data的开发人员表示, 它的设计与基础的持久性机制无关, 因此它与基于HTTP的JSON API以及与流式WebSockets或本地IndexedDB存储一样有效。它提供了你在服务器端对象关系映射(ORM)中可以找到的许多功能, 例如ActiveRecord, 但是它是专门为浏览器中的JavaScript的独特环境而设计的。

尽管Ember Data可能需要花费一些时间, 但一旦这样做, 你可能会发现它非常值得投资。最终, 它将使你的系统开发, 增强和维护变得更加容易。

使用Ember Data模型, 适配器和序列化器表示API时, 每个关联都简单地成为字段名称。这封装了每个关联的内部细节, 从而使其余代码免受关联本身的更改的影响。例如, 如果某个特定的关联是多态的, 或者是由许多关联组成的映射的结果, 则其余的代码将不在乎。

而且, 即使你的代码库很重要, 代码库也基本上不受后端更改的影响, 因为你的代码库希望的只是模型上的字段和函数, 而不是模型的JSON或XML或YAML表示形式。

在本教程中, 我们将重点介绍一个真实的示例, 介绍Ember Data的最显着功能, 并演示它如何最大程度地减少代码流失。

还提供了附录, 讨论了一些更高级的Ember Data主题和示例。

注意:本文假定对Ember.js有一些基本的了解。如果你不熟悉Ember.js, 请查看我们流行的Ember.js教程以获取介绍。我们还提供了俄语, 葡萄牙语和西班牙语的全栈JavaScript指南。

Ember Data价值主张

Ember数据库如何帮助满足客户需求的示例。

让我们首先考虑一个简单的例子。

假设我们有一个基本博客系统的工作代码库。该系统包含帖子和标签, 它们之间具有多对多关系。

一切正常, 直到我们需要支持Pages。该要求还指出, 由于可以在WordPress中标记页面, 因此我们也应该能够这样做。

因此, 现在, 标签将不再仅适用于帖子, 也可能适用于页面。结果, 我们在标签和帖子之间的简单关联将不再足够。相反, 我们需要多对多的单边多态关系, 例如:

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

过渡到这种新的, 更复杂的关联集可能会在我们的整个代码中产生重大影响, 从而导致大量流失。由于我们不知道如何将多态关联序列化为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上的标签关联名称保持不变。另外, 我们其余的代码库仅依赖于标签关联的存在, 而忽略了其详细信息。

Ember Data入门

在深入探讨真实示例之前, 让我们回顾一些Ember Data基础知识。

路线和模型

在Ember.js中, 路由器负责显​​示模板, 加载数据以及设置应用程序状态。路由器会将当前网址与你定义的路由进行匹配, 因此, 路由负责指定要显示模板的模型(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接受四种数据类型:字符串, 数字, 布尔值和日期。

RESTSerializer

默认情况下:

  • Ember Data使用RESTSerializer从API响应创建对象(反序列化), 并为API请求生成JSON(序列化)。
  • RESTSerializer期望由DS.belongsTo创建的字段在服务器的JSON响应中包含一个名为user的字段。该字段包含引用记录的ID。
  • RESTSerializer将用户字段添加到传递给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){返回评论。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添加到Comment:

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

不过请注意, 默认情况下, RESTSerializer不会将DS.hasMany关联的ID添加到其序列化的对象中, 因为这些关联是在”许多”侧指定的(即那些具有DS.belongsTo关联的关联)。因此, 在我们的示例中, 尽管帖子有很多注释, 但是这些ID不会添加到Post对象中:

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

要”强制” DS.hasMans 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:启用单个订单即可包含来自多个提供商的商品。

在现有系统中, 提供者和订单之间存在一对多关系。但是, 一旦我们扩展订单以包括来自多个提供商的项目, 这种简单的关系将不再足够。

具体地, 如果提供商与整个订单相关联, 则在增强型系统中, 该订单也可能很好地还包括从其他提供商订购的项目。因此, 需要一种方法来指示每个订单的哪一部分与每个提供者有关。此外, 当提供者访问他们的订单时, 他们应该只对从他们那里订购的项目具有可见性, 而不是客户可能从其他提供商那里订购的任何其他项目。

一种方法是引入两个新的多对多关联;一个在订单和商品之间, 另一个在订单和提供者之间。

但是, 为了简化起见, 我们在数据模型中引入了一种新的构造, 我们称之为” 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})
  });

订单和ProviderOrder之间的一对多关系(每个订单由1到n个ProviderOrder组成):

  App.Order = DS.Model.extend({
    createdAt:      DS.attr('date'), user:           DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true})
  });

Provider和ProviderOrder之间的一对多关系(每个Provider可能与0到n个ProviderOrder相关联):

  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})
  });

ProviderOrder和项之间的一对多关系(每个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都有一个Provider, 这是我们的主要目标。项目从订单移动到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:支持多种类型的提供程序。

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

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})
});

上面的多态标记表示可以与ProviderOrder关联的各种类型的Provider(在我们的例子中是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})
});

但是, 还有一个问题:提供程序与Items具有非多态关联, 但是Item是抽象类型。因此, 我们有两个解决方案:

  1. 要求所有提供者都与相同的项目类型相关联(即, 声明与提供者相关联的特定类型的项目)
  2. 将提供者上的项目关联声明为多态

在我们的例子中, 我们需要使用选项#2并将Provider上的items关联声明为多态的:

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

请注意, 这不会引起任何代码混乱。所有协会都只是按照他们在变更之前所做的方式工作。 Ember Data的强大功能!

Ember Data可以真正为我的所有数据建模吗?

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

has_many:用户通过::所有权或表示中间模型

这将查询称为所有权的数据透视模型以查找关联的用户。如果数据透视表模型基本上是数据透视表, 则可以避免在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:可定位

在我们的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})
});

对于可定位对象(如用户), 服务器应返回关联位置的ID:

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

对于位置, 服务器应在对象数组中返回ID和Locatable类型:

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和更新。你始终可以手动将新的/更新的记录推入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);
    });
  }
});

我个人仅将套接字用于有效载荷很小的事件。一个典型的事件是’recordUpdated’, 其有效载荷为{” type”:” shop”, ” id”:” 14″}, 然后在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. 适配器负责处理通信, 当前仅用于HTTP上的REST。
  2. 序列化程序管理从JSON或相反的模型的创建。
  3. 存储缓存创建的记录。
  4. 容器将所有这些粘合在一起。

这种设计的优点包括:

  1. 数据的反序列化和存储与所使用的通信通道和所请求的资源无关。
  2. 开箱即用地提供了工作配置, 例如ActiveModelSerializer或EmbeddedRecordsMixin。
  3. 数据源(例如LocalStorage, CouchDB实现等)可以通过更改适配器来换入和换出。
  4. 尽管在配置方面有很多约定, 但可以配置所有内容并与社区共享你的配置/实现。

侧面装载

Ember Data支持数据的”侧载”;即, 指示应检索的辅助数据(以及请求的主要数据), 以帮助合并多个相关的HTTP请求。

一个常见的用例是侧面加载关联的模型。例如, 每个商店都有许多杂货, 因此我们可以在/ 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端点中返回关联的食品杂货, 则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之间的用法差异是非常有限的。即:

  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序列化程序和适配器, 服务器应该期望:

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的扩展, 它允许配置关联如何序列化或反序列化。尽管尚未完成(特别是在多态关联方面), 但它仍然很有趣。

你可以选择:

  1. 不序列化或反序列化关联。
  2. 序列化或反序列化具有ID或ID的关联。
  3. 序列化或反序列化与嵌入式模型的关联。

这在一对多关系中特别有用, 在默认情况下, DS.hasMany关联的ID不会添加到序列化的对象中, 而在一对多关系中。以包含许多物品的购物车为例。在此示例中, 在已知项的同时创建购物车。但是, 当你保存购物车时, 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对象序列化和/或反序列化的关联。序列化和反序列化的有效值为:-‘no’:在序列化/反序列化的数据中不包含关联-‘id’或’ids’:在序列化/反序列化的数据中包含关联的ID-‘records’:包括实际属性(即记录字段值)为序列化/反序列化数据中的数组

关联修饰符(异步, 逆向和多态)

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

多态编辑

在多态关联中, 关联的一侧或两侧代表一类对象, 而不是特定的对象。

回想一下我们先前的博客示例, 在该示例中, 我们需要支持同时标记帖子和页面的功能。为了支持这一点, 我们得出了以下模型:

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

按照该模型, 可以使用多态修饰符来声明Tag与任何类型的” Taggable”(可以是Post或Page)相关, 如下所示:

// A Taggable is something that can be tagged (i.e., 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 (i.e., the 'taggable') is polymorphic
  taggable: DS.belongsTo('taggable', {polymorphic: true})
});

反向编辑

通常, 关联是双向的。例如, “帖子有很多评论”将是关联的一个方向, 而”评论属于帖子”将是该关联的另一个方向(即”相反”)。

在关联中不存在歧义的情况下, 只需指定一个方向, 因为Ember Data可以推断出关联的倒数部分。

但是, 在模型中的对象具有多个关联的情况下, Ember Data无法自动导出每个关联的逆关系, 因此需要使用invers修饰符进行指定。

例如, 考虑以下情况:

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

在我们的模型中, 需要指定以下内容:

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:true}指示对关联数据的请求应异步处理。因此, 该请求会立即返回一个Promise, 并且一旦关联数据已获取并可用, 就会调用提供的回调。

如果async为false, 则获取关联对象的操作如下:

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

当async为true时, 将按以下方式获取关联的对象(注意指定的回调函数):

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

请注意:async的当前默认值为false, 但在Ember Data 1.0中, 默认值为true。

GET请求中的” ids”参数

默认情况下, Ember Data预计资源不会嵌套。以有很多评论的帖子为例。在REST的典型解释中, API端点可能如下所示:

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

但是, Ember Data期望API端点是平坦的, 并且不嵌套;例如。:

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.

在上面的示例中, ids是数组的名称, []指示此查询参数是一个数组, 而=指示将新的ID压入该数组。

现在, Ember Data可以避免下载已经拥有的资源, 也可以将下载推迟到最后一刻。

另外, 要使用StrongParameters gem将阵列参数列入白名单, 你可以将其声明为params.require(:shop).permit(item_ids:[])。

赞(0)
未经允许不得转载:srcmini » Ember Data:Ember数据库的综合教程

评论 抢沙发

评论前必须登录!