Dati braci: un tutorial completo per la libreria dei dati braci

Pubblicato: 2022-03-11

Ember Data (noto anche come ember-data o ember.data) è una libreria per la gestione solida dei dati del modello nelle applicazioni Ember.js. Gli sviluppatori di Ember Data affermano che è progettato per essere agnostico rispetto al meccanismo di persistenza sottostante, quindi funziona altrettanto bene con le API JSON su HTTP come con i WebSocket in streaming o lo storage locale di IndexedDB. Fornisce molte delle funzionalità che potresti trovare nelle mappature relazionali degli oggetti lato server (ORM) come ActiveRecord, ma è progettato specificamente per l'ambiente unico di JavaScript nel browser.

Anche se Ember Data potrebbe richiedere del tempo per crescere, una volta che l'hai fatto, probabilmente scoprirai che è valsa la pena investire. In definitiva, renderà molto più semplice lo sviluppo, il miglioramento e la manutenzione del sistema.

Quando un'API viene rappresentata utilizzando modelli, adattatori e serializzatori Ember Data, ogni associazione diventa semplicemente un nome di campo. Questo incapsula i dettagli interni di ogni associazione, isolando così il resto del codice dalle modifiche alle associazioni stesse. Al resto del codice non importerà, ad esempio, se una particolare associazione è polimorfica o è il risultato di una mappa di molte associazioni.

Inoltre, la tua base di codice è in gran parte isolata dalle modifiche di back-end, anche se significative, poiché tutto ciò che la tua base di codice si aspetta sono campi e funzioni sui modelli, non una rappresentazione JSON o XML o YAML del modello.

In questo tutorial, introdurremo le caratteristiche più salienti di Ember Data e dimostreremo come aiuta a ridurre al minimo l'abbandono del codice, concentrandoci su un esempio del mondo reale.

Viene fornita anche un'appendice che discute una serie di argomenti ed esempi più avanzati di Ember Data.

Nota: questo articolo presuppone una certa familiarità di base con Ember.js. Se non hai familiarità con Ember.js, dai un'occhiata al nostro popolare tutorial Ember.js per un'introduzione. Abbiamo anche una guida JavaScript completa disponibile in russo, portoghese e spagnolo.

La proposta di valore di Ember Data

Un esempio di come la libreria Ember Data può aiutare a soddisfare le esigenze dei clienti.

Iniziamo considerando un semplice esempio.

Supponiamo di avere una base di codice funzionante per un sistema di blog di base. Il sistema contiene post e tag, che hanno una relazione molti-a-molti tra loro.

Tutto va bene fino a quando non avremo un requisito per supportare Pages. Il requisito afferma anche che, poiché è possibile taggare una Pagina in WordPress, dovremmo essere in grado di farlo anche noi.

Quindi ora, i tag non si applicheranno più solo ai post, ma potrebbero applicarsi anche alle pagine. Di conseguenza, la nostra semplice associazione tra tag e post non sarà più adeguata. Invece, avremo bisogno di una relazione polimorfica molti-a-molti unilaterale, come la seguente:

  • Ogni Post è un Taggable e ha molti Tag
  • Ogni Pagina è un Taggable e ha molti Tag
  • Ogni Tag ha molti Taggable polimorfici

È probabile che il passaggio a questo nuovo, più complesso, insieme di associazioni abbia ramificazioni significative in tutto il nostro codice, con conseguente abbandono. Poiché non abbiamo idea di come serializzare un'associazione polimorfica in JSON, probabilmente creeremo semplicemente più endpoint API come GET /posts/:id/tags e GET /pages/:id/tags . E poi, elimineremo tutte le nostre funzioni di parser JSON esistenti e ne scriveremo di nuove per le nuove risorse aggiunte. Uffa. Noioso e doloroso.

Ora consideriamo come ci avvicineremmo a questo utilizzando Ember Data.

In Ember Data, accogliere questo insieme modificato di associazioni comporterebbe semplicemente il passaggio da:

 App.Post = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Tag = DS.Model.extend({ post: DS.belongsTo('post', {async: true}) });

a:

 App.PostType = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Post = App.PostType.extend({ }); App.Page = App.PostType.extend({ }); App.Tag = DS.Model.extend({ posts: DS.hasMany('postType', {polymorphic: true, async: true}) });

L'abbandono risultante nel resto del nostro codice sarebbe minimo e saremmo in grado di riutilizzare la maggior parte dei nostri modelli. Si noti in particolare che il nome dell'associazione dei tags su Post rimane invariato. Inoltre, il resto della nostra base di codice si basa solo sull'esistenza dell'associazione dei tags ed è ignaro dei suoi dettagli.

