Ember Data: Un tutorial cuprinzător pentru biblioteca ember-data

Publicat: 2022-03-11

Ember Data (alias ember-data sau ember.data) este o bibliotecă pentru gestionarea robustă a datelor modelului în aplicațiile Ember.js. Dezvoltatorii Ember Data declară că este conceput pentru a fi agnostic față de mecanismul de persistență de bază, așa că funcționează la fel de bine cu API-urile JSON prin HTTP, precum și cu streaming WebSockets sau stocarea locală IndexedDB. Oferă multe dintre facilitățile pe care le-ai găsi în mapările relaționale de obiecte (ORM) de pe server, cum ar fi ActiveRecord, dar este conceput special pentru mediul unic al JavaScript din browser.

În timp ce Ember Data poate dura ceva timp, odată ce ați făcut acest lucru, probabil veți descoperi că a meritat investiția. În cele din urmă, va face dezvoltarea, îmbunătățirea și întreținerea sistemului dvs. mult mai ușoară.

Când un API este reprezentat folosind modele, adaptoare și serializatoare Ember Data, fiecare asociere devine pur și simplu un nume de câmp. Acest lucru încapsulează detaliile interne ale fiecărei asociații, izolând astfel restul codului dvs. de modificările aduse asociațiilor în sine. Restul codului dvs. nu va interesa, de exemplu, dacă o anumită asociere este polimorfă sau este rezultatul unei hărți a mai multor asociații.

Mai mult, baza de cod este în mare parte izolată de modificările backend, chiar dacă acestea sunt semnificative, deoarece tot ce se așteaptă baza de cod sunt câmpuri și funcții pe modele, nu o reprezentare JSON sau XML sau YAML a modelului.

În acest tutorial, vom introduce cele mai importante caracteristici ale Ember Data și vom demonstra modul în care ajută la minimizarea pierderii codului, concentrându-ne pe un exemplu din lumea reală.

De asemenea, este furnizată o anexă care discută o serie de subiecte și exemple mai avansate Ember Data.

Notă: acest articol presupune o familiaritate de bază cu Ember.js. Dacă nu sunteți familiarizat cu Ember.js, consultați tutorialul nostru popular Ember.js pentru o introducere. Avem, de asemenea, un ghid JavaScript complet disponibil în rusă, portugheză și spaniolă.

Propunerea de valoare a datelor Ember

Un exemplu despre modul în care biblioteca Ember Data poate ajuta la satisfacerea nevoilor clienților.

Să începem prin a lua în considerare un exemplu simplu.

Să presupunem că avem o bază de cod de lucru pentru un sistem de blog de bază. Sistemul conține postări și etichete, care au o relație de la mulți la mulți.

Totul este în regulă până când primim o cerință de a sprijini Pagini. Cerința prevede, de asemenea, că, deoarece este posibil să etichetăm o pagină în WordPress, ar trebui să putem face și noi.

Deci, acum, etichetele nu se vor mai aplica numai postărilor, ci se pot aplica și paginilor. Ca urmare, simpla noastră asociere între etichete și postări nu va mai fi adecvată. În schimb, vom avea nevoie de o relație polimorfă unilaterală multi-la-mulți, cum ar fi următoarea:

  • Fiecare postare este etichetabilă și are multe etichete
  • Fiecare pagină este etichetabilă și are multe etichete
  • Fiecare etichetă are multe etichete polimorfe

Tranziția la acest set nou, mai complex de asocieri este probabil să aibă ramificații semnificative în codul nostru, ceea ce duce la o mulțime de pierderi. Deoarece nu avem idee cum să serializeze o asociere polimorfă la JSON, probabil că vom crea mai multe puncte finale API precum GET /posts/:id/tags și GET /pages/:id/tags . Și apoi, vom arunca toate funcțiile noastre de analiză JSON existente și vom scrie altele noi pentru noile resurse adăugate. Uf. Obositor și dureros.

Acum să ne gândim cum am aborda acest lucru folosind Ember Data.

În Ember Data, adaptarea acestui set modificat de asocieri ar implica pur și simplu trecerea de la:

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

la:

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

Schimbarea rezultată în restul codului nostru ar fi minimă și am putea reutiliza majoritatea șabloanelor noastre. Rețineți în special că numele asociației tags de pe Post rămâne neschimbat. În plus, restul bazei noastre de cod se bazează doar pe existența asocierii tags și nu ține seama de detaliile acesteia.

