Come utilizzare più modelli con un unico percorso in EmberJS / Ember Data?


85

Dalla lettura dei documenti, sembra che tu debba (o dovresti) assegnare un modello a un percorso in questo modo:

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

Cosa succede se devo utilizzare più oggetti in un determinato percorso? cioè post, commenti e utenti? Come faccio a indicare al percorso di caricarli?

Risposte:


117

Ultimo aggiornamento per sempre : non posso continuare ad aggiornare questo. Quindi questo è deprecato e probabilmente sarà in questo modo. ecco un thread migliore e più aggiornato EmberJS: come caricare più modelli sullo stesso percorso?

Aggiornamento: nella mia risposta originale ho detto di utilizzare embedded: truenella definizione del modello. Non è corretto. Nella revisione 12, Ember-Data prevede che le chiavi esterne siano definite con un suffisso ( collegamento ) _idper un singolo record o _idsper la raccolta. Qualcosa di simile al seguente:

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

Ho aggiornato il violino e lo farò di nuovo se qualcosa cambia o se trovo più problemi con il codice fornito in questa risposta.


Rispondi con i modelli correlati:

Per lo scenario che si sta descrivendo, vorrei contare su associazioni tra i modelli (impostazione embedded: true) e caricare solo il Postmodello quell'itinerario, considerando posso definire DS.hasManyun'associazione per la Commentmodella e DS.belongsToassociazione per la Usersia nella CommentePost modelli. Qualcosa come questo:

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

Questa definizione produrrebbe qualcosa di simile al seguente:

Associazioni tra modelli

Con questa definizione, ogni volta che creo findun post, avrò accesso a una raccolta di commenti associati a quel post, nonché all'autore del commento e all'utente che è l'autore del post, poiché sono tutti incorporati . Il percorso rimane semplice:

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

Quindi in PostRoute(o PostsPostRoutese stai usando resource), i miei modelli avranno accesso a quello del controller content, che è il Postmodello, quindi posso fare riferimento all'autore, semplicemente comeauthor

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

(vedi violino )


Rispondi con modelli non correlati:

Tuttavia, se lo scenario è un po 'più complessa di quello che hai descritto, e / o hanno da usare (o query) diversi modelli per una linea particolare, vi consiglio di farlo in Route#setupController. Per esempio:

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

E ora quando sono nel percorso Post, i miei modelli avranno accesso alla userproprietà nel controller così come è stato impostato in setupControllerhook:

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

(vedi violino )


15
Grazie mille per aver dedicato del tempo a postarlo, l'ho trovato davvero utile.
Anonimo

1
@MilkyWayJoe, post davvero buono! Ora il mio approccio sembra davvero ingenuo :)
intuitivepixel

Il problema con i tuoi modelli non correlati è che non accetta promesse come fa il gancio del modello, giusto? C'è qualche soluzione alternativa per questo?
miguelcobain

1
Se capisco correttamente il tuo problema, puoi adattarlo in modo semplice. Aspetta solo che la promessa si mantenga e solo allora imposta il / i modello / i come variabile del controller
Fabio

A parte l'iterazione e la visualizzazione dei commenti, sarebbe bello se potessi mostrare un esempio di come qualcuno potrebbe aggiungere un nuovo commento post.comments.
Damon Aw

49

L'utilizzo Em.Objectper incapsulare più modelli è un buon modo per ottenere tutti i dati in modelhook. Ma non può garantire che tutti i dati siano preparati dopo il rendering della vista.

Un'altra scelta è usare Em.RSVP.hash. Combina diverse promesse insieme e restituisce una nuova promessa. La nuova promessa se risolta dopo che tutte le promesse sono state risolte. E setupControllernon viene chiamato fino a quando la promessa non viene risolta o rifiutata.

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

In questo modo ottieni tutti i modelli prima di visualizzare il rendering. E l'utilizzo Em.Objectnon ha questo vantaggio.

