Подтвердить что ты не робот

Ember-data: загрузка hasMany по запросу

(Обновлено для API-интерфейсов Ember-Data Rev 11...)

TL; DR

Каков правильный способ использования DS.Adapter.findAssociation(...) DS.Adapter.findHasMany(...) для загрузки ассоциаций hasMany по запросу? Особенно, когда вы загружаете дочерние записи, как вы справляетесь с тем, что перезагрузка родительской записи с сервера опустошает массив hasMany? Я не хочу (возможно, не могу) включать id дочерних записей в массив в родительском. Или есть другой способ сделать это, чтобы я отсутствовал?

В качестве побочного примечания я смущен тем, какие параметры должны быть переданы в определении hasMany/belongsTo для привязки ключей (и я должен использовать сопоставления, если у меня нет данных с боковой загрузкой или массива идентификаторов?), поэтому, если вы считаете, что моя проблема может заключаться в определении моей ассоциации, есть хороший шанс, что вы правы.

Длинная версия

Я пишу свой собственный подкласс DS.RESTAdapter для привязки данных ember к бэкэнду ASP.NET WebAPI (используя Entity Framework). До сих пор так хорошо, но у меня есть время, чтобы ассоциации работали правильно.

Подобно этому плакату, я заметил, что на передней панели говорится, что если у вас есть ассоциация hasMany в вашей модели, и вы get это свойство, магазин выдает запрос для дочерних записей. Цитата со страницы:

Если вы хотите запросить профиль, выполните следующие действия:

author.get('profile');

... адаптер REST отправит запрос URL/профили? author_id = 1.

Импликация - это то, что происходит, если вы не босиком и не включаете массив идентификаторов. Я понимаю, что эти документы несколько устарели, но я не смог это сделать, либо в версии API версии 7, либо в более поздней версии 9. Однако в версии 9 я нашел метод findAssociation в версии 11 существует метод findHasMany, который, как я предполагаю, может быть использован для этого, и который я сейчас пытаюсь использовать.

Почему бы не включить массив идентификаторов или боковой нагрузки?

Три основные причины, по которым я не хочу этого делать (и, возможно, не могу):

  • Не очевидно, как сделать любую из этих вещей с помощью ASP.NET WebAPI, по крайней мере, не с помощью простого подхода, основанного на декорировании, который я использую. И мне очень нравится простота и тонкость бэкэнда прямо сейчас, когда EF и WebAPI почти полностью настраиваются для каждого объекта, и я закончил! Я даже получаю поддержку фильтрации OData "бесплатно".

  • Мои дочерние записи часто генерируются с помощью дорогостоящих запросов (например, сводки агрегатов... показателей). И существует множество разных классов дочерних объектов для одного родительского объекта. Таким образом, даже получение идентификаторов для всех дочерних типов будет дорогостоящим, и вопрос о создании и загрузке всех дочерних записей не может быть и речи.

  • У меня есть дочерние объекты, где первичный ключ является составным. Я не видел пример того, что это даже поддерживается/возможно в данных ember-данных, по крайней мере, не для работы с ассоциациями (например, как бы вы сделали массив идентификаторов?). Я сделал вычисляемое свойство в моей клиентской модели, которая объединяет составной ключ в одну строку, поэтому я могу получить одну запись из хранилища с помощью 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 = 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.
        }
    });

}

Чтобы сделать эту работу, мои определения hasMany устанавливают значение параметра query, которое является функцией в записи, которая возвращает хэш параметров строки запроса в запросе для детей. Поскольку это для бэкэнд ASPAP 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 < a.length; i++) {
        type.eachAssociation(function (key) {
            var meta = type.metaForProperty(key);
            a[i][key] = true;
        });
    }

Это звучит глупо, но вот почему: если вы посмотрите на реализацию DS.Store.findMany и найдите, где вызывается findAssociation findHasMany, вы найдете:

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

И если вы посмотрите на эту строку из внутренней функции ember-data hasAssociation hasRelationship, где вызывается findMany, вы увидите, что передается для второго параметра:

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

Таким образом, единственный способ получить findAssociation findHasMany - это присвоить значение в JSON что-то, что является "правдивым", но не является массивом - я используйте true. Я думаю, что это либо ошибка/неполная, либо указание на то, что я ошибаюсь - если кто-то может сказать мне, что это тоже здорово.

При этом я могу получить данные ember-data, чтобы автоматически выдавать запрос на сервер для дочерних записей, например. до http://myserver.net/api/child_entity/$filter=ChildForeignKey eq '42', и он работает - дочерние записи загружаются, и они связаны с родительской записью (кстати, обратная связь belongsTo также заполняется должным образом, несмотря на то, что я явно не прикасаюсь к ней) Я понятия не имею, где и как это происходит).

Но это довольно быстро ломается, если я не останавливаюсь там.

Работа с перезагрузкой родительской записи

Итак, скажите, что я успешно загрузил дочерние записи в родительскую запись, но затем перейдите к месту, где извлекаются все родительские записи (чтобы заполнить меню). Поскольку у недавно загруженных родительских записей нет массива идентификаторов и ничего не загружено, родительская запись обновляется без каких-либо детей снова! Хуже того, свойство ManyArray isLoaded остается true! Поэтому я не могу ничего наблюдать за повторной загрузкой детей.

Итак, если у меня одновременно есть вид на экране, отображающий дочерние значения, он сразу же не имеет никаких дочерних записей. Или, если я вернусь к одному, когда вызывается App.store.find('App.ParentEntity', 42), запись загружается из хранилища без запроса на сервер и, конечно же, у него нет дочерних записей.

Это намек №2, что, вероятно, я ошибаюсь. Итак... Каков правильный способ загрузки дочерних записей по запросу?

Спасибо большое!

4b9b3361

Ответ 1

Основываясь на последних данных Ember (по состоянию на 25 января 2013 года)... здесь мое решение для ленивой загрузки имеет много отношений. Я модифицировал DS.hasMany и добавил метод DS.Adapter.

Я изменил две строки в 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);

};

Сначала я добавил объект key в объект meta...

meta.key = key;

... и, во-вторых, как ранее отмечалось выше, я удалил пустой массив из вызова findMany, изменив...

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

... до...

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

... разрешающая ids передать в findMany как undefined.

Затем я добавил привязку didFindHasMany к 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);
  }

});

Я смоделировал это после крюка DS.Adapter didFindQuery, используя loadHasMany, который я нашел уже реализованным в DS.Store. Затем в моем пользовательском адаптере я реализовал метод findHasMany, который использует следующий код в своем обратном вызове:

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

Я не тестировал это широко, но, похоже, работает правильно. Просматривая последние изменения, внесенные в код данных ember-data, мне кажется, что они медленно продвигаются в направлении, в котором что-то похожее на этот подход будет поддерживаться в какой-то момент в будущем. Или, по крайней мере, моя надежда.