Un Ember Data Primer

Înainte de a ne scufunda într-un exemplu din lumea reală, să trecem în revistă câteva elemente fundamentale ale Ember Data.

Trasee și Modele

În Ember.js, routerul este responsabil pentru afișarea șabloanelor, încărcarea datelor și, în alt mod, configurarea stării aplicației. Routerul potrivește URL-ul curent cu rutele pe care le-ați definit, astfel încât o Rută este responsabilă pentru specificarea modelului pe care trebuie să-l afișeze un șablon (Ember se așteaptă ca acest model să fie subclasa Ember.Object ):

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

Ember Data oferă DS.Model , care este o subclasă a Ember.Object și adaugă capabilități precum salvarea sau actualizarea unei singure înregistrări sau a mai multor înregistrări pentru comoditate.

Pentru a crea un model nou, creăm o subclasă de DS.Model (de exemplu, App.User = DS.Model.extend({}) ).

Ember Data se așteaptă la o structură JSON bine definită, intuitivă de la server și serializează înregistrările nou create în același JSON structurat.

Ember Data oferă, de asemenea, o suită de clase de matrice, cum ar fi DS.RecordArray pentru lucrul cu modele. Acestea au responsabilități precum gestionarea relațiilor unu-la-mulți sau mulți-la-mulți, gestionarea preluării asincrone a datelor și așa mai departe.

Atributele modelului

Atributele modelului de bază sunt definite folosind DS.attr ; de exemplu:

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

Doar câmpurile create de DS.attr vor fi incluse în sarcina utilă care este transmisă serverului pentru crearea sau actualizarea înregistrărilor.

DS.attr acceptă patru tipuri de date: string , number , boolean și date .

RESSErializator

În mod implicit:

  • Ember Data folosește RESTSerializer pentru a crea obiecte din răspunsurile API (deserializare) și pentru a genera JSON pentru solicitările API (serializare).
  • RESTSerializer se așteaptă ca câmpurile create de DS.belongsTo să aibă un câmp numit user inclus în răspunsul JSON de la server. Acel câmp conține id-ul înregistrării la care se face referire.
  • RESTSerializer adaugă un câmp user la sarcina utilă transmisă API-ului cu id-ul comenzii asociate.

Un răspuns ar putea, de exemplu, să arate astfel:

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

Și o solicitare HTTP creată de RESTSerializer pentru salvarea unei comenzi ar putea arăta astfel:

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

Relații unu-la-unu

Spunem, de exemplu, că fiecare Utilizator are un Profil unic. Putem reprezenta această relație în Ember Data folosind DS.belongsTo atât pe User, cât și pe Profil:

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

Apoi putem obține asocierea cu user.get('profile') sau o putem seta cu user.set('profile', aProfile) .

RESTSerializer se așteaptă ca ID-ul modelului asociat să fie furnizat pentru fiecare model; de exemplu:

 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 */ } ] }

În mod similar, include ID-ul modelului asociat într-o sarcină utilă de solicitare:

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

Relații unu-la-mulți și multi-la-unu

Să presupunem că avem un model în care o postare are multe comentarii. În Ember Data, putem reprezenta această relație cu DS.hasMany('comment', {async: true}) pe Post și DS.belongsTo('post', {async: true}) pe 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}) });

Putem apoi să obținem elemente asociate cu post.get('comments', {async: true}) și să adăugăm o nouă asociere cu post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

Serverul va răspunde apoi cu o serie de ID-uri pentru comentariile corespunzătoare la o Postare:

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

… și cu un ID pentru fiecare Comentariu:

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

RESTSerializer adaugă id-ul postării asociate la Comentariu:

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

Rețineți totuși că, în mod implicit, RESTSerializer nu va adăuga ID-uri asociate DS.hasMany la obiectele pe care le serializează, deoarece acele asociații sunt specificate pe partea „multe” (adică cele care au o asociere DS.belongsTo ). Deci, în exemplul nostru, deși o Postare are multe comentarii, acele ID-uri nu vor fi adăugate la obiectul Post:

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

Pentru a „forța” DS.hasMany ID-uri să fie, de asemenea, serializate, puteți utiliza Mixin-ul Embedded Records.

