ember-data:正在加载有多个关联按需 [英] ember-data: Loading hasMany association on demand

查看:200
本文介绍了ember-data:正在加载有多个关联按需的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

(更新为ember-data API Rev 11 ...)



TL; DR



什么是正确的方式使用 DS.Adapter.findAssociation(...) DS.Adapter.findHasMany(.. 。)需要加载 hasMany 关联?特别是,一旦加载子记录,您如何处理从服务器重新加载父记录清空hasMany数组的事实?我不想希望(也许不能)将父记录中的子记录的id包含在数组中。或者还有另一种方法可以做到这一点,我失踪了?



作为一个附注,我对在<$ c $中应该传递什么选项感到困惑c> hasMany / belongsTo 关键连接的定义(如果我没有边框数据或ids数组,我应该使用映射)所以如果你认为我的问题可能在于我的关联定义,那么很有可能你是对的。



长版本



我正在编写自己的子类DS.RESTAdapter 将ember-data与ASP.NET WebAPI后端(使用Entity Framework)进行绑定。到目前为止这么好,但是我有一段时间让协会正常工作。



类似于这个海报,我注意到,ember数据的首页表示 使用表示如果你有一个 hasMany 关联在您的模型,而您获取该属性,商店将发出一个请求的子记录。从页面引用:


如果您要请求个人资料,如下所示:

  author.get( '轮廓'); 

... REST适配器会向URL发送请求/profiles?author_id = 1.


这意味着,如果您不负载并且不包含ids数组,会发生什么。我意识到这些文档有些过时,但是我无法在API版本7或更新版本9中发生这种情况。但是在版本9中的中,我找到了 findAssociation 方法,在版本11中有 findHasMany 方法,我猜想是可能已经用来做



为什么不包括ids或sideload的数组?



三个主要原因我不想这样做(可能不能):


  1. 这不是很明显如何使用ASP.NET WebAPI来完成这些任务之一,至少不能使用我所使用的简单的基于装饰的方法。而且,我真的很喜欢后端的简单性和薄度,EF和WebAPI几乎完全是每个实体的样板,我已经完成了!我甚至得到了免费的OData过滤支持。


  2. 我的子记录通常会通过昂贵的查询生成(例如聚合...指标汇总)。对于单个父实体,还有很多不同类的子实体。所以即使获得的所有的id也是昂贵的,生成和加载所有子记录是不成问题的。


  3. 我有一个子实体,主键是一个复合键。我甚至没有看到一个这样的例子,即使在ember数据中被支持/可能,至少不是为了处理关联(例如,你将如何做一个ids数组)?我在客户端模型中创建了一个计算属性,将复合键强制为单个字符串,因此我可以使用 find(...),但是我再也不知道如何使用这个协会。




尝试使用 findAssociation findHasMany



在API版本 9(和一些早期版本,但不是全部?) 11中,我可以实现 DS.Adapter.findAssociation DS.Adapter.findHasMany 方法来检索 hasMany 关联的子记录。这主要是工作,但需要一些体操。这是我广义的 findAssociation findHasMany 方法:

  findHasMany:function(store,record,relationship,ids){

var adapter = this;
var root = this.rootForType(relationship.type);
var query = relationships.options.query(record);

var hits = store.findQuery(relationship.type,query);

hits.on('didLoad',function(){
//注意:这必须发生在回调中,因为findHasMany在
//执行路径记录.get(relationship.key)!!!否则会导致
//无限循环!!!
var arrMany = record.get(relationship.key);

if(hits .get('isLoaded')){
arrMany.loadingRecordsCount = 1 + hits.get('length')+(typeof arrMany.loadingRecordsCount ==number?arrMany.loadingRecordsCount:0);
hits.forEach(function(item,index,enumerable){
arrMany.addToContent(item);
arrMany.loadedRecord();
});
arrMany.loadedRecord() ; //很奇怪,但是这个和上面的1 +确保isLoaded / didLoad触发,即使没有结果。
}
});

}

为了使这项工作,我的 hasMany 定义设置一个查询选项值,它是记录上的函数,该函数返回对请求中的查询字符串的参数的哈希值。因为这是一个ASP.NET WebAPI后端,这可能是一个OData过滤器,例如:

  App.ParentEntity = DS.Model.extend({
...
children:DS.hasMany('App.ChildEntity',{
query:function(record){
return {
$ filter:ChildForeignKey eq'+ record.get('id')+'
};
}
})
});

其中一个技巧是将项目添加到 ManyArray 使用 addToContent(item),以便父记录没有被标记为脏,就像编辑一样。另一个是,当我最初检索父记录的JSON时,我必须为关联名称的键手动设置一个值 true (来自服务器的JSON没有钥匙)。例如:

  var a = Ember.isArray(json)? json:[json]; 
