Wie verwende ich mehrere Modelle mit einer einzigen Route in EmberJS / Ember Data?


85

Nach dem Lesen der Dokumente müssen (oder sollten) Sie einer Route wie folgt ein Modell zuweisen:

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

Was ist, wenn ich mehrere Objekte auf einer bestimmten Route verwenden muss? dh Beiträge, Kommentare und Benutzer? Wie sage ich die Route, um diese zu laden?

Antworten:


117

Letztes Update für immer : Ich kann das nicht ständig aktualisieren. Das ist also veraltet und wird wahrscheinlich so sein. Hier ist ein besserer und aktuellerer Thread. EmberJS: Wie lade ich mehrere Modelle auf derselben Route?

Update: In meiner ursprünglichen Antwort habe ich gesagt, embedded: truein der Modelldefinition zu verwenden. Das ist falsch In Revision 12 erwartet Ember-Data, dass Fremdschlüssel mit einem Suffix ( Link ) _idfür einen einzelnen Datensatz oder _idsfür die Erfassung definiert werden. Ähnliches wie das Folgende:

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

Ich habe die Geige aktualisiert und werde dies erneut tun, wenn sich etwas ändert oder wenn ich weitere Probleme mit dem in dieser Antwort angegebenen Code finde.


Antwort mit verwandten Modellen:

Für das Szenario, das Sie beschreiben, würde ich mich auf Assoziationen zwischen Modellen (Einstellung embedded: true) verlassen und das PostModell nur auf dieser Route laden , da ich eine DS.hasManyAssoziation für das CommentModell und eine DS.belongsToAssoziation für das Usersowohl im Commentals auch im PostModell definieren kann. Etwas wie das:

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

Diese Definition würde ungefähr Folgendes ergeben:

Assoziationen zwischen Modellen

Mit dieser Definition habe ich immer dann, wenn ich findeinen Beitrag verfasse, Zugriff auf eine Sammlung von Kommentaren, die diesem Beitrag zugeordnet sind, sowie auf den Autor des Kommentars und den Benutzer, der der Autor des Beitrags ist, da alle eingebettet sind . Die Route bleibt einfach:

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

In PostRoute(oder PostsPostRoutewenn Sie verwenden resource) haben meine Vorlagen Zugriff auf die Controller content, bei denen es sich um das PostModell handelt, sodass ich mich einfach auf den Autor beziehen kannauthor

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

(siehe Geige )


Antwort mit nicht verwandten Modellen:

Allerdings, wenn Ihr Szenario ein wenig komplexer als das, was Sie beschrieben, und / oder hat zu verwenden (oder Abfrage) verschiedene Modelle für eine bestimmte Route, würde ich empfehlen , es in zu tun Route#setupController. Beispielsweise:

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

Und jetzt, wenn ich mich in der Post-Route befinde, haben meine Vorlagen Zugriff auf die userEigenschaft im Controller, wie sie in setupControllerHook eingerichtet wurde:

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

(siehe Geige )


15
Vielen Dank so viel für die Zeit nehmen , das zu schreiben, fand ich es wirklich nützlich.
Anonym

1
@ MilkyWayJoe, wirklich guter Beitrag! Jetzt sieht mein Ansatz wirklich naiv aus :)
intuitivepixel

Das Problem mit Ihren nicht verwandten Modellen ist, dass sie keine Versprechen annehmen, wie es der Modellhaken tut, oder? Gibt es dafür eine Problemumgehung?
Miguelcobain

1
Wenn ich Ihr Problem richtig verstehe, können Sie es auf einfache Weise anpassen. Warten Sie einfach, bis das Versprechen erfüllt ist, und legen Sie erst dann die Modelle als Variable des Controllers fest
Fabio,

Abgesehen vom Iterieren und Anzeigen von Kommentaren wäre es schön, wenn Sie ein Beispiel dafür zeigen könnten, wie jemand einen neuen Kommentar hinzufügen könnte post.comments.
Damon Aw

49

Die Verwendung Em.Objectder Kapselung mehrerer Modelle ist ein guter Weg, um alle Daten in den modelHaken zu bekommen . Es kann jedoch nicht sichergestellt werden, dass alle Daten nach dem Rendern der Ansicht vorbereitet werden.

Eine andere Wahl ist zu verwenden Em.RSVP.hash. Es kombiniert mehrere Versprechen miteinander und gibt ein neues Versprechen zurück. Das neue Versprechen wird gelöst, nachdem alle Versprechen gelöst wurden. Und setupControllerwird erst aufgerufen, wenn das Versprechen gelöst oder abgelehnt wurde.

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