Relații multi-la-multe

Să spunem că în modelul nostru un Autor poate avea mai multe postări și o postare poate avea mai mulți autori.

Pentru a reprezenta această relație în Ember Data, putem folosi DS.hasMany('author', {async: true}) pe Post și DS.hasMany('post', {async: true}) pe 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}) });

Apoi putem obține articole asociate cu author.get('posts') și adăugam o nouă asociere cu author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

Serverul va răspunde apoi cu o serie de ID-uri pentru obiectele corespunzătoare; de exemplu:

 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 */ } ] }

Deoarece aceasta este o relație multi-la-mulți, RESTSerializer adaugă o serie de ID-uri ale obiectelor asociate; de exemplu:

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

Un exemplu real: îmbunătățirea unui sistem de comandă existent

În sistemul nostru de comandă existent, fiecare Utilizator are multe Comenzi și fiecare Comandă are multe Articole. Sistemul nostru are mai mulți Furnizori (adică, vânzători) de la care pot fi comandate produse, dar fiecare comandă poate conține doar articole de la un singur furnizor.

Noua cerință #1: activați o singură comandă pentru a include articole de la mai mulți furnizori.

În sistemul existent, există o relație unu-la-mulți între Furnizori și Comenzi. Odată ce extindem o comandă pentru a include articole de la mai mulți furnizori, totuși, această relație simplă nu va mai fi adecvată.

Mai exact, dacă un furnizor este asociat cu o comandă întreagă, în sistemul îmbunătățit acea comandă poate include foarte bine articole comandate de la alți furnizori. Prin urmare, trebuie să existe o modalitate de a indica ce parte din fiecare comandă este relevantă pentru fiecare furnizor. Mai mult, atunci când un furnizor își accesează comenzile, ar trebui să aibă vizibilitate numai asupra articolelor comandate de la ei, nu asupra altor articole pe care clientul le-ar fi comandat de la alți furnizori.

O abordare ar putea fi introducerea a două noi asociații multi-la-multi; unul între Comandă și Articol și altul între Comandă și Furnizor.

Cu toate acestea, pentru a simplifica lucrurile, introducem o nouă construcție în modelul de date, la care ne referim ca „ProviderOrder”.

Redactarea relaţiilor

Modelul de date îmbunătățit va trebui să găzduiască următoarele asocieri:

  • Relație unu-la-mulți între Utilizatori și Comenzi (fiecare Utilizator poate fi asociat cu 0 până la n Comenzi) și o relație Unu-la-mulți între Utilizatori și Furnizori (fiecare Utilizator poate fi asociat cu 0 până la n Furnizori)

     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}) });
  • Relație unu-la-mulți între Comenzi și Comenzi Furnizor (fiecare Comandă constă din 1 până la n Comenzi Furnizor):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • Relație unu-la-mulți între Furnizori și Comenzi Furnizor (fiecare Furnizor poate fi asociat cu 0 până la n Comenzi Furnizor):

     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}) });
  • Relație unu-la-mulți între ProviderComenzi și Articole (fiecare ProviderOrder constă din 1 până la n articole):

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

Și să nu uităm definiția traseului nostru:

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

Acum, fiecare ProviderOrder are un Furnizor, care a fost obiectivul nostru principal. Articolele sunt mutate de la Comandă la Comandă de furnizor și se presupune că toate articolele dintr-o Comandă de furnizor aparțin unui singur Furnizor.

Minimizarea pierderii codului

Din nefericire, aici au loc câteva schimbări radicale. Deci, să vedem cum Ember Data ne poate ajuta să minimizăm orice pierdere de cod rezultată în baza noastră de cod.

Anterior, împingam articole cu items.pushObject(item) . Acum trebuie să găsim mai întâi ProviderOrder-ul corespunzător și să împingem un articol către el:

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

Deoarece aceasta este multă pierdere și mai multă sarcină a Comandei decât a controlorului, este mai bine dacă mutăm acest cod în 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); }); }); } });

Acum putem adăuga articole direct la comandă, cum ar fi order.pushItem(item) .

Și pentru listarea articolelor din fiecare comandă:

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

Relații polimorfe

Să introducem acum o solicitare suplimentară de îmbunătățire a sistemului nostru, care complică și mai mult lucrurile:

