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

85

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

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

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

Анонимный
источник

Ответы:

117

Последнее обновление навсегда : я не могу продолжать обновлять это. Так что это устарело и, скорее всего, будет таким. вот лучшая и более современная ветка 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')
});

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

Связи между моделями

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

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>

(см. скрипку )


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

Однако, если ваш сценарий немного сложнее того, что вы описали, и / или вам необходимо использовать (или запрашивать) разные модели для определенного маршрута, я бы рекомендовал сделать это в 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's template
        controller.set('user', App.User.find(window.user_id));
    }
});

И теперь, когда я нахожусь на маршруте Post, мои шаблоны будут иметь доступ к userсвойству в контроллере, как это было настроено в setupControllerхуке:

<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>

(см. скрипку )

MilkyWayJoe
источник
15
Спасибо так много за время , потраченное на пост , который я нашел , что это очень полезно.
Аноним
1
@MilkyWayJoe, действительно хороший пост! Теперь мой подход выглядит действительно наивным :)
intuitivepixel
Проблема с вашими не связанными моделями в том, что они не принимают обещания, как это делает хук модели, верно? Есть ли обходной путь для этого?
miguelcobain
1
Если я правильно понимаю вашу проблему, вы можете легко ее адаптировать. Просто дождитесь выполнения обещания и только затем установите модель (ы) в качестве переменной контроллера
Fabio
Помимо повторения и отображения комментариев, было бы прекрасно, если бы вы могли показать пример того, как кто-то может добавить новый комментарий post.comments.
Дэймон Ав
49

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

Другой вариант - использовать Em.RSVP.hash. Он объединяет несколько обещаний и возвращает новое обещание. Новое обещание, если оно разрешено после выполнения всех обещаний. И 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вызывается ловушка, которая setupControllerполучает простой объект после выполнения обещания.

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

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

темный ребенок123
источник
2
Мой ответ относится к более ранней версии Ember & Ember-Data. Это действительно хороший подход +1
MilkyWayJoe
11
На самом деле есть. если вы хотите передать идентификатор модели вместо самой модели в помощник по ссылке, всегда запускается ловушка модели.
darkbaby123
2
Это должно быть задокументировано где-нибудь в руководствах Ember (лучшие практики или что-то в этом роде). Я уверен, что это важный вариант использования, с которым сталкиваются многие люди.
yorbro
1
Если вы используете controller.setProperties(model);, не забудьте добавить эти свойства со значением по умолчанию в контроллер. В противном случае будет выдано исключениеCannot delegate set...
Матеуш Новак
13

Некоторое время я использовал 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 даже более подходящим. Если нет, то вышеуказанное должно сработать для вас.

РойИнтеллект
источник
8

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

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

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

интуитивно понятный пиксель
источник
1
Такой подход хорош, только не слишком много преимуществ Ember Data. Для некоторых сценариев, где модели не связаны, я бы получил что-то похожее на это.
MilkyWayJoe
4

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

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

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

Вот миксин:

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);
  }
});
Кристофер Сансон
источник
2

https://stackoverflow.com/a/16466427/2637573 подходит для связанных моделей. Однако в последней версии 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'))
    });
  }
});
AWM
источник
У меня есть postмаршрут / представление редактирования, где я хочу отобразить все существующие теги в БД, чтобы пользователь мог щелкнуть, чтобы добавить их в редактируемую запись. Я хочу определить переменную, представляющую массив / коллекцию этих тегов. Подойдет ли для этого подход, который вы использовали выше?
Джо
Конечно, вы бы создали PromiseArray(например, «теги»). Затем в своем шаблоне вы передадите его элементу выбора соответствующей формы.
AWM
1

Добавление к ответу MilkyWayJoe, спасибо, кстати:

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: 'MilkyWayJoe@example.com',
    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,
}

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

philn5d
источник
0

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

Согласно документам асинхронной маршрутизации :

Перехватчик модели охватывает множество вариантов использования переходов «пауза при обещании», но иногда вам понадобится помощь связанных перехватчиков 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'));
}
Райан Д
источник
1
Я думаю, что использование thisвнутри обещания даст сообщение об ошибке. Вы можете установить var _this = thisперед возвратом, а затем выполнить _this.set(внутри then(метода, чтобы получить желаемый результат
Дункан Уокер,