for(var i = 0; i type.eachAssociation(function(key){
var meta = type.metaForProperty(key);
a [i] [key] = true;
});
}

这听起来很棒,但这就是为什么:如果你看看 DS.Store.findMany 并找到 findAssociation findHasMany 被调用,你会发现:

  findMany:function ,ids,record,relationship){
...
if(!Ember.isArray(ids)){
var adapter = this.adapterForType(type);
if(adapter&& adapter.findHasMany){adapter.findHasMany(this,record,relationship,id); }
else {throw fmt(Adapter is null or does not implements`findMany` method,this); }

return this.createManyArray(type,Ember.A());
}

如果从ember-data内部函数中看这行> hasAssociation hasRelationship 其中 findMany 被调用,你会看到第二个参数传递的内容:

  relationship = store.findMany(type,ids || [] ,这个,元); 

所以,获取 findAssociation findHasMany 要调用的是将JSON中的值设置为真实,但不是一个数组 - 我使用 true 。我认为这是一个错误/不完整的,或表明我在错误的轨道 - 如果有人可以告诉我哪个是伟大的。



所有这一切,我可以得到ember数据自动向服务器发出一个请求给子记录,例如到 http://myserver.net/api/child_entity/$filter=ChildForeignKey eq'42',它的工作原理 - 子记录被加载,并且它们与父记录(顺便说一下,反向 belongsTo 关系也正确填充,尽管事实上我没有明确地触及它 - 我不知道在哪里或如何



但是如果我不停在那里,那很快就会崩溃。



处理重新加载父记录



所以说我成功地将子记录加载到父记录中,但是导航到所有父记录的地方被检索(填充菜单)。由于新加载的父记录没有ids的数组,没有任何东西被加载,所以父记录不再有任何孩子被刷新!更糟糕的是, ManyArray isLoaded 属性保留 true !所以我甚至不能观察到重新加载孩子的任何东西。



所以如果我同时有一个屏幕显示孩子的值,它立即就没有孩子记录值。或者如果我导航回到一个,当调用 App.store.find('App.ParentEntity',42)时,记录从商店加载而不需要服务器,当然没有子记录。



这是提示#2,我可能会这样做错了。所以...什么是正确的方法来加载子记录?



非常感谢!

解决方案

根据最新的Ember Data(截至2013年1月25日)...这里我的解决方案是延迟加载有很多关系。我修改了 DS.hasMany 并添加了一个方法到 DS.Adapter



我在 DS.hasMany 中更改了两行:

  DS.hasMany = function(type,options){
Ember.assert(传递给DS.hasMany必须定义的类型,!! type);
return(function(type,options){
options = options || {};
var meta = {type:type,isRelationship:true,options:options,kind:'hasMany' };
返回Ember.computed(function(key,value){
var data = get(this,'data')。hasMany,
store = get(this,'store') ,
ids,relationship;

if(typeof type ==='string'){
type = get(this,type,false)|| get(Ember.lookup ,


meta.key = key;
ids = data [key];
relationship = store.findMany(type,ids,this,
set(relationship,'owner',this);
set(relationship,'name',key);
return relationship;
})property() .meta(meta);
})(type,options);

};

首先,我将添加到对象...

  meta.key = key; 

...其次,如上所述,我从 findMany 通过更改调用...

  relationship = store.findMany(type,id || [],this,meta); 

...至...

  relationship = store.findMany(type,ids,this,meta); 

...允许 ids 被传递 findMany as undefined



接下来,我添加了一个 didFindHasMany 钩子到 DS.Adapter

  DS.Adapter.reopen({

/ **
将响应加载到findHasMany的记录请求

您的适配器应该从`findHasMany`
方法调用此方法,并从后端返回。

@param {DS.Store}存储
@param {DS的子类。模型} type
@param {any} payload
@param {DS.Model的子类}记录关系是成员的记录(父记录)
@param {String}键父记录上关系的属性名称
* /
didFindHasMany:function(store,type,payload,record,key){

var loader = DS.loaderFor store);

loader.populateArray = function(references){
store.loa dHasMany(record,key,references.map(function(reference){return reference.id; }));
};

get(this,'serializer')。extractMany(loader,payload,type);
}

});

我在 DS.Adapter didFindQuery 钩子,使用我发现已经在 DS.Store上实现的 loadHasMany / code>。然后在我的自定义适配器中,我实现了一个 findHasMany 方法,在其成功回调中使用以下代码:

  Ember.run(this,function(){
adapter.didFindHasMany(store,type,response.data,record,key);
});

我没有广泛测试,但似乎工作正常。看看最近对ember-data代码的修改,在我看来,他们已经慢慢地朝着与未来某个时刻支持类似于这种方法的方向发展。或者至少这是我的希望。


(Updated for ember-data API Rev 11...)

TL;DR

What is the right way to use DS.Adapter.findAssociation(...)DS.Adapter.findHasMany(...) to load hasMany associations on demand? Especially, once you load child records, how do you deal with the fact that re-loading the parent record from the server empties the hasMany array? I do not want to (perhaps can't) include the id's of the child records in an array in the parent. Or is there another way to do this that I'm missing?

As a side note, I'm confused about what options should be passed in a hasMany/belongsTo definition for key linkage (and am I supposed to use mappings if I have no sideloaded data or array of ids?), so if you think my problem could lie in my association definition, there's a good chance you're right.

Long version

I am writing my own subclass of DS.RESTAdapter to tie ember-data to an ASP.NET WebAPI backend (using Entity Framework). So far so good, but I'm having a heck of a time getting associations to work right.

Similar to this poster, I noticed that ember-data's front page says used to say that if you have a hasMany association in your model, and you get that property, the store will issue a request for the child records. Quoting from the page:

If you were to request the profile, like this:

author.get('profile');

…the REST adapter would send a request to the URL /profiles?author_id=1.

The implication is this is what happens if you don't sideload and don't include an array of ids. I realize that these docs are somewhat out of date, but I haven't been able to make this happen, either in API version 7 or more recently version 9. However in version 9 I did find the findAssociation method, in version 11 there is the findHasMany method, which I'm guessing is what might have been used to make this happen, and which I'm now trying to use.

Why not include an array of ids or sideload?

Three main reasons I don't want to do this (and possibly can't):

  1. It's not obvious how to do either of these things with ASP.NET WebAPI, at least not with the simple decoration-based approach I'm using. And, I really like the simplicity and thinness of the backend right now, with EF and WebAPI it's almost entirely boilerplate for each entity and I'm done! I even get OData filtering support "free".

  2. My child records will often be generated via expensive queries (aggregates...metrics rollups, for instance). And there are lots of different classes of child entities for a single parent entity. So even getting the ids for all the child types would be expensive, and generating and sideloading all the child records are out of the question.

  3. I have child entities where the primary key is a composite key. I haven't seen an example of this even being supported/possible in ember-data, at least not for dealing with associations (e.g. how would you do an array of ids?). I made a computed property in my client-side model that coerces the composite key into a single string, so I can retrieve a single record from the store using find(...), but again I have no idea how this would even work with an association.

Trying to use findAssociationfindHasMany

I've figured out that in API version 9 (and some earlier versions but not all?) 11, I can perhaps implement the DS.Adapter.findAssociation DS.Adapter.findHasMany method to retrieve the child records of a hasMany association. This mostly works, but requires some gymnastics. Here is my generalized findAssociation findHasMany method:

findHasMany: function (store, record, relationship, ids) {

    var adapter = this;
    var root = this.rootForType(relationship.type);
    var query = relationship.options.query(record);

    var hits = store.findQuery(relationship.type, query);

    hits.on('didLoad', function () {
        // NOTE: This MUST happen in the callback, because findHasMany is in
        // the execution path for record.get(relationship.key)!!! Otherwise causes
        // infinite loop!!!
        var arrMany = record.get(relationship.key);

        if (hits.get('isLoaded')) {
            arrMany.loadingRecordsCount = 1 + hits.get('length') + (typeof arrMany.loadingRecordsCount == "number" ? arrMany.loadingRecordsCount : 0);
            hits.forEach(function (item, index, enumerable) {
                arrMany.addToContent(item);
                arrMany.loadedRecord();
            });
            arrMany.loadedRecord(); // weird, but this and the "1 +" above make sure isLoaded/didLoad fires even if there were zero results.
        }
    });

}

To make this work, my hasMany definitions set a query option value which is a function on the record that returns a hash of parameters for the query string in the request for the children. Since this is for a ASP.NET WebAPI backend, this will probably be an OData filter, e.g.:

App.ParentEntity = DS.Model.extend({
    ...
    children: DS.hasMany('App.ChildEntity', {
        query: function (record) {
            return {
                "$filter": "ChildForeignKey eq '" + record.get('id') + "'"
            };
        }
    })
});

One of the tricks is adding the items to the ManyArray using addToContent(item), so that the parent record doesn't get marked "dirty" as if it has been edited. The other is, when I retrieve the JSON for the parent record initially, I have to manually set a value of true for the association name's key (the JSON coming from the server does not have the key at all). I.e.:

    var a = Ember.isArray(json) ? json : [json];
    for (var i = 0; i < a.length; i++) {
        type.eachAssociation(function (key) {
            var meta = type.metaForProperty(key);
            a[i][key] = true;
        });
    }

This sounds nuts, but this is why: If you look at the implementation of DS.Store.findMany and find where findAssociation findHasMany is called, you'll find:

findMany: function(type, ids, record, relationship){
...
if (!Ember.isArray(ids)) {
  var adapter = this.adapterForType(type);
  if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); }
  else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }

  return this.createManyArray(type, Ember.A());
}

And if you look at this line from the ember-data internal function hasAssociation hasRelationship where findMany is called, you'll see what is passed for the 2nd parameter:

relationship = store.findMany(type, ids || [], this, meta);

So, the only way to get findAssociation findHasMany to be called is to have the value in the JSON be something that is "truthy", but is not an Array--I use true. I'm thinking this is either a bug/incomplete, or an indication that I'm on the wrong track--if someone could tell me which that would be great too.

With all that, I can get ember-data to automatically issue a request to the server for the child records, e.g. to http://myserver.net/api/child_entity/$filter=ChildForeignKey eq '42' and it works--the child records get loaded, and they get associated with the parent record (incidentally, the inverse belongsTo relationship gets populated properly too, despite the fact that I'm not explicitly touching it--I have no idea where or how that is happening).

But that breaks down pretty quickly if I don't stop there.

Dealing with reloading the parent record

So say I successfully load up the child records into a parent record, but then navigate to a place where all the parent records are retrieved (to populate a menu). Since the newly loaded parent records have no array of ids and nothing is sideloaded, the parent record is refreshed without any children again! Worse, the ManyArray's isLoaded property remains true! So I can't even observe anything to re-load the children.

So if I simultaneously have a view onscreen displaying the child values, it immediately zaps to having no child record values. Or if I navigate back to one, when App.store.find('App.ParentEntity', 42) is called, the record is loaded from the store without a request to the server, and of course it has no child records.

This is hint #2 that I probably am going about this the wrong way. So...what is the right way to load child records on demand?

Thanks much!

解决方案

Based on the latest Ember Data (as of Jan 25th 2013)... here's my solution to lazy loading hasMany relationships. I modifed DS.hasMany and added a method to DS.Adapter.

I changed two lines in DS.hasMany:

DS.hasMany = function(type, options) {
  Ember.assert("The type passed to DS.hasMany must be defined", !!type);
  return (function(type, options) {
    options = options || {};
    var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' };
    return Ember.computed(function(key, value) {
      var data = get(this, 'data').hasMany,
          store = get(this, 'store'),
          ids, relationship;

      if (typeof type === 'string') {
        type = get(this, type, false) || get(Ember.lookup, type);
      }

      meta.key = key;
      ids = data[key];
      relationship = store.findMany(type, ids, this, meta);
      set(relationship, 'owner', this);
      set(relationship, 'name', key);
      return relationship;
    }).property().meta(meta);
  })(type, options);

};

First, I added the key to the meta object...

meta.key = key;

...and second, as previously noted above, I removed the empty array from the findMany call by changing...

relationship = store.findMany(type, ids || [], this, meta);

...to...

relationship = store.findMany(type, ids, this, meta);

...allowing ids to be passed in to findMany as undefined.

Next, I added a didFindHasMany hook to DS.Adapter:

DS.Adapter.reopen({

  /**
   Loads the response to a request for records by findHasMany.

   Your adapter should call this method from its `findHasMany`
   method with the response from the backend.

   @param {DS.Store} store
   @param {subclass of DS.Model} type
   @param {any} payload
   @param {subclass of DS.Model} record the record of which the relationship is a member (parent record)
   @param {String} key the property name of the relationship on the parent record
   */
  didFindHasMany: function(store, type, payload, record, key) {

    var loader = DS.loaderFor(store);

    loader.populateArray = function(references) {
      store.loadHasMany(record, key, references.map(function(reference) { return reference.id; }));
    };

    get(this, 'serializer').extractMany(loader, payload, type);
  }

});

I modeled this after the DS.Adapter's didFindQuery hook using the loadHasMany that I found already implemented on the DS.Store. Then in my custom adapter I implemented a findHasMany method that uses the following code in its success callback:

Ember.run(this, function() {
  adapter.didFindHasMany(store, type, response.data, record, key);
});

I haven't tested this extensively but it seems to be working correctly. Looking through recent modifications made to ember-data's code, it seems to me that they have slowly been moving in a direction in which something similar to this approach will be supported at some point in the future. Or at the very least that's my hope.

这篇关于ember-data:正在加载有多个关联按需的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