Noua cerință #2: Acceptați mai multe tipuri de Furnizori.

Pentru exemplul nostru simplu, să presupunem că sunt definiți două tipuri de Furnizori („Magazin” și „Librărie”):

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

Iată unde va fi utilă suportul Ember Data pentru relațiile polimorfe. Ember Data acceptă relații polimorfe unu-la-unu, unu-la-mulți și mulți-la-mulți. Acest lucru se face pur și simplu prin adăugarea atributului polymorphic: true la specificația asocierii. De exemplu:

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

Steagul polymorphic de mai sus indică faptul că există diferite tipuri de Furnizori care pot fi asociați cu o Comanda Provider (în cazul nostru, fie un Magazin, fie o Librărie).

Când o relație este polimorfă, răspunsul serverului ar trebui să indice atât ID-ul, cât și tipul obiectului returnat ( RESTSerializer face acest lucru în mod implicit); de exemplu:

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

Îndeplinirea noilor cerințe

Avem nevoie de Furnizori și Articole polimorfe pentru a îndeplini cerințele. Deoarece ProviderOrder conectează Furnizorii cu Elemente, putem schimba asocierile sale în asocieri polimorfe:

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

Totuși, există o problemă rămasă: furnizorul are o asociere nepolimorfă cu Elemente, dar Itemul este de tip abstract. Prin urmare, avem două opțiuni pentru a rezolva acest lucru:

  1. Solicitați ca toți furnizorii să fie asociați cu același tip de articol (adică să declare un anumit tip de articol pentru asocierea cu Furnizorul)
  2. Declarați asocierea items pe Furnizor ca polimorfă

În cazul nostru, trebuie să mergem cu opțiunea #2 și să declarăm asocierea items pe Provider ca polimorfă:

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

Rețineți că acest lucru nu introduce nicio modificare a codului; toate asociațiile funcționează pur și simplu așa cum făceau înainte de această schimbare. Puterea Ember Data la maxim!

Ember Data poate modela cu adevărat toate datele mele?

Există, desigur, excepții, dar consider convențiile ActiveRecord ca o modalitate standard și flexibilă de structurare și modelare a datelor, așa că permiteți-mi să vă arăt cum se mapează convențiile ActiveRecord la Ember Data:

has_many :users prin: :ownerships sau Reprezentant Intermediate Models

Aceasta va consulta un model pivot numit Proprietate pentru a găsi utilizatori asociați. Dacă modelul pivot este practic un tabel pivot, puteți evita crearea unui model intermediar în Ember Data și reprezentați relația cu DS.hasMany pe ambele părți.

Cu toate acestea, dacă aveți nevoie de această relație pivot în interiorul front-end-ului, configurați un model de proprietate care să includă DS.belongsTo('user', {async: true}) și DS.belongsTo('provider', {async: true}) , și apoi adăugați o proprietate atât pentru Utilizatori, cât și pentru Furnizori, care se mapează cu asociația folosind Proprietatea; de exemplu:

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

are_many :mappings, ca: localizabil

În obiectul nostru ActiveRecord, avem o relație polimorfă tipică:

 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

Aceasta este o relație multi (polimorfă) la mai multe (nepolimorfă normală). În Ember Data putem exprima acest lucru cu un DS.hasMany('locatable', {polymorphic: true, async: true}) și un DS.hasMany('location', {async: true}) :

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

Pentru Locabile, cum ar fi User, serverul ar trebui să returneze ID-urile pentru locațiile asociate:

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

Pentru Locație, serverul ar trebui să returneze atât ID-ul, cât și tipul de Locatable în matricea de obiecte:

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

De asemenea, puteți reprezenta relații după tip cu o relație statică multi-la-mulți:

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

Și cum rămâne cu datele în timp real?

Ember Data are push , pushPayload și update . Puteți oricând să introduceți manual înregistrări noi/actualizate în memoria cache locală a Ember Data (numit magazin) și se va ocupa de restul.

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

Eu personal folosesc doar prize pentru evenimente cu încărcături foarte mici. Un eveniment obișnuit este „recordUpdated” cu sarcina utilă de {"type": "shop", "id": "14"} și apoi în ApplicationRoute voi verifica dacă acea înregistrare este în memoria cache locală (magazin) și dacă este eu Îl voi relua.

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