Un altro vantaggio è che puoi combinare promessa e non promessa. Come questo:

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

Controlla questo per saperne di più su Em.RSVP: https://github.com/tildeio/rsvp.js


Ma non utilizzare Em.Objecto la Em.RSVPsoluzione se il tuo percorso ha segmenti dinamici

Il problema principale è link-to. Se si modifica l'URL facendo clic sul collegamento generato da link-towith models, il modello viene passato direttamente a quella route. In questo caso l' modelhook non viene chiamato e setupControllersi ottiene il modellolink-to ti dà.

Un esempio è questo:

Il codice del percorso:

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

E il link-tocodice, il post è un modello:

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

Le cose diventano interessanti qui. Quando si utilizza url /post/1per visitare la pagina, modelviene chiamato l' hook e setupControllerottiene l'oggetto semplice quando la promessa viene risolta.

Ma se visiti la pagina facendo clic sul link-tolink, passa il postmodello a PostRoutee il percorso ignorerà l' modelhook. In questo caso setupControllerotterrai il postmodello, ovviamente non puoi ottenere l'utente.

Quindi assicurati di non utilizzarli in percorsi con segmenti dinamici.


2
La mia risposta si applica a una versione precedente di Ember & Ember-Data. Questo è un ottimo approccio +1
MilkyWayJoe

11
In realtà c'è. se vuoi passare l'ID del modello invece del modello stesso all'helper link-to, l'hook del modello viene sempre attivato.
darkbaby123

2
Questo dovrebbe essere documentato da qualche parte sulle guide Ember (best practice o qualcosa del genere). È un caso d'uso importante che sono sicuro che molte persone incontrano.
yorbro

1
Se stai usando controller.setProperties(model);, non dimenticare di aggiungere queste proprietà con il valore predefinito al controller. Altrimenti lancerà un'eccezioneCannot delegate set...
Mateusz Nowak

13

Per un po 'stavo usando Em.RSVP.hash, tuttavia il problema che ho riscontrato è stato che non volevo che la mia vista aspettasse il caricamento di tutti i modelli prima del rendering. Tuttavia, ho trovato una soluzione fantastica (ma relativamente sconosciuta) grazie alla gente di Novelys che prevede l'utilizzo di Ember.PromiseProxyMixin :

Supponiamo che tu abbia una vista che ha tre sezioni visive distinte. Ciascuna di queste sezioni dovrebbe essere supportata dal proprio modello. Il modello che supporta il contenuto "splash" nella parte superiore della visualizzazione è piccolo e verrà caricato rapidamente, quindi puoi caricarlo normalmente:

Crea un percorso main-page.js:

import Ember from 'ember';

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

Quindi puoi creare un modello di manubri corrispondente 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>

Quindi diciamo che nel tuo modello vuoi avere sezioni separate sulla tua relazione di amore / odio con il formaggio, ciascuna (per qualche motivo) supportata dal proprio modello. Hai molti record in ogni modello con dettagli dettagliati relativi a ciascun motivo, tuttavia desideri che il contenuto in alto venga visualizzato rapidamente. È qui che {{render}}entra in gioco l' helper. Puoi aggiornare il tuo modello in questo modo:

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

Ora dovrai creare controller e modelli per ciascuno. Dal momento che sono effettivamente identici per questo esempio, ne userò solo uno.

Crea un controller chiamato 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);
        }
    }
});

Noterai che stiamo usando PromiseProxyMixinqui, il che rende il controller consapevole delle promesse. Quando il controller viene inizializzato, indichiamo che la promessa dovrebbe caricare il love-cheesemodello tramite Ember Data. Dovrai impostare questa proprietà sulla proprietà del controller promise.

Ora crea un modello chiamato love-cheese.hbs:

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

