Как использовать несколько моделей с одним маршрутом в EmberJS/Ember Data?

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

App.PostRoute = Ember.Route.extend({
    model: function() {
        return App.Post.find();
    }
});

Что делать, если мне нужно использовать несколько объектов на определенном маршруте? т.е. сообщения, комментарии и пользователи? Как указать маршрут для загрузки?

Ответ 1

Последнее обновление навсегда. Я не могу продолжать обновлять это. Поэтому это устарело и, вероятно, будет таким. здесь лучше, и более свежий поток EmberJS: Как загрузить несколько моделей на одном и том же маршруте?

Обновление: В моем первоначальном ответе я сказал использовать embedded: true в определении модели. Это неверно. В редакции 12 Ember-Data ожидает, что внешние ключи будут определены с суффиксом (ссылка) _id для одиночной записи или _ids для коллекция. Что-то похожее на следующее:

{
    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]
}

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


Отвечайте на связанные модели:

В описываемом сценарии я полагаюсь на ассоциации между моделями (установка embedded: true) и загружать только модель Post в этом маршруте, учитывая, что я могу определить ассоциацию DS.hasMany для модели Comment и DS.belongsTo для User в моделях Comment и Post. Что-то вроде этого:

App.User = DS.Model.extend({
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    email: DS.attr('string'),
    posts: DS.hasMany('App.Post'),
    comments: DS.hasMany('App.Comment')
});

App.Post = DS.Model.extend({
    title: DS.attr('string'),
    body: DS.attr('string'),
    author: DS.belongsTo('App.User'),
    comments: DS.hasMany('App.Comment')
});

App.Comment = DS.Model.extend({
    body: DS.attr('string'),
    post: DS.belongsTo('App.Post'),
    author: DS.belongsTo('App.User')
});

Это определение приведет к следующему:

Associations between models

С этим определением всякий раз, когда я find Post, у меня будет доступ к коллекции комментариев, связанных с этим сообщением, и к автору комментария, а также к пользователю, который является автором сообщения, так как все они встроены. Маршрут остается простым:

App.PostsPostRoute = Em.Route.extend({
    model: function(params) {
        return App.Post.find(params.post_id);
    }
});

Итак, в PostRoute (или PostsPostRoute, если вы используете resource), мои шаблоны будут иметь доступ к контроллеру content, который является моделью Post, поэтому я могу ссылаться на автора, просто как author

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>{{title}}</h3>
    <div>by {{author.fullName}}</div><hr />
    <div>
        {{body}}
    </div>
    {{partial comments}}
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    {{#each content.comments}}
    <hr />
    <span>
        {{this.body}}<br />
        <small>by {{this.author.fullName}}</small>
    </span>
    {{/each}}
</script>

(см. fiddle)


Ответ на не связанные модели:

Однако, если ваш сценарий немного сложнее описанного вами и/или должен использовать (или запрашивать) разные модели для определенного маршрута, я бы рекомендовал сделать это в Route#setupController. Например:

App.PostsPostRoute = Em.Route.extend({
    model: function(params) {
        return App.Post.find(params.post_id);
    },
    // in this sample, "model" is an instance of "Post"
    // coming from the model hook above
    setupController: function(controller, model) {
        controller.set('content', model);
        // the "user_id" parameter can come from a global variable for example
        // or you can implement in another way. This is generally where you
        // setup your controller properties and models, or even other models
        // that can be used in your route template
        controller.set('user', App.User.find(window.user_id));
    }
});

И теперь, когда я нахожусь на пути "Почта", мои шаблоны будут иметь доступ к свойству User в контроллере, поскольку он был настроен в setupController hook:

<script type="text/x-handlebars" data-template-name="posts/post">
    <h3>{{title}}</h3>
    <div>by {{controller.user.fullName}}</div><hr />
    <div>
        {{body}}
    </div>
    {{partial comments}}
</script>

<script type="text/x-handlebars" data-template-name="_comments">
    <h5>Comments</h5>
    {{#each content.comments}}
    <hr />
    <span>
        {{this.body}}<br />
        <small>by {{this.author.fullName}}</small>
    </span>
    {{/each}}
</script>

(см. fiddle)

Ответ 2

Использование Em.Object для инкапсуляции нескольких моделей - хороший способ получить все данные в model hook. Но он не может гарантировать, что все данные будут подготовлены после рендеринга представления.

Другой выбор - использовать Em.RSVP.hash. Он объединяет несколько promises вместе и возвращает новое обещание. Новое обещание, если разрешено после всех promises, будет разрешено. И setupController не вызывается до тех пор, пока обещание не будет разрешено или отклонено.

App.PostRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      post:     // promise to get post
      comments: // promise to get comments,
      user:     // promise to get user
    });
  },

  setupController: function(controller, model) {
    // You can use model.post to get post, etc
    // Since the model is a plain object you can just use setProperties
    controller.setProperties(model);
  }
});

Таким образом вы получаете все модели перед рендерингом рендеринга. И использование Em.Object не имеет этого преимущества.

Еще одно преимущество заключается в том, что вы можете комбинировать обещания и обещания. Вот так:

Em.RSVP.hash({
  post: // non-promise object
  user: // promise object
});

Проверьте это, чтобы узнать больше о Em.RSVP: https://github.com/tildeio/rsvp.js


Но не используйте Em.Object или Em.RSVP решение, если ваш маршрут имеет динамические сегменты

Основная проблема - link-to. Если вы измените URL-адрес по ссылке, сгенерированной с помощью link-to с моделями, модель передается непосредственно на этот маршрут. В этом случае крюк model не вызывается, а в setupController вы получаете модель link-to.

Пример такой:

Код маршрута:

App.Router.map(function() {
  this.route('/post/:post_id');
});

App.PostRoute = Em.Route.extend({
  model: function(params) {
    return Em.RSVP.hash({
      post: App.Post.find(params.post_id),
      user: // use whatever to get user object
    });
  },

  setupController: function(controller, model) {
    // Guess what the model is in this case?
    console.log(model);
  }
});

И link-to код, сообщение является моделью:

{{#link-to "post" post}}Some post{{/link-to}}

Здесь интересны. Когда вы используете url /post/1 для посещения страницы, вызывается model hook, а setupController получает простой объект, когда обещание разрешено.

Но если вы заходите на страницу по ссылке link-to, она передает модель post на PostRoute, и маршрут игнорирует крюк model. В этом случае setupController получит модель post, конечно, вы не сможете получить пользователя.

Поэтому убедитесь, что вы не используете их в маршрутах с динамическими сегментами.

Ответ 3

Некоторое время я использовал Em.RSVP.hash, однако проблема, с которой я столкнулся, заключалась в том, что я не хотел, чтобы мое представление дождалось загрузки всех моделей до рендеринга. Тем не менее, я нашел отличное (но относительно неизвестное) решение благодаря людям в Novelys, что предполагает использование Ember.PromiseProxyMixin:

Скажем, у вас есть представление, которое имеет три разных визуальных раздела. Каждый из этих разделов должен подкрепляться собственной моделью. Модель, поддерживающая содержимое "всплеска" в верхней части представления, мала и будет загружаться быстро, поэтому вы можете загрузить ее обычно:

Создайте маршрут main-page.js:

import Ember from 'ember';

export default Ember.Route.extend({
    model: function() {
        return this.store.find('main-stuff');
    }
});

Затем вы можете создать соответствующий шаблон Handlebars main-page.hbs:

<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
    <li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
</section>

Итак, скажем, в вашем шаблоне вы хотите иметь отдельные разделы о своих отношениях любви/ненависти с сыром, каждый из которых (по какой-то причине) подкреплен собственной моделью. У вас есть много записей в каждой модели с подробными сведениями по каждой причине, однако вы хотите, чтобы контент сверху отображался быстро. Здесь находится помощник {{render}}. Вы можете обновить свой шаблон так:

<h1>My awesome page!</h1>
<ul>
{{#each thing in model}}
    <li>{{thing.name}} is really cool.</li>
{{/each}}
</ul>
<section>
    <h1>Reasons I Love Cheese</h1>
    {{render 'love-cheese'}}
</section>
<section>
    <h1>Reasons I Hate Cheese</h1>
    {{render 'hate-cheese'}}
</section>

Теперь вам нужно создать контроллеры и шаблоны для каждого. Так как они в действительности одинаковы для этого примера, я просто использую его.

Создайте контроллер с именем love-cheese.js:

import Ember from 'ember';

export default Ember.ObjectController.extend(Ember.PromiseProxyMixin, {
    init: function() {
        this._super();
        var promise = this.store.find('love-cheese');
        if (promise) {
            return this.set('promise', promise);
        }
    }
});

Вы заметите, что мы используем PromiseProxyMixin здесь, что делает контроллер обещающим. Когда контроллер инициализируется, мы указываем, что обещание должно загружать модель love-cheese через Ember Data. Вам нужно будет установить это свойство в свойстве контроллера promise.

Теперь создайте шаблон с именем love-cheese.hbs:

{{#if isPending}}
  <p>Loading...</p>
{{else}}
  {{#each item in promise._result }}
    <p>{{item.reason}}</p>
  {{/each}}
{{/if}}

В вашем шаблоне вы сможете отображать разные материалы в зависимости от состояния обещания. Когда ваша страница изначально загружается, в разделе "Причины, по которой я люблю сыр" будет отображаться Loading.... Когда обещание будет загружено, оно отобразит все причины, связанные с каждой записью вашей модели.

Каждая секция будет загружаться независимо и не будет немедленно блокировать основной контент от рендеринга.

Это упрощенный пример, но я надеюсь, что все остальные считают его полезным, как и я.

Если вы хотите сделать что-то подобное для многих строк контента, вы можете найти пример Novelys выше даже более актуальным. Если нет, вышеприведенное должно работать нормально для вас.

Ответ 4

Это может быть не самая лучшая практика и наивный подход, но она концептуально показывает, как вы могли бы использовать несколько моделей на одном центральном маршруте:

App.PostRoute = Ember.Route.extend({
  model: function() {
    var multimodel = Ember.Object.create(
      {
        posts: App.Post.find(),
        comments: App.Comments.find(),
        whatever: App.WhatEver.find()
      });
    return multiModel;
  },
  setupController: function(controller, model) {
    // now you have here model.posts, model.comments, etc.
    // as promises, so you can do stuff like
    controller.set('contentA', model.posts);
    controller.set('contentB', model.comments);
    // or ...
    this.controllerFor('whatEver').set('content', model.whatever);
  }
});

надеюсь, что это поможет

Ответ 5

Благодаря всем остальным отличным ответам, я создал mixin, который объединяет лучшие решения здесь в простой и многоразовый интерфейс. Он выполняет Ember.RSVP.hash в afterModel для указанных вами моделей, затем вводит свойства в контроллер в setupController. Он не мешает стандарту model hook, поэтому вы все равно определяете это как обычно.

Пример использования:

App.PostRoute = Ember.Route.extend(App.AdditionalRouteModelsMixin, {

  // define your model hook normally
  model: function(params) {
    return this.store.find('post', params.post_id);
  },

  // now define your other models as a hash of property names to inject onto the controller
  additionalModels: function() {
    return {
      users: this.store.find('user'), 
      comments: this.store.find('comment')
    }
  }
});

Вот mixin:

App.AdditionalRouteModelsMixin = Ember.Mixin.create({

  // the main hook: override to return a hash of models to set on the controller
  additionalModels: function(model, transition, queryParams) {},

  // returns a promise that will resolve once all additional models have resolved
  initializeAdditionalModels: function(model, transition, queryParams) {
    var models, promise;
    models = this.additionalModels(model, transition, queryParams);
    if (models) {
      promise = Ember.RSVP.hash(models);
      this.set('_additionalModelsPromise', promise);
      return promise;
    }
  },

  // copies the resolved properties onto the controller
  setupControllerAdditionalModels: function(controller) {
    var modelsPromise;
    modelsPromise = this.get('_additionalModelsPromise');
    if (modelsPromise) {
      modelsPromise.then(function(hash) {
        controller.setProperties(hash);
      });
    }
  },

  // hook to resolve the additional models -- blocks until resolved
  afterModel: function(model, transition, queryParams) {
    return this.initializeAdditionalModels(model, transition, queryParams);
  },

  // hook to copy the models onto the controller
  setupController: function(controller, model) {
    this._super(controller, model);
    this.setupControllerAdditionalModels(controller);
  }
});

Ответ 6

fooobar.com/questions/67030/... отлично подходит для связанных моделей. Однако с недавней версией Ember CLI и Ember Data существует более простой подход к несвязанным моделям:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller,model);
    var model2 = DS.PromiseArray.create({
      promise: this.store.find('model2')
    });
    model2.then(function() {
      controller.set('model2', model2)
    });
  }
});

Если вы хотите получить свойство объекта для model2, используйте DS.PromiseObject вместо DS.PromiseArray:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller,model);
    var model2 = DS.PromiseObject.create({
      promise: this.store.find('model2')
    });
    model2.then(function() {
      controller.set('model2', model2.get('value'))
    });
  }
});