Un primer per i dati sulla brace

Prima di immergerci in un esempio del mondo reale, esaminiamo alcuni fondamenti di Ember Data.

Percorsi e Modelli

In Ember.js, il router è responsabile della visualizzazione dei modelli, del caricamento dei dati e della configurazione dello stato dell'applicazione. Il router abbina l'URL corrente ai percorsi che hai definito, quindi un percorso è responsabile di specificare il modello che un modello deve visualizzare (Ember si aspetta che questo modello sia una sottoclasse di Ember.Object ):

 App.ItemsRoute = Ember.Route.extend({ model: function(){ // GET /items // Retrieves all items. return this.modelFor('orders.show').get('items'); } });

Ember Data fornisce DS.Model che è una sottoclasse di Ember.Object e aggiunge funzionalità come il salvataggio o l'aggiornamento di un singolo record o più record per comodità.

Per creare un nuovo modello, creiamo una sottoclasse di DS.Model (ad esempio, App.User = DS.Model.extend({}) ).

Ember Data si aspetta una struttura JSON ben definita e intuitiva dal server e serializza i record appena creati sullo stesso JSON strutturato.

Ember Data fornisce anche una suite di classi di array come DS.RecordArray per lavorare con i modelli. Questi hanno responsabilità come la gestione delle relazioni uno-a-molti o molti-a-molti, la gestione del recupero asincrono dei dati e così via.

Attributi del modello

Gli attributi di base del modello sono definiti utilizzando DS.attr ; per esempio:

 App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string') });

Solo i campi creati da DS.attr verranno inclusi nel payload passato al server per la creazione o l'aggiornamento dei record.

DS.attr accetta quattro tipi di dati: string , number , boolean e date .

RESTSerializzatore

Per impostazione predefinita:

  • Ember Data utilizza RESTSerializer per la creazione di oggetti dalle risposte API (deserializzazione) e per la generazione di JSON per le richieste API (serializzazione).
  • RESTSerializer prevede che i campi creati da DS.belongsTo abbiano un campo denominato user incluso nella risposta JSON dal server. Quel campo contiene l'id del record di riferimento.
  • RESTSerializer aggiunge un campo user al payload passato all'API con l'ID dell'ordine associato.

Una risposta potrebbe, ad esempio, essere simile a questa:

 GET http://api.example.com/orders?ids[]=19&ids[]=28 { "orders": [ { "id": "19", "createdAt": "1401492647008", "user": "1" }, { "id": "28", "createdAt": "1401492647008", "user": "1" } ] }

E una richiesta HTTP creata da RESTSerializer per il salvataggio di un ordine potrebbe assomigliare a questa:

 POST http://api.example.com/orders { "order": { "createdAt": "1401492647008", "user": "1" } }

Relazioni uno a uno

Supponiamo, ad esempio, che ogni Utente abbia un Profilo unico. Possiamo rappresentare questa relazione in Ember Data utilizzando DS.belongsTo sia sull'utente che sul profilo:

 App.User = DS.Model.extend({ profile: DS.belongsTo('profile', {async: true}) }); App.Profile = DS.Model.extend({ user: DS.belongsTo('user', {async: true}) });

Possiamo quindi ottenere l'associazione con user.get('profile') o impostarla con user.set('profile', aProfile) .

RESTSerializer prevede che l'ID del modello associato venga fornito per ciascun modello; per esempio:

 GET /users { "users": [ { "id": "14", "profile": "1" /* ID of profile associated with this user */ } ] } GET /profiles { "profiles": [ { "id": "1", "user": "14" /* ID of user associated with this profile */ } ] }

Allo stesso modo, include l'ID del modello associato in un payload della richiesta:

 POST /profiles { "profile": { "user": "17" /* ID of user associated with this profile */ } }

Relazioni uno-a-molti e molti-a-uno

Supponiamo di avere un modello in cui un post ha molti commenti. In Ember Data, possiamo rappresentare questa relazione con DS.hasMany('comment', {async: true}) su Post e DS.belongsTo('post', {async: true}) su Comment:

 App.Post = DS.Model.extend({ content: DS.attr('string'), comments: DS.hasMany('comment', {async: true}) }); App.Comment = DS.Model.extend({ message: DS.attr('string'), post: DS.belongsTo('post', {async: true}) });

Possiamo quindi ottenere elementi associati con post.get('comments', {async: true}) e aggiungere una nuova associazione con post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

Il server risponderà quindi con una serie di ID per i commenti corrispondenti su un post:

 GET /posts { "posts": [ { "id": "12", "content": "", "comments": ["56", "58"] } ] }

… e con un ID per ogni Commento:

 GET /comments?ids[]=56&ids[]=58 { "comments": [ { "id": "56", "message": "", "post": "12" }, { "id": "58", "message": "", "post": "12" } ] }

RESTSerializer aggiunge l'id del Post associato al Commento:

 POST /comments { "comment": { "message": "", "post": "12" /* ID of post associated with this comment */ } }

Si noti tuttavia che, per impostazione predefinita, RESSTSerializer non aggiungerà gli ID associati DS.hasMany agli oggetti che serializza, poiché tali associazioni sono specificate sul lato "molti" (cioè quelle che hanno un'associazione DS.belongsTo ). Quindi, nel nostro esempio, sebbene un Post abbia molti commenti, tali ID non verranno aggiunti all'oggetto Post:

 POST /posts { "post": { "content": "" /* no associated post IDs added here */ } }

Per "forzare" la serializzazione anche degli ID DS.hasMany , puoi utilizzare il Mixin dei record incorporati.

Molti-a-molti relazioni

Supponiamo che nel nostro modello un Autore possa avere più Post e un Post possa avere più Autori.

Per rappresentare questa relazione in Ember Data, possiamo utilizzare DS.hasMany('author', {async: true}) su Post e DS.hasMany('post', {async: true}) su Author:

 App.Author = DS.Model.extend({ name: DS.attr('string'), posts: DS.hasMany('post', {async: true}) }); App.Post = DS.Model.extend({ content: DS.attr('string'), authors: DS.hasMany('author', {async: true}) });

Possiamo quindi ottenere elementi associati con author.get('posts') e aggiungere una nuova associazione con author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

Il server risponderà quindi con una matrice di ID per gli oggetti corrispondenti; per esempio:

 GET /authors { "authors": [ { "id": "1", "name": "", "posts": ["12"] /* IDs of posts associated with this author */ } ] } GET /posts { "posts": [ { "id": "12", "content": "", "authors": ["1"] /* IDs of authors associated with this post */ } ] }

Poiché si tratta di una relazione molti-a-molti, RESTSerializer aggiunge una matrice di ID di oggetti associati; per esempio:

 POST /posts { "post": { "content": "", "authors": ["1", "4"] /* IDs of authors associated with this post */ } }

Un esempio reale: migliorare un sistema di ordinazione esistente

Nel nostro sistema di ordinazione esistente, ogni Utente ha molti Ordini e ogni Ordine ha molti Articoli. Il nostro sistema ha più fornitori (ovvero, fornitori) dai quali è possibile ordinare i prodotti, ma ogni ordine può contenere solo articoli di un unico fornitore.

Nuovo requisito n. 1: abilitare un singolo ordine per includere articoli da più fornitori.

Nel sistema esistente, esiste una relazione uno-a-molti tra Fornitori e Ordini. Tuttavia, una volta esteso un ordine per includere articoli da più fornitori, questa semplice relazione non sarà più adeguata.

In particolare, se un fornitore è associato a un intero ordine, nel sistema avanzato quell'ordine può benissimo includere anche articoli ordinati da altri fornitori. Deve esserci quindi un modo per indicare quale parte di ciascun ordine è rilevante per ciascun fornitore. Inoltre, quando un fornitore accede ai propri ordini, dovrebbe avere visibilità solo sugli articoli ordinati da lui, non su altri articoli che il cliente potrebbe aver ordinato da altri fornitori.

Un approccio potrebbe essere quello di introdurre due nuove associazioni molti-a-molti; uno tra Ordine e Articolo e un altro tra Ordine e Fornitore.

Tuttavia, per semplificare le cose, introduciamo un nuovo costrutto nel modello di dati che chiamiamo "ProviderOrder".

Redazione di relazioni

Il modello di dati avanzato dovrà accogliere le seguenti associazioni:

  • Relazione uno-a-molti tra Utenti e Ordini (ogni Utente può essere associato a da 0 a n Ordini) e una relazione Uno-a-molti tra Utenti e Fornitori (ogni Utente può essere associato a da 0 a n Provider)

     App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), isAdmin: DS.attr('boolean'), orders: DS.hasMany('order', {async: true}), providers: DS.hasMany('provider', {async: true}) });
  • Relazione uno-a-molti tra Orders e ProviderOrders (ogni Ordine è composto da 1 a n ProviderOrders):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • Relazione uno-a-molti tra Provider e ProviderOrders (ogni Provider può essere associato da 0 a n ProviderOrders):

     App.Provider = DS.Model.extend({ link: DS.attr('string'), admin: DS.belongsTo('user', {async: true}), orders: DS.belongsTo('providerOrder', {async: true}), items: DS.hasMany('items', {async: true}) });
  • Relazione uno-a-molti tra ProviderOrders e Items (ogni ProviderOrder è composto da 1 a n articoli):

     App.ProviderOrder = DS.Model.extend({ // One of ['processed', 'in_delivery', 'delivered'] status: DS.attr('string'), provider: DS.belongsTo('provider', {async: true}), order: DS.belongsTo('order', {async: true}), items: DS.hasMany('item', {async: true}) }); App.Item = DS.Model.extend({ name: DS.attr('string'), price: DS.attr('number'), order: DS.belongsTo('order', {async: true}), provider: DS.belongsTo('provider', {async: true}) });

E non dimentichiamo la nostra Definizione del percorso :

 App.OrdersRoute = Ember.Route.extend({ model: function(){ // GET /orders // Retrieves all orders. return this.store.find('order'); } });

Ora ogni ProviderOrder ha un Provider, che era il nostro obiettivo principale. Gli articoli vengono spostati da Order a ProviderOrder e presuppone che tutti gli articoli in un ProviderOrder appartengano a un unico Provider.

Ridurre al minimo l'abbandono del codice

Sfortunatamente, ci sono alcune modifiche importanti qui. Vediamo quindi come Ember Data può aiutarci a ridurre al minimo qualsiasi abbandono del codice risultante nella nostra base di codice.

In precedenza, stavamo spingendo gli elementi con items.pushObject(item) . Ora dobbiamo prima trovare il ProviderOrder appropriato e inviarci un articolo:

 order.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); });

Dato che si tratta di un sacco di abbandono e più lavoro di Order che di controller, è meglio se spostiamo questo codice in Order#pushItem :

 App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}), /** returns a promise */ pushItem: function(item){ return this.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); }); } });

