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, что, вероятно, я ошибаюсь. Итак... Каков правильный способ загрузки дочерних записей по запросу?
Спасибо большое!
Ответы
Ответ 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, мне кажется, что они медленно продвигаются в направлении, в котором что-то похожее на этот подход будет поддерживаться в какой-то момент в будущем. Или, по крайней мере, моя надежда.