Ember-Data:如何使用`DS.Adapter.findHasMany`

时间:2013-01-17 15:32:57

标签: ember.js ember-data

更新

请注意,此问题适用于Ember Data pre-1.0 beta,通过URL加载关系的机制在1.0 beta后发生了显着变化!


我前一段时间问了一个更长的问题,但自从那时候图书馆发生了变化,我会问一个更简单的版本:

您如何使用DS.Adapter.findHasMany?我正在构建一个适配器,我希望能够在关系属性的get上加载关系的内容,而这个看起来像一样。但是,看看Ember数据代码,我看不出如何调用这个函数(如果需要,我可以在评论中解释)。

我的后端没有一种简单的方法可以在我发送的JSON中的属性键中包含一个id数组 - 我正在使用的序列化器不允许我在任何好的地方挂钩以改变它,并且它也是计算上昂贵的。

曾几何时,Ember Data首页显示了这样做“延迟加载”的一个例子......这是可能的,还是路径图中列出的“处理部分加载的记录”,而不是还没完成。?

我正在使用API​​修订版11,即1月15日的主分支。

更新

好的,以下内容大部分都有效。首先,我根据测试用例的实现在我的适配器中创建了以下findHasMany方法:

findHasMany: function(store, record, relationship, details) {
    var type = relationship.type;
    var root = this.rootForType(type);
    var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);

    this.ajax(url, "GET", {
        success: function(json) {
            var serializer = this.get('serializer');
            var pluralRoot = serializer.pluralize(root);
            var hashes = json[pluralRoot]; //FIXME: Should call some serializer method to get this?
            store.loadMany(type, hashes);

            // add ids to record...
            var ids = [];
            var len = hashes.length;
            for(var i = 0; i < len; i++){
                ids.push(serializer.extractId(type, hashes[i]));
            }
            store.loadHasMany(record, relationship.key, ids);
        }
    });
}

上面的先决条件是您必须在序列化程序中使用运行良好的extractId方法,但在大多数情况下RESTAdapter内置的方法可能会这样做。

这个有效,但有一个重大问题,我在这种延迟加载方法的任何尝试中都没有真正解决过:如果原始记录是从服务器重新加载的,一切都进入底池。显示这一点的最简单的用例是,如果您加载单个记录,然后检索hasMany,然后再加载所有父记录。例如:

var p = App.Post.find(1);
var comments = p.get('comments');
// ...later...
App.Post.find();

在仅上述代码的情况下,当Ember Data重新实现记录时,它会识别记录中已存在值(posts/1),尝试重新填充它,并遵循不同的代码路径,它将JSON哈希中的URL字符串视为单字符ID数组。具体来说,它将值从JSON传递给Ember.EnumerableUtils.map,这可以理解地将字符串的字符枚举为数组成员。

<击> 因此,我尝试通过“修补”DS.Model.hasManyDidChange来解决这个问题,如下所示:

// Need this function for transplanted hasManyDidChange function...
var map = Ember.EnumerableUtils.map;

DS.Model.reopen({

});

(^没关系,这是一个非常糟糕的主意。)

更新2

当从服务器重新加载父模型时,我发现我必须做(至少)一件事来解决上面提到的问题。将URL拆分为单个字符的代码路径位于DS.Model.reloadHasManys中。所以,我用以下代码覆盖了这个方法:

DS.Model.reopen({

  reloadHasManys: function() {
    var relationships = get(this.constructor, 'relationshipsByName');
    this.updateRecordArraysLater();
    relationships.forEach(function(name, relationship) {
      if (relationship.kind === 'hasMany') {

        // BEGIN FIX FOR OPAQUE HASMANY DATA
        var cachedValue = this.cacheFor(relationship.key);
        var idsOrReferencesOrOpaque = this._data.hasMany[relationship.key] || [];
        if(cachedValue && !Ember.isArray(idsOrReferencesOrOpaque)){
          var adapter = this.store.adapterForType(relationship.type);
          var reloadBehavior = relationship.options.reloadBehavior;
          relationship.name = relationship.name || relationship.key; // workaround bug in DS.Model.clearHasMany()?
          if (adapter && adapter.findHasMany) {
            switch (reloadBehavior) {
              case 'ignore':
                //FIXME: Should probably replace this._data with references/ids, currently has a string!
                break;
              case 'force':
              case 'reset':
              default:
                this.clearHasMany(relationship);
                cachedValue.set('isLoaded', false);
                if (reloadBehavior == 'force' || Ember.meta(this).watching[relationship.key]) {
                  // reload the data now...
                  adapter.findHasMany(this.store, this, relationship, idsOrReferencesOrOpaque);
                } else {
                  // force getter code to rerun next time the property is accessed...
                  delete Ember.meta(this).cache[relationship.key];
                }
                break;
            }
          } else if (idsOrReferencesOrOpaque !== undefined) {
            Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter);
            Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany);
          }  
        } else {
          this.hasManyDidChange(relationship.key);
        }
        //- this.hasManyDidChange(relationship.key);
        // END FIX FOR OPAQUE HASMANY DATA

      }
    }, this);
  }

});

通过这种添加,使用基于URL的hasManys 几乎可用,还有两个主要问题:

首先,反belongsTo关系无法正常工作 - 您必须将它们全部删除。这似乎是使用ArrayProxies完成RecordArrays的方式的一个问题,但它很复杂。当父记录被重新加载时,两个关系都被处理为“删除”,所以当循环遍历数组时,belongsTo解除关联代码同时从数组中删除项目,然后循环因为尝试访问而变得怪异一个不再存在的索引。我还没想出这个,这很难。