Auf diese Weise erhalten Sie alle Modelle vor dem Rendern der Ansicht. Und die Verwendung Em.Objecthat diesen Vorteil nicht.

Ein weiterer Vorteil ist, dass Sie Versprechen und Nichtversprechen kombinieren können. So was:

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

Überprüfen Sie dies, um mehr zu erfahren über Em.RSVP: https://github.com/tildeio/rsvp.js


Verwenden Em.Objectoder Em.RSVPlösen Sie jedoch nicht, wenn Ihre Route dynamische Segmente aufweist

Das Hauptproblem ist link-to. Wenn Sie die URL durch Klicken auf einen link-tomit Modellen generierten Link ändern , wird das Modell direkt an diese Route übergeben. In diesem Fall wird der modelHaken nicht aufgerufen und setupControllerSie erhalten das Modell link-to.

Ein Beispiel ist wie folgt:

Der Routencode:

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

Und link-toCode, der Beitrag ist ein Modell:

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

Hier wird es interessant. Wenn Sie /post/1die Seite mit URL besuchen, wird der modelHook aufgerufen und setupControllererhält das einfache Objekt, wenn das Versprechen gelöst wurde.

Wenn Sie die Seite jedoch per Klick- link-toLink besuchen , wird das postModell an übergeben PostRouteund die Route ignoriert den modelHook. In diesem Fall setupControllererhalten Sie das postModell, natürlich können Sie Benutzer nicht erhalten.

Stellen Sie daher sicher, dass Sie sie nicht in Routen mit dynamischen Segmenten verwenden.


2
Meine Antwort gilt für eine frühere Version von Ember & Ember-Data. Dies ist ein wirklich guter Ansatz +1
MilkyWayJoe

11
Eigentlich gibt es. Wenn Sie die Modell-ID anstelle des Modells selbst an den Link-to-Helper übergeben möchten, wird der Modell-Hook immer ausgelöst.
darkbaby123

2
Dies sollte irgendwo in den Ember-Handbüchern dokumentiert werden (Best Practices oder ähnliches). Es ist ein wichtiger Anwendungsfall, dem sicher viele Menschen begegnen.
Yorbro

1
Wenn Sie verwenden controller.setProperties(model);, vergessen Sie nicht, diese Eigenschaften mit dem Standardwert zum Controller hinzuzufügen. Andernfalls wird es eine Ausnahme Cannot delegate set...
auslösen

13

Für eine Weile habe ich verwendet Em.RSVP.hash, aber das Problem, auf das ich gestoßen bin, war, dass ich nicht wollte, dass meine Ansicht wartet, bis alle Modelle geladen sind, bevor ich gerendert habe. Ich habe jedoch dank der Mitarbeiter von Novelys eine großartige (aber relativ unbekannte) Lösung gefunden , bei der Ember.PromiseProxyMixin verwendet wird :

Angenommen, Sie haben eine Ansicht mit drei unterschiedlichen visuellen Abschnitten. Jeder dieser Abschnitte sollte von einem eigenen Modell unterstützt werden. Das Modell, das den "Splash" -Inhalt oben in der Ansicht unterstützt, ist klein und wird schnell geladen, sodass Sie diesen normal laden können:

Erstellen Sie eine Route main-page.js:

import Ember from 'ember';

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

Anschließend können Sie eine entsprechende Lenkervorlage erstellen 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>

Nehmen wir also an, Sie möchten in Ihrer Vorlage separate Abschnitte über Ihre Hassliebe zu Käse haben, die jeweils (aus irgendeinem Grund) von einem eigenen Modell unterstützt werden. Sie haben in jedem Modell viele Datensätze mit ausführlichen Details zu jedem Grund. Sie möchten jedoch, dass der Inhalt oben schnell gerendert wird. Hier kommt der {{render}}Helfer ins Spiel. Sie können Ihre Vorlage folgendermaßen aktualisieren:

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

Jetzt müssen Sie jeweils Controller und Vorlagen erstellen. Da sie für dieses Beispiel praktisch identisch sind, verwende ich nur eine.

Erstellen Sie einen Controller mit dem Namen 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);
        }
    }
});

Sie werden feststellen, dass wir das PromiseProxyMixinhier verwenden, wodurch der Controller vielversprechend wird. Wenn der Controller initialisiert wird, geben wir an, dass das Versprechen das love-cheeseModell über Ember Data laden soll . Sie müssen diese Eigenschaft für die Eigenschaft des Controllers festlegen promise.

Erstellen Sie nun eine Vorlage mit dem Namen love-cheese.hbs:

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