Ora possiamo aggiungere articoli direttamente all'ordine come order.pushItem(item) .

E per elencare gli articoli di ciascun ordine:

 App.Order = DS.Model.extend({ /* ... */ /** returns a promise */ items: function(){ return this.get('restaurantOrders').then(function(restaurantOrders){ var arrayOfPromisesContainingItems = restaurantOrders.mapBy('items') return arrayOfPromisesContainingItems.then(function(items){ return items.reduce(function flattenByReduce(memo, index, element){ return memo.pushObject(element); }, Ember.A([])); }); }); }.property('[email protected]') /* ... */ }); App.ItemsRoute = Ember.Route.extend({ model: function(){ // Multiple GET /items with ids[] query parameter. // Returns a promise. return this.modelFor('orders.show').get('items'); } });

Relazioni polimorfiche

Introduciamo ora un'ulteriore richiesta di miglioramento al nostro sistema che complica ulteriormente le cose:

Nuovo requisito n. 2: supportare più tipi di provider.

Per il nostro semplice esempio, diciamo che si definiscono due tipologie di Provider (“Shop” e “Bookstore”):

 App.Shop = App.Provider.extend({ status: DS.attr('string') }); App.BookStore = App.Provider.extend({ name: DS.attr('string') });

Ecco dove tornerà utile il supporto di Ember Data per le relazioni polimorfiche. Ember Data supporta relazioni polimorfiche uno-a-uno, uno-molti e molti-a-molti. Questo viene fatto semplicemente aggiungendo l'attributo polymorphic: true alla specifica dell'associazione. Per esempio:

 App.Provider = DS.Model.extend({ providerOrders: DS.hasMany('providerOrder', {async: true}) }); App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}) });