Ответ 7

Добавляя к MilkyWayJoe ответ, спасибо btw:

this.store.find('post',1) 

возвращает

{
    id: 1,
    title: 'string',
    body: 'string string string string...',
    author_id: 1,
    comment_ids: [1, 2, 3, 6],
    tag_ids: [3,4]
};

автор будет

{
    id: 1,
    firstName: 'Joe',
    lastName: 'Way',
    email: '[email protected]',
    points: 6181,
    post_ids: [1,2,3,...,n],
    comment_ids: [1,2,3,...,n],
}

комментарии

{
    id:1,
    author_id:1,
    body:'some words and stuff...',
    post_id:1,
}

... Я считаю, что обратные ссылки важны, чтобы установить полную связь. Надежда, которая помогает кому-то.

Ответ 8

Вы можете использовать крючки beforeModel или afterModel, поскольку они всегда вызываются, даже если model не вызывается, потому что вы используете динамические сегменты.

В соответствии с асинхронная маршрутизация docs:

Крючок модели охватывает множество вариантов использования для переходов с паузой на обещание, но иногда вам понадобится помощь связанных крючков beforeModel и afterModel. Наиболее распространенная причина этого заключается в том, что если вы переходите на маршрут с динамическим сегментом URL через {{link-to}} или transitionTo (в отличие от перехода, вызванного изменением URL-адреса), модель для маршрута вы (например, {{link-to 'article' article}} или this.transitionTo('article', article), и в этом случае крюк модели не будет вызван. В этих случаях вам нужно использовать либо метод beforeModel, либо afterModel для размещения любой логики, пока маршрутизатор все еще собирает все модели маршрутов для выполнения перехода.

Итак, скажите, что у вас есть свойство themes на вашем SiteController, вы можете иметь что-то вроде этого:

themes: null,
afterModel: function(site, transition) {
  return this.store.find('themes').then(function(result) {
    this.set('themes', result.content);
  }.bind(this));
},
setupController: function(controller, model) {
  controller.set('model', model);
  controller.set('themes', this.get('themes'));
}