Nel tuo modello, sarai in grado di eseguire il rendering di contenuti diversi a seconda dello stato della promessa. Quando la tua pagina viene caricata inizialmente, verrà visualizzata la sezione "Reasons I Love Cheese"Loading... . Quando la promessa viene caricata, renderà tutti i motivi associati per ogni record del tuo modello.

Ogni sezione verrà caricata in modo indipendente e non bloccherà immediatamente il rendering del contenuto principale.

Questo è un esempio semplicistico, ma spero che tutti gli altri lo trovino utile come me.

Se stai cercando di fare qualcosa di simile per molte righe di contenuto, potresti trovare l'esempio di Novelys sopra ancora più pertinente. In caso contrario, quanto sopra dovrebbe funzionare bene per te.


8

Questa potrebbe non essere una buona pratica e un approccio ingenuo, ma mostra concettualmente come faresti per avere più modelli disponibili su un percorso centrale:

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

spero che sia d'aiuto


1
Questo approccio va bene, semplicemente non traendo troppo vantaggio da Ember Data. Per alcuni scenari in cui i modelli non sono correlati, avevo qualcosa di simile a questo.
MilkyWayJoe

4

Grazie a tutte le altre ottime risposte, ho creato un mixin che combina le migliori soluzioni qui in un'interfaccia semplice e riutilizzabile. Esegue un Ember.RSVP.hashin afterModelper i modelli specificati, quindi inietta le proprietà nel controller in setupController. Non interferisce con lo standardmodel hook , quindi lo definiresti comunque come normale.

Esempio di utilizzo:

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

Ecco il 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 va bene per i modelli correlati. Tuttavia, con la versione recente di Ember CLI e Ember Data, esiste un approccio più semplice per i modelli non correlati:

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

Se desideri recuperare la proprietà di un oggetto solo per model2, utilizza DS.PromiseObject invece di 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'))
    });
  }
});

Ho un postpercorso / vista di modifica in cui voglio eseguire il rendering di tutti i tag esistenti nel DB in modo che l'utente possa fare clic per aggiungerli al post in modifica. Voglio definire una variabile che rappresenta un array / raccolta di questi tag. L'approccio che hai usato sopra funzionerebbe per questo?
Joe

Certo, creeresti un PromiseArray(ad esempio "tag"). Quindi, nel tuo modello dovresti passarlo all'elemento di selezione del modulo corrispondente.
AWM

1

Aggiungendo alla risposta di MilkyWayJoe, grazie btw:

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

ritorna

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

l'autore sarebbe

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

Commenti

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

... Credo che i collegamenti a ritroso siano importanti per stabilire il rapporto completo. Spero che aiuti qualcuno.


0

Puoi utilizzare gli hook beforeModelo afterModelpoiché vengono sempre chiamati, anche se modelnon vengono chiamati perché stai utilizzando segmenti dinamici.

Secondo i documenti di instradamento asincrono :

L'hook del modello copre molti casi d'uso per le transizioni pause-on-promise, ma a volte avrai bisogno dell'aiuto degli hook correlati beforeModel e afterModel. Il motivo più comune è che se stai passando a un percorso con un segmento di URL dinamico tramite {{link-to}} o transitTo (al contrario di una transizione causata da una modifica dell'URL), il modello per il percorso in fase di transizione sarà già stato specificato (ad esempio {{# link-to 'article' article}} o this.transitionTo ('article', article)), nel qual caso l'hook del modello non verrà chiamato. In questi casi, dovrai utilizzare l'hook beforeModel o afterModel per ospitare qualsiasi logica mentre il router sta ancora raccogliendo tutti i modelli del percorso per eseguire una transizione.

Quindi supponi di avere una themesproprietà sul tuo SiteController, potresti avere qualcosa del genere:

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
Penso che l'utilizzo thisall'interno della promessa darebbe un messaggio di errore. Puoi impostare var _this = thisprima del ritorno e poi fare _this.set(all'interno del then(metodo per ottenere il risultato desiderato
Duncan Walker
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.