Как вернуть обещание, составленное из вложенных моделей в EmberJS, с помощью EmberData?
Enviroment
# Ember : 1.4.0
# Ember Data : 1.0.0-beta.7+canary.b45e23ba
модель
Я упростил свой вариант использования, чтобы сделать этот вопрос более понятным и понятным. Предположим, у нас есть 3 модели: Country
, Region
а также Area
:
Country:
- id: DS.attr('number')
- name: DS.attr('string')
- regions: DS.hasMany('region')
Region:
- id: DS.attr('number')
- name: DS.attr('string')
- country: DS.belongsTo('country')
- areas: DS.hasMany('area')
Area:
- id: DS.attr('number')
- name: DS.attr('string')
- region: DS.belongsTo('region')
Ожидаемые результаты
Хук модели Route должен возвращать массив объектов. Как это:
Примечание: отступы только для того, чтобы сделать пример более читабельным.
Country I
Region A
Area 1
Area 2
Region B
Area 3
Country II
Region C
Area 4
Country III
Region D
Area 5
Текущий подход
App.MyRoute = Ember.Route.extend({
model: function() {
return this.store.find('country').then(function(countries){
// promise all counties
// map resolved countires into an array of promises for owned regions
var regions = countries.map(function(country){
return country.get('regions');
});
// map resolved regions into an array of promises for owned areas
var areas = regions.then(function(regions){
return regions.map(function(region){
return region.get('areas');
});
});
// do not return until ALL promises are resolved
return Ember.RSVP.all(countries, regions, areas).then(function(data){
// here somehow transform the data into expected output format and return it
});
});
}
)};
ошибка
я собираюсь Error while loading route: TypeError: Object [object Array] has no method 'then'
что очевидно происходит из этого кода:
var regions = countries.map(function(country){
return country.get('regions');
});
var areas = regions.then(function(regions){
// regions is not a promise
Однако это должно показать реальную проблему, которую я имею:
Эта проблема
я нуждаюсь countries
решил получить regions
что, в свою очередь, мне нужно, чтобы получить areas
, Я проверял RSVP.hash
а также RSVP.all
функций, читая официальный API и наблюдая за этим разговором, однако мне не удается создать правильный код для цепочки обещаний и в финале then
изменить полученный результат, чтобы он соответствовал моим ожиданиям.
Последние мысли
Мне сказали, что подобная загрузка данных может вызвать много HTTP-запросов, и, вероятно, это будет лучше решено путем дополнительной загрузки, но:
- в данный момент я использую
FixturesAdapter
так что HTTP-запросы не проблема - Я действительно хочу лучше понять RSVP и обещания
Вот почему для меня важно выяснить, как это должно быть сделано правильно.
Редактировать 1: применение изменений, предложенных kingpin2k
Я создал JSBin для моего примера с изменениями, предложенными anwser kingpin2k.
Пока код работает, результаты... неожиданные:
- в
countries
массив я нашел обаcountry
а такжеregion
объекты. Зачем? country
а такжеregion
кажется, что объекты загружены, а области нет (см. результаты журнала консоли в JSBin). Почему?
Edit 2: Объяснение неожиданного поведения от Edit1.
Итак, я наконец заметил, куда я сбился с праведного пути Эмбер. Ответ Kingpin2k был огромным шагом вперед, но он содержит небольшую ошибку:
return this.store.find('country').then(function(countries){
// this does not return an array of regions, but an array of region SETs
var regionPromises = countries.getEach('regions');
// wait for regions to resolve to get the areas
return Ember.RSVP.all(regionPromises).then(function(regions){
// thats why here the variable shouldn't be called "regions"
// but "regionSets" to clearly indicate what it holds
// for this example i'll just reassign it to new var name
var regionSets = regions;
// now compare these two lines (old code commented out)
//var areaPromises = regions.getEach('areas');
var areaPromises = regionSets.getEach('areas');
// since regionSet does not have a property "areas" it
// won't return a promise or ever resolve (it will be undefined)
// the correct approach would be reduceing the array of sets
// an array of regions
var regionsArray = regionSets.reduce(function(sum, val){
// since val is a "Ember.Set" object, we must use it's "toArray()" method
// to get an array of contents and then push it to the resulting array
return sum.pushObjects(val.toArray());
}, []);
// NOW we can get "areas"
var realAreaPromises = regionsArray.getEach('areas');
// and now we can use Ember.RSVP to wait for them to resolve
return Ember.RSVP.all(realAreaPromises).then(function(areaSets){
// note: here again, we don't get an array of areas
// we get an array of area sets - each set for the corresponding region
var results = [];
Итак... теперь я наконец-то правильно определил все объекты (страны, регионы, районы) и могу продолжить свою работу:)
РЕДАКТИРОВАТЬ 3: Рабочее решение в этом JSBin!
2 ответа
Хитрость в том, что вам нужно разрешить определенные обещания, прежде чем вы сможете получить доступ к свойствам этих записей. Ember.RSVP.all
принимает массив обещаний. Ember.RSVP.hash
берет хэш обещаний. К сожалению, вы находитесь в ситуации, когда вы не можете построить свои обещания, пока предыдущие обещания не будут решены (а-а, вы не знаете, какие regions
чтобы добраться до countries
решены, и вы не знаете, какие areas
чтобы добраться до regions
решены). В этом случае у вас действительно есть набор обещаний (хотя и массив обещаний на каждом уровне). Эмбер знает, что нужно дождаться разрешения самого глубокого обещания и использовать это значение в качестве модели.
Теперь нам нужно сделать вид, что regions
а также area
асинхронные, если нет, вы сообщаете Ember Data, что информация будет включена в запрос с country
или в запросе с region
и эти коллекции не будут обещаниями, поэтому код, который я включил ниже, не будет работать.
regions: DS.hasMany('region', {async: true})
areas: DS.hasMany('area', {async: true})
App.IndexRoute = Ember.Route.extend({
controllerName: 'application',
model: function() {
return this.store.find('country').then(function(countries){
// get each country promises
var regionCollectionPromises = countries.getEach('regions');
// wait for regions to resolve to get the areas
return Ember.RSVP.all(regionCollectionPromises).then(function(regionCollections){
var regions = regionCollections.reduce(function(sum, val){
return sum.pushObjects(val.toArray());
}, []);
var areaCollectionPromises = regions.getEach('areas');
//wait on the areas to resolve
return Ember.RSVP.all(areaCollectionPromises).then(function(areaCollections){
// yay, we have countries, regions, and areas resolved
return countries;
});
});
});
}
});
Все это, как говорится, так как кажется, что вы используете Ember Data, я просто вернусь this.store.find('country')
и позволить Ember Data извлекать данные, когда они используются... Этот шаблон будет работать без всего этого кода обещания и заполнится, когда Ember Data выполнит обещания самостоятельно (он запросит данные, как только обнаружит, что вы пытались используйте данные, хорошая ленивая загрузка).
{{#each country in model}}
Country: {{country.name}}
{{#each region in country.regions}}
Region: {{region.name}}
{{#each area in region.areas}}
Area: {{area.name}}
{{/each}}
{{/each}}
{{/each}}
Что вы могли бы сделать вместо этого:
Если вы здесь, вы, вероятно, делаете ту же ошибку, что и я:)
Если вам нужно сложное дерево объектов для отображения вашего маршрута, вы также можете:
- Если вы используете RESTAdapter, вы можете загружать данные в один HTTP-запрос.
- Если вы используете FixturesAdapter (например, на этапе разработки) с фиксированным набором данных, вы можете переключиться на LocalStorageAdapter - например, когда вы запрашиваете модель, она загружает все связанные модели. Так будет проще простого
this.store.find('mymodel', model_id)
Тем не менее, я оставляю оригинальный ответ как помеченный как "принятый", поскольку он фактически отвечает на первоначальный вопрос, и этот ответ - просто примечание для будущих ссылок / других пользователей.