Il flag polymorphic sopra indica che esistono vari tipi di Provider che possono essere associati a un ProviderOrder (nel nostro caso, un Negozio o una Libreria).

Quando una relazione è polimorfica, la risposta del server dovrebbe indicare sia l'ID che il tipo dell'oggetto restituito ( RESTSerializer lo fa per impostazione predefinita); per esempio:

 GET /providerOrders { "providerOrders": [{ "status": "in_delivery", "provider": 1, "providerType": "shop" }] }

Soddisfare i nuovi requisiti

Abbiamo bisogno di fornitori e articoli polimorfici per soddisfare i requisiti. Poiché ProviderOrder collega i fornitori con gli articoli, possiamo modificare le sue associazioni in associazioni polimorfiche:

 App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });

C'è un problema rimanente, però: il provider ha un'associazione non polimorfica a Items ma Item è un tipo astratto. Abbiamo quindi due opzioni per affrontare questo problema:

  1. Richiedere che tutti i fornitori siano associati allo stesso tipo di articolo (ad esempio, dichiarare un tipo specifico di articolo per l'associazione con il fornitore)
  2. Dichiara l'associazione degli items su Provider come polimorfa

Nel nostro caso, dobbiamo andare con l'opzione n. 2 e dichiarare l'associazione degli items su Provider come polimorfa:

 App.Provider = DS.Model.extend({ /* ... */ items: DS.hasMany('items', {polymorphic: true, async: true}) });

Si noti che questo non introduce alcun abbandono del codice; tutte le associazioni funzionano semplicemente come prima di questo cambiamento. La potenza di Ember Data al suo meglio!

Ember Data può davvero modellare tutti i miei dati?

Ci sono ovviamente delle eccezioni, ma considero le convenzioni ActiveRecord come un modo standard e flessibile per strutturare e modellare i dati, quindi lascia che ti mostri come le convenzioni ActiveRecord vengono mappate su Ember Data:

has_many :users tramite: :ownerships o Rappresentare modelli intermedi

Questo consulterà un modello pivot chiamato Proprietà per trovare gli Utenti associati. Se il modello pivot è fondamentalmente una tabella pivot , puoi evitare di creare un modello intermedio in Ember Data e rappresentare la relazione con DS.hasMany su entrambi i lati.

Tuttavia, se hai bisogno di quella relazione pivot all'interno del tuo front-end, imposta un modello di proprietà che includa DS.belongsTo('user', {async: true}) e DS.belongsTo('provider', {async: true}) , e quindi aggiungere una proprietà sia per gli utenti che per i provider che esegue il mapping all'associazione utilizzando la proprietà; per esempio:

 App.User = DS.Model.extend({ // Omitted ownerships: DS.hasMany('ownership'), /** returns a promise */ providers: function(){ return this.get('ownerships').then(function(ownerships){ return ownerships.mapBy('provider'); }); }.property('[email protected]') }); App.Ownership = DS.Model.extend({ // One of ['approved', 'pending'] status: DS.attr('string'), user: DS.belongsTo('user', {async: true}), provider: DS.belongsTo('provider', {async: true}) }); App.Provider = DS.Model.extend({ // Omitted ownerships: DS.hasMany('ownership', {async: true}), /** returns a promise */ users: function(){ return this.get('ownerships').then(function(ownerships){ return ownerships.mapBy('user'); }); }.property('[email protected]') });

has_many :mappings, as: locatable

Nel nostro oggetto ActiveRecord, abbiamo una tipica relazione polimorfica:

 class User < ActiveRecord::Base has_many :mappings, as: locatable has_many :locations, through: :mappings end class Mapping < ActiveRecord::Base belongs_to :locatable, polymorphic: true belongs_to :location end class Location < ActiveRecord::Base has_many :mappings has_many :users, through: :mappings, source: :locatable, source_type: 'User' has_many :providers, through: :mappings, source: :locatable, source_type: 'Provider' def locatables users + providers end end

Questa è una relazione da molti (polimorfici) a molti (normali non polimorfici). In Ember Data possiamo esprimerlo con un DS.hasMany('locatable', {polymorphic: true, async: true}) e un DS.hasMany('location', {async: true}) statico:

 App.Locatable = DS.Model.extend({ locations: DS.hasMany('location', {async: true}) }); App.User = App.Locatable.extend({ userName: DS.attr('string') }); App.Location = DS.Model.extend({ locatables: DS.hasMany('locatable', {polymorphic: true, async: true}) });

Per i localizzabili, come Utente, il server deve restituire gli ID per le posizioni associate:

 GET /users { "users": [ { "id": "1", "userName": "Pooyan", "locations": ["1"] } ] }

Per Posizione, il server deve restituire sia l'ID che il tipo di Localizzabile nell'array di oggetti:

 GET /locations { "locations": [ { "id": "1", "locatables": [ {"id": "1", "type": "user"}, {"id": "2", "type": "provider"} ] } ] }

Inoltre, puoi rappresentare le relazioni per tipo con una relazione statica molti-a-molti:

 App.User = App.Locatable.extend({ userName: DS.attr('string'), locations: DS.hasMany('location', {async: true}) }); App.Provider = App.Locatable.extend({ link: DS.attr('string'), locations: DS.hasMany('location, {async: true} }); App.Location = DS.Model.extend({ users: DS.hasMany('user', {async: true}), providers: DS.hasMnay('provider', {async: true}) });

E per quanto riguarda i dati in tempo reale?

Ember Data dispone di push , pushPayload e update . Puoi sempre inviare manualmente i record nuovi/aggiornati nella cache locale di Ember Data (chiamata store) e gestirà tutto il resto.

 App.ApplicationRoute = Ember.Route.extend({ activate: function(){ socket.on('recordUpdated', function(response){ var type = response.type; var payload = response.payload; this.store.pushPayload(type, payload); }); } });

Personalmente uso i socket solo per eventi con payload molto piccoli. Un tipico evento è 'recordUpdated' con payload di {"type": "shop", "id": "14"} e quindi in ApplicationRoute controllerò se quel record è nella cache locale (store) e se è I Lo riacquisterò.

 App.ApplicationRoute = Ember.Route.extend({ activate: function(){ socket.on('recordUpdated', function(response){ var type = response.type; var id = response.id; if( this.store.hasRecordForId(type, id) ){ this.store.find(type, id); } }); } });

In questo modo possiamo inviare eventi record aggiornati a tutti i client senza un sovraccarico inaccettabile.

Esistono essenzialmente due approcci in Ember Data per gestire i dati in tempo reale:

  1. Scrivi un adattatore per il tuo canale di comunicazione in tempo reale e usalo al posto di RESTadapter.
  2. Invia i record al negozio principale ogni volta che sono disponibili.

Lo svantaggio della prima opzione è che è in qualche modo simile a reinventare la ruota. Per la seconda opzione, dobbiamo accedere al negozio principale, che è disponibile su tutte le rotte come route#store .

Incartare

In questo articolo, ti abbiamo presentato i costrutti e i paradigmi chiave di Ember Data, dimostrando il valore che può fornire a te come sviluppatore. Ember Data fornisce un flusso di lavoro di sviluppo più flessibile e snello, riducendo al minimo l'abbandono del codice in risposta a quelle che altrimenti sarebbero modifiche ad alto impatto.

L'investimento iniziale (tempo e curva di apprendimento) che fai utilizzando Ember Data per il tuo progetto si rivelerà senza dubbio utile poiché il tuo sistema si evolve inevitabilmente e deve essere esteso, modificato e migliorato.


APPENDICE: Argomenti avanzati sui dati di Ember

Questa appendice introduce una serie di argomenti di Ember Data più avanzati, tra cui:

  • Il design modulare di Ember
  • Caricamento laterale
  • Collegamenti
  • Serializzatore e adattatore modello attivo
  • Mixin di record incorporati
  • Modificatori di associazione (Async, Inverse e Polymorphic)
  • Parametro 'ids' nelle richieste GET

Design modulare

Ember Data ha un design modulare sotto il cofano. I componenti chiave includono:

  1. Gli Adapters sono responsabili della gestione della comunicazione, attualmente solo REST su HTTP.
  2. I Serializers gestiscono la creazione di modelli da JSON o viceversa.
  3. Store i record creati dalle cache.
  4. Il Container incolla tutti questi insieme.

I vantaggi di questo design includono:

  1. La deserializzazione e l'archiviazione dei dati funzionano indipendentemente dal canale di comunicazione utilizzato e dalla risorsa richiesta.
  2. Le configurazioni di lavoro, come ActiveModelSerializer o EmbeddedRecordsMixin, sono fornite immediatamente.
  3. Le origini dati (ad es. LocalStorage, implementazione CouchDB, ecc.) possono essere scambiate in entrata e in uscita cambiando gli adattatori.
  4. Nonostante molte convenzioni sulla configurazione, è possibile configurare tutto e condividere la propria configurazione/implementazione con la comunità.

Caricamento laterale

Ember Data supporta il "sideloading" dei dati; cioè, indicando i dati ausiliari che dovrebbero essere recuperati (insieme ai dati primari richiesti) in modo da aiutare a consolidare più richieste HTTP correlate.

Un caso d'uso comune è il sideload dei modelli associati. Ad esempio, ogni negozio ha molti generi alimentari, quindi possiamo includere tutti i generi alimentari correlati nella risposta di /shops :

 GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ] }

Quando si accede all'associazione della spesa, Ember Data rilascerà:

 GET /groceries?ids[]=98&ids[]=99&ids[]=112 { "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }

Tuttavia, se invece restituiamo i generi alimentari associati nell'endpoint /shops , Ember Data non dovrà inviare un'altra richiesta:

 GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ], "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }

Collegamenti

Ember Data accetta link al posto degli ID di associazione. Quando si accede a un'associazione specificata come collegamento, Ember Data emetterà una richiesta GET a quel collegamento per ottenere i record associati.

Ad esempio, potremmo restituire un collegamento per un'associazione di generi alimentari:

 GET /shops { "shops": [ { "id": "14", "links": { "groceries": "/shops/14/groceries" } } ] }

E Ember Data avrebbe quindi inviato una richiesta a /shops/14/groceries

 GET /shops/14/groceries { "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }

Tieni presente che devi comunque rappresentare l'associazione nei dati; i link suggeriscono semplicemente una nuova richiesta HTTP e non influiranno sulle associazioni.

Serializzatore e adattatore modello attivo

Probabilmente, ActiveModelSerializer e ActiveModelAdapter sono più utilizzati nella pratica rispetto a RESTSerializer e RESTAdapter . In particolare, quando il backend utilizza Ruby on Rails e la gemma ActiveModel::Serializers , l'opzione migliore è utilizzare ActiveModelSerializer e ActiveModelAdapter , poiché supportano ActiveModel::Serializers out of the box.

Fortunatamente, tuttavia, le differenze di utilizzo tra ActiveModelSerializer / ActiveModelAdapter e RESTSerializer / RESTAdapter sono piuttosto limitate; vale a dire:

  1. ActiveModelSerializer utilizzerà i nomi dei campi snake_case mentre RESTSerializer richiede i nomi dei campi camelCased.
  2. ActiveModelAdapter invia richieste ai metodi API snake_case mentre RESTSerializer invia richieste ai metodi API camelCased.
  3. ActiveModelSerializer prevede che i nomi dei campi correlati all'associazione terminino con _id o _ids mentre RESTSerializer prevede che i nomi dei campi correlati all'associazione siano gli stessi del campo dell'associazione.

Indipendentemente dalla scelta dell'adattatore e del serializzatore, i modelli Ember Data saranno esattamente gli stessi. Solo la rappresentazione JSON e gli endpoint API saranno diversi.

Prendi come esempio il nostro ProviderOrder finale:

 App.ApplicationSerializer = DS.ActiveModelSerializer.extend({ }); App.ProviderOrder = DS.Model.extend({ // One of ['processed', 'in_delivery', 'delivered'] status: DS.attr('string'), provider: DS.belongsTo('provider', {polymorphic: true, async: true}), order: DS.belongsTo('order', {async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });

Con Active Model Serializer e Adapter, il server dovrebbe aspettarsi:

 Post /provider_orders { "provider_order": [ "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", ] }

… e dovrebbe rispondere con:

 GET /provider_orders { "provider_orders": [ "id": "1", "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", "items": [ {"id": "57", "type": "grocery"}, {"id": "89", "type": "grocery"} ] ] }

Mixin di record incorporati

DS.EmbeddedRecordsMixin è un'estensione per DS.ActiveModelSerializer che consente di configurare la modalità di serializzazione o deserializzazione delle associazioni. Sebbene non sia ancora completo (soprattutto per quanto riguarda le associazioni polimorfiche), è comunque intrigante.

Puoi scegliere:

  1. Non serializzare o deserializzare le associazioni.
  2. Per serializzare o deserializzare associazioni con ID o ID.
  3. Per serializzare o deserializzare le associazioni con i modelli incorporati.

Ciò è particolarmente utile nelle relazioni uno-a-molti in cui, per impostazione predefinita, gli ID associati DS.hasMany non vengono aggiunti agli oggetti serializzati. Prendi un carrello della spesa che contiene molti articoli come esempio. In questo esempio, il carrello viene creato mentre gli articoli sono noti. Tuttavia, quando salvi il carrello, Ember Data non inserisce automaticamente gli ID degli articoli associati nel payload della richiesta.

Utilizzando DS.EmbeddedRecordsMixin , tuttavia, è possibile indicare a Ember Data di serializzare gli ID articolo sul carrello come segue:

 App.CartSerializer = DS.ActiveModelSerializer .extend(DS.EmbeddedRecordsMixin) .extend{ attrs: { // thanks EmbeddedRecordsMixin! items: {serialize: 'ids', deserialize: 'ids'} } }); App.Cart = DS.Model.extend({ items: DS.hasMany('item', {async: true}) }); App.Item = DS.Model.extend({ cart: DS.belongsTo('item', {async: true}) });

Come mostrato nell'esempio precedente, EmbeddedRecordsMixin consente la specifica esplicita di quali associazioni serializzare e/o deserializzare tramite l'oggetto attrs . I valori validi per serialize e deserialize sono: - 'no' : non include l'associazione nei dati serializzati/deserializzati - 'id' o 'ids' : include gli ID associati nei dati serializzati/deserializzati - 'records ': include le proprietà effettive (vale a dire, i valori dei campi del record) come matrice in dati serializzati/deserializzati

Modificatori di associazione (Async, Inverse e Polymorphic)

Sono supportati i seguenti modificatori di associazione: polymorphic , inverse e async

Modificatore polimorfico

In un'associazione polimorfica, uno o entrambi i lati dell'associazione rappresentano una classe di oggetti, piuttosto che un oggetto specifico.

Ricordiamo il nostro precedente esempio di blog in cui dovevamo supportare la possibilità di taggare sia i post che le pagine. Per supportare questo, siamo arrivati ​​al seguente modello:

  • Ogni Post è un Taggable e ha molti Tag
  • Ogni Pagina è un Taggable e ha molti Tag
  • Ogni Tag ha molti Taggable polimorfici

Seguendo quel modello, un modificatore polymorphic può essere utilizzato per dichiarare che i Tag sono correlati a qualsiasi tipo di "Taggable" (che può essere un Post o una Pagina), come segue:

 // A Taggable is something that can be tagged (ie, that has tags) App.Taggable = DS.Model.extend({ tags: DS.hasMany('tag') }); // A Page is a type of Taggable App.Page = App.Taggable.extend({}); // A Post is a type of Taggable App.Post = App.Taggable.extend App.Tag = DS.Model.extend({ // the "other side" of this association (ie, the 'taggable') is polymorphic taggable: DS.belongsTo('taggable', {polymorphic: true}) });

Modificatore inverso

Di solito le associazioni sono bidirezionali. Ad esempio, "Il post ha molti commenti" sarebbe una direzione di un'associazione, mentre "Il commento appartiene a un post" sarebbe l'altra direzione (cioè "inversa") di tale associazione.

Nei casi in cui non vi siano ambiguità nell'associazione, è necessario specificare solo una direzione poiché Ember Data può dedurre la parte inversa dell'associazione.

However, in cases where objects in your model have multiple associations with one another, Ember Data cannot derive the inverse of each association automatically and it therefore needs to be specified using the invers modifier.

Consider, for example, a case where:

  • Each Page may have many Users as Collaborators
  • Each Page may have many Users as Maintainers
  • Each User may have many Favorite Pages
  • Each User may have many Followed Pages

This would need to be specified as follows in our model:

 App.User = DS.Model.extend({ favoritePages: DS.hasMany('page', {inverse: 'favoritors}), followedPages: DS.hasMany('page', {inverse: 'followers'}), collaboratePages: DS.hasMany('page', {inverse: 'collaborators'}), maintainedPages: DS.hasMany('page', {inverse: 'maintainers'}) }); App.Page = DS.Model.extend({ favoritors: DS.hasMany('user', {inverse: 'favoritePages'}), followers: DS.hasMany('user', {inverse: 'followedPages'}), collaborators: DS.hasMany('user', {inverse: 'collaboratedPages}), maintainers: DS.hasMany('user', {inverse: 'maintainedPages'}) });

Async Modifier

When data needs to be retrieved based on relevant associations, that associated data may or may not already have been loaded. If not, a synchronous association will throw an error since the associated data has not been loaded.

{async: true} indicates that the request for the associated data should be handled asynchronously. The request therefore immediately returns a promise and the supplied callback is invoked once the associated data has been retrieved and is available.

When async is false , getting associated objects would be done as follows:

 post.get('comments').pushObject(comment);

When async is true , getting associated objects would be done as follows (note the callback function specified):

 post.get('comments').then(function(comments){ comments.pushObject(comment); })

Note well: The current default value of async is false , but in Ember Data 1.0 the default will be true .

'ids' Parameter in GET Requests

By default, Ember Data expects that resources aren't nested. Take Posts which have many Comments as an example. In a typical interpretation of REST, API endpoints might look like these:

 GET /users GET /users/:id/posts GET /users/:id/posts/:id/comments

However, Ember Data expects API endpoints to be flat, and not nested; per esempio:

 GET /users with ?ids[]=4 as query parameters. GET /posts with ?ids[]=18&ids[]=27 as query parameters. GET /comments with ?ids[]=74&ids[]=78&ids[]=114&ids[]=117 as query parameters.

In the above example, ids is the name of the array, [] indicates that this query parameter is an array, and = indicates that a new ID is being pushed onto the array.

Now Ember Data can avoid downloading resources it already has or defer downloading them to very last moment.

Also, to white list an array parameter using the StrongParameters gem, you can declare it as params.require(:shop).permit(item_ids: []) .