În acest fel, putem trimite înregistrări de evenimente actualizate către toți clienții fără costuri generale inacceptabile.

Există în esență două abordări în Ember Data pentru a trata datele în timp real:

  1. Scrieți un adaptor pentru canalul dvs. de comunicare în timp real și utilizați-l în loc de RESTAdapter.
  2. Împingeți înregistrările în magazinul principal ori de câte ori sunt disponibile.

Dezavantajul primei opțiuni este că seamănă oarecum cu reinventarea roții. Pentru a doua opțiune, trebuie să accesăm magazinul principal, care este disponibil pe toate rutele ca route#store .

Învelire

În acest articol, v-am prezentat principalele constructe și paradigme ale Ember Data, demonstrând valoarea pe care vi le poate oferi în calitate de dezvoltator. Ember Data oferă un flux de lucru de dezvoltare mai flexibil și mai eficient, minimizând pierderea codului ca răspuns la ceea ce altfel ar fi schimbări cu impact mare.

Investiția inițială (timp și curba de învățare) pe care o faceți utilizând Ember Data pentru proiectul dvs. se va dovedi, fără îndoială, utilă, deoarece sistemul dumneavoastră evoluează inevitabil și trebuie extins, modificat și îmbunătățit.


ANEXĂ: Subiecte avansate de date Ember

Această anexă prezintă o serie de subiecte Ember Data mai avansate, inclusiv:

  • Designul modular al lui Ember
  • Încărcare laterală
  • Legături
  • Serializator și adaptor de model activ
  • Înregistrări încorporate Mixin
  • Modificatori de asociere (asincrone, inverse și polimorfe)
  • Parametrul „ids” în cererile GET

Design modular

Ember Data are un design modular sub capotă. Componentele cheie includ:

  1. Adapters sunt responsabile pentru gestionarea comunicațiilor, în prezent doar REST prin HTTP.
  2. Serializers gestionează crearea de modele din JSON sau invers.
  3. Store în cache înregistrările create.
  4. Container lipește toate acestea împreună.

Beneficiile acestui design includ:

  1. Deserializarea și stocarea datelor funcționează independent de canalul de comunicare folosit și de resursa solicitată.
  2. Configurațiile de lucru, cum ar fi ActiveModelSerializer sau EmbeddedRecordsMixin, sunt furnizate imediat.
  3. Sursele de date (de exemplu, LocalStorage, implementarea CouchDB etc.) pot fi schimbate prin schimbarea adaptoarelor.
  4. În ciuda multor convenții asupra configurației, este posibil să configurați totul și să vă împărtășiți configurația/implementarea cu comunitatea.

Încărcare laterală

Ember Data acceptă „încărcarea laterală” a datelor; adică, indicarea datelor auxiliare care ar trebui recuperate (împreună cu datele primare solicitate) pentru a ajuta la consolidarea multiplelor solicitări HTTP asociate.

Un caz comun de utilizare este încărcarea laterală a modelelor asociate. De exemplu, fiecare magazin are multe produse alimentare, așa că putem include toate alimentele aferente în răspunsul /shops :

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

Când se accesează asociația de produse alimentare, Ember Data va emite:

 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" } ] }

Cu toate acestea, dacă în schimb returnăm produse alimentare asociate în punctul final /shops , Ember Data nu va trebui să emită o altă solicitare:

 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" } ] }

Legături

Ember Data acceptă link-uri în locul ID-urilor de asociere. Când o asociație specificată ca link este accesată, Ember Data va emite o solicitare GET către acel link pentru a obține înregistrările asociate.

De exemplu, am putea returna un link pentru o asociație de produse alimentare:

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

Și Ember Data ar trimite apoi o solicitare către /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" } ] }

Rețineți că mai trebuie să reprezentați asociația în date; linkurile sugerează doar o nouă solicitare HTTP și nu vor afecta asocierile.

Serializator și adaptor de model activ

Se poate spune că ActiveModelSerializer și ActiveModelAdapter sunt mai utilizate în practică decât RESTSerializer și RESTAdapter . În special, atunci când backend-ul folosește Ruby on Rails și bijuteria ActiveModel::Serializers , cea mai bună opțiune este să utilizați ActiveModelSerializer și ActiveModelAdapter , deoarece acceptă ActiveModel::Serializers din cutie.