其次,它通常效率低下 - 我最终经常从服务器重新加载hasMany ......但至少我可以通过在服务器端发送一些缓存头来解决这个问题。

任何人都试图在这个问题中使用这些解决方案,我建议你将上面的代码添加到你的应用程序中,它可能会让你最终到达某个地方。但是,我认为这确实需要在Ember Data中修复才能正常工作。

我希望最终得到更好的支持。一方面,他们明确指出的JSONAPI方向说这种事情是规范的一部分。但另一方面,Ember Data 0.13(或rev 12?)更改了默认的序列化格式,因此如果您想这样做,您的URL必须位于名为*_ids的JSON属性中... child_object_ids ...如果在这种情况下你甚至不是你发送的ID!这似乎表明,在用例列表中不使用ID数组并不高。任何Ember Data开发者都会这样说:请支持这个功能!

欢迎进一步思考!

4 个答案:

答案 0 :(得分:3)

有效载荷需要包含“别的东西”而不是数组。

而不是一组id

对于RESTAdapter,返回的JSON就是:

{blog: {id: 1, comments: [1, 2, 3]}

如果您想手动/不同地处理关联,您可以返回类似的JSON:

{blog: {id: 1, comments: "/posts/1/comments"}

然后由您的适配器来获取指定URL中的数据。

请参阅相关测试:https://github.com/emberjs/data/blob/master/packages/ember-data/tests/integration/has_many_test.js#L112

答案 1 :(得分:1)

我很高兴找到这篇文章,帮助了我。这是我的版本,基于当前的ember-data和您的代码。

findHasMany: function(store, record, relationship, details) {
    var adapter = this;
    var serializer = this.get('serializer');
    var type = relationship.type;
    var root = this.rootForType(type);
    var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
    return this.ajax(url, "GET", {}).then(function(json) {
            adapter.didFindMany(store, type, json);
            var list = $.map(json[relationship.key], function(o){ return serializer.extractId(type, o);});
            store.loadHasMany(record, relationship.key, list);
        }).then(null, $.rejectionHandler);
},

对于重新加载问题,我根据我在另一个地方找到的代码,在序列化器内部进行了覆盖:

materializeHasMany: function(name, record, hash, relationship) {
    var type = record.constructor,
            key = this._keyForHasMany(type, relationship.key),
            cache = record.cacheFor('data');
    if(cache) {
        var hasMany = cache.hasMany[relationship.key];
        if (typeof(hasMany) == 'object' || hasMany instanceof Object) {
            record.materializeHasMany(name, hasMany);
            return;
        }
    }
    var value = this.extractHasMany(type, hash, key);
    record.materializeHasMany(name, value);
}

我还在努力搞清楚分页,因为我正在使用的一些系列需要它。

答案 2 :(得分:0)

我更接近于使用修订版13并基于sfossen的findHasMany实现。

对于拥有hasMany关系'blogPosts'的Ember模特'作者',我的其余api看起来像'/ api / authors /:author_id / blog_posts'。当查询id为11的作者的其余api时,blog_posts字段为'/ authors / 11 / blog_posts'。

我现在看到服务器返回相关的博客帖子,但是Ember仍然会抛出一个模糊的错误,它在呈现页面时无法从未定义的模型对象中读取“id”。所以我还没到那里,但至少从其他服务中正确地请求了相关数据。

我的完整适配器:

App.Adapter = DS.RESTAdapter.extend({
    url: 'http://localhost:3000',
    namespace: 'api',
    serializer: DS.RESTSerializer.extend({
        keyForHasMany: function(type, name) {
            return Ember.String.underscore(name);
        },
        extractHasMany: function(record, json, relationship) {
            var relationShip = relationship + '_path';
            return { url : json[relationShip] }
        }
    }),
    findHasMany: function(store, record, relationship, details) {
        var type = relationship.type;
        var root = this.rootForType(type);
        var url = this.url + '/' + this.namespace + details.url;
        var serializer = this.get('serializer');

        return this.ajax(url, "GET", {}).then(
            function(json) {
                var relationship_key = Ember.String.underscore(relationship.key);
                store.loadMany(type, json[relationship_key]);
                var list = $.map(json[relationship_key], function(o){
                        return serializer.extractId(type, o);}
                );
                store.loadHasMany(record, relationship.key, list);
            }).then(null, $.rejectionHandler);
    }
});

答案 3 :(得分:0)

这是我的解决方案,但它是在Ember-data 0.14上,所以即使我们仍在这个代码基础上,世界仍然继续前进:

  findHasMany: function(store, record, relationship, details) {
    if(relationship.key !== 'activities') {
      return;
    }

    var type = relationship.type,
        root = this.rootForType(type),
        url = this.url + details.url,
        self = this;

    this.ajax(url, "GET", {
      data: {page: 1}
    }).then(function(json) {
      var data = record.get('data'),
        ids = [],
        references = json[relationship.key];

      ids = references.map(function(ref){
        return ref.id;
      });

      data[relationship.key] = ids;

      record.set('data', data);

      self.didFindMany(store, type, json);
      record.suspendRelationshipObservers(function() {
        record.hasManyDidChange(relationship.key);
      });
    }).then(null, DS.rejectionHandler);
  },

我发现使用为我工作的ID替换数据。