Как вернуть обещание, составленное из вложенных моделей в 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)

Тем не менее, я оставляю оригинальный ответ как помеченный как "принятый", поскольку он фактически отвечает на первоначальный вопрос, и этот ответ - просто примечание для будущих ссылок / других пользователей.

Другие вопросы по тегам