Din fericire, totuși, diferențele de utilizare între ActiveModelSerializer / ActiveModelAdapter și RESTSerializer / RESTAdapter sunt destul de limitate; și anume:

  1. ActiveModelSerializer va folosi nume de câmpuri snake_case, în timp ce RESTSerializer necesită nume de câmpuri camelCased.
  2. ActiveModelAdapter emite solicitări către metodele API snake_case, în timp ce RESTSerializer emite metode API camelCased.
  3. ActiveModelSerializer se așteaptă ca numele câmpurilor legate de asociere să se termine în _id sau _ids , în timp ce RESTSerializer se așteaptă ca numele câmpurilor legate de asociere să fie aceleași cu câmpul de asociere.

Indiferent de alegerea adaptorului și serializatorului, modelele Ember Data vor fi exact aceleași. Numai reprezentarea JSON și punctele finale API vor fi diferite.

Luați Comanda noastră finală ca exemplu:

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

Cu Active Model Serialize and Adapter, serverul ar trebui să se aștepte la:

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

… și ar trebui să răspundă cu:

 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"} ] ] }

Înregistrări încorporate Mixin

DS.EmbeddedRecordsMixin este o extensie pentru DS.ActiveModelSerializer care permite configurarea modului în care asociațiile sunt serializate sau deserializate. Deși nu este încă complet (în special în ceea ce privește asociațiile polimorfe), este totuși intrigant.

Tu poti alege:

  1. A nu serializa sau deserializa asociațiile.
  2. Pentru a serializa sau deserializa asocierile cu id sau id-uri.
  3. Pentru a serializa sau deserializa asocierile cu modele încorporate.

Acest lucru este util în special în relațiile unu-la-mai multe în care, implicit, DS.hasMany ID-uri asociate nu sunt adăugate la obiectele care sunt serializate. Luați ca exemplu un coș de cumpărături care are multe articole. În acest exemplu, coșul este creat în timp ce articolele sunt cunoscute. Cu toate acestea, atunci când salvați coșul de cumpărături, Ember Data nu va pune automat ID-urile articolelor asociate în sarcina utilă a cererii.

Cu toate acestea, folosind DS.EmbeddedRecordsMixin , este posibil să îi spuneți lui Ember Data să serializeze ID-urile articolului din coș, după cum urmează:

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

După cum se arată în exemplul de mai sus, EmbeddedRecordsMixin permite specificarea explicită a asociațiilor de serializat și/sau deserializat prin obiectul attrs . Valorile valide pentru serialize și deserialize sunt: ​​- 'no' : nu includ asocierea în datele serializate/deserializate - 'id' sau 'ids' : includ ID-urile asociate în datele serializate/deserializate - 'records ”: includeți proprietățile reale (adică, înregistrează valorile câmpului) ca o matrice în date serializate/deserializate

Modificatori de asociere (asincrone, inverse și polimorfe)

Sunt acceptați următorii modificatori de asociere: polymorphic , inverse și async

Modificator polimorf

Într-o asociere polimorfă, una sau ambele părți ale asocierii reprezintă o clasă de obiecte, mai degrabă decât un obiect specific.

Amintiți-vă de exemplul nostru anterior de blog în care trebuia să sprijinim capacitatea de a eticheta atât postări, cât și pagini. Pentru a susține acest lucru, am ajuns la următorul model:

  • Fiecare postare este etichetabilă și are multe etichete
  • Fiecare pagină este etichetabilă și are multe etichete
  • Fiecare etichetă are multe etichete polimorfe

Urmând acest model, un modificator polymorphic poate fi utilizat pentru a declara că Etichetele sunt legate de orice tip de „Taggable” (care poate fi fie o postare, fie o pagină), după cum urmează:

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

Modificator invers

De obicei, asociațiile sunt bidirecționale. De exemplu, „Postarea are multe comentarii” ar fi o direcție a unei asocieri, în timp ce „Comentariul aparține unei postări” ar fi cealaltă direcție (adică „inversa”) a acelei asociații.

În cazurile în care nu există ambiguitate în asociere, trebuie specificată o singură direcție, deoarece Ember Data poate deduce partea inversă a asocierii.

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; de exemplu:

 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: []) .