In Ihrer Vorlage können Sie je nach Versprechungsstatus unterschiedliche Inhalte rendern. Wenn Ihre Seite zum ersten Mal geladen wird, wird der Abschnitt "Gründe, warum ich Käse liebe" angezeigt Loading.... Wenn das Versprechen geladen wird, werden alle Gründe für jeden Datensatz Ihres Modells angezeigt.

Jeder Abschnitt wird unabhängig geladen und verhindert nicht, dass der Hauptinhalt sofort gerendert wird.

Dies ist ein vereinfachtes Beispiel, aber ich hoffe, alle anderen finden es genauso nützlich wie ich.

Wenn Sie für viele Inhaltszeilen etwas Ähnliches tun möchten, ist das obige Novelys-Beispiel möglicherweise noch relevanter. Wenn nicht, sollte das oben genannte für Sie gut funktionieren.


8

Dies ist möglicherweise keine bewährte Methode und kein naiver Ansatz, zeigt jedoch konzeptionell, wie Sie mehrere Modelle auf einer zentralen Route verfügbar machen würden:

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

ich hoffe es hilft


1
Dieser Ansatz ist in Ordnung und nutzt Ember Data nur zu wenig. Für einige Szenarien, in denen die Modelle nicht verwandt sind, hatte ich etwas Ähnliches.
MilkyWayJoe

4

Dank all der anderen hervorragenden Antworten habe ich ein Mixin erstellt, das die besten Lösungen hier zu einer einfachen und wiederverwendbaren Oberfläche kombiniert. Es führt ein Ember.RSVP.hashIn afterModelfür die von Ihnen angegebenen Modelle aus und fügt dann die Eigenschaften in den Controller ein setupController. Es stört den Standardhaken nicht model, daher würden Sie dies immer noch als normal definieren.

Anwendungsbeispiel:

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

Hier ist das 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);
  }
});

2

https://stackoverflow.com/a/16466427/2637573 ist für verwandte Modelle in Ordnung. Mit der neuesten Version von Ember CLI und Ember Data gibt es jedoch einen einfacheren Ansatz für nicht verwandte Modelle:

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

Wenn Sie nur die Eigenschaft eines Objekts für abrufen möchtenmodel2 , verwenden Sie DS.PromiseObject anstelle von 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'))
    });
  }
});

Ich habe eine postBearbeitungsroute / -ansicht, in der ich alle vorhandenen Tags in der Datenbank rendern möchte, damit die Verwendung darauf klicken kann, um sie dem zu bearbeitenden Beitrag hinzuzufügen. Ich möchte eine Variable definieren, die ein Array / eine Sammlung dieser Tags darstellt. Würde der oben verwendete Ansatz dafür funktionieren?
Joe

Sicher würden Sie ein PromiseArray(zB "Tags") erstellen . Dann würden Sie es in Ihrer Vorlage an das Auswahlelement des entsprechenden Formulars übergeben.
AWM

1

Zur Antwort von MilkyWayJoe hinzufügen, danke übrigens:

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

kehrt zurück

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

Autor wäre

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

Bemerkungen

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

... Ich glaube, dass die Linkbacks wichtig sind, damit die vollständige Beziehung hergestellt wird. Hoffe das hilft jemandem.


0

Sie können die beforeModeloder afterModelHooks verwenden, da diese immer aufgerufen werden, auch wenn sie modelnicht aufgerufen werden, weil Sie dynamische Segmente verwenden.

Gemäß den asynchronen Routing- Dokumenten:

Der Modell-Hook deckt viele Anwendungsfälle für Pausen-bei-Versprechen-Übergänge ab, aber manchmal benötigen Sie die Hilfe der zugehörigen Hooks vor und nach dem Modell. Der häufigste Grund dafür ist, dass beim Übergang in eine Route mit einem dynamischen URL-Segment über {{link-to}} oder TransitionTo (im Gegensatz zu einem durch eine URL-Änderung verursachten Übergang) das Modell für die Route, die Sie verwenden Der Übergang zu wurde bereits angegeben (z. B. {{# link-to 'article' article}} oder this.transitionTo ('article', article)). In diesem Fall wird der Modell-Hook nicht aufgerufen. In diesen Fällen müssen Sie entweder den beforeModel- oder den afterModel-Hook verwenden, um eine Logik aufzunehmen, während der Router noch alle Routenmodelle sammelt, um einen Übergang durchzuführen.

Angenommen, Sie haben eine themesImmobilie auf Ihrem Grundstück SiteController. Sie könnten so etwas haben:

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
Ich denke, die Verwendung thisinnerhalb des Versprechens würde eine Fehlermeldung geben. Sie könnten var _this = thisvor der Rückkehr einstellen und dann _this.set(innerhalb der then(Methode tun , um das gewünschte Ergebnis zu erhalten
Duncan Walker
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.