Ember Data: Ein umfassendes Tutorial für die Ember-Data-Bibliothek

Veröffentlicht: 2022-03-11

Ember Data (auch bekannt als ember-data oder ember.data) ist eine Bibliothek zur robusten Verwaltung von Modelldaten in Ember.js-Anwendungen. Die Entwickler von Ember Data geben an, dass es so konzipiert ist, dass es für den zugrunde liegenden Persistenzmechanismus agnostisch ist, sodass es mit JSON-APIs über HTTP genauso gut funktioniert wie mit Streaming von WebSockets oder lokalem IndexedDB-Speicher. Es bietet viele der Möglichkeiten, die Sie in serverseitigen objektrelationalen Mappings (ORMs) wie ActiveRecord finden würden, wurde jedoch speziell für die einzigartige Umgebung von JavaScript im Browser entwickelt.

Es kann zwar einige Zeit dauern, bis Ember Data grok ist, aber sobald Sie dies getan haben, werden Sie wahrscheinlich feststellen, dass sich die Investition gelohnt hat. Letztendlich wird es die Entwicklung, Erweiterung und Wartung Ihres Systems erheblich vereinfachen.

Wenn eine API mithilfe von Ember Data-Modellen, Adaptern und Serialisierern dargestellt wird, wird jede Zuordnung einfach zu einem Feldnamen. Dadurch werden die internen Details jeder Zuordnung gekapselt, wodurch der Rest Ihres Codes von Änderungen an den Zuordnungen selbst isoliert wird. Der Rest Ihres Codes kümmert sich beispielsweise nicht darum, ob eine bestimmte Assoziation polymorph ist oder das Ergebnis einer Karte vieler Assoziationen ist.

Darüber hinaus ist Ihre Codebasis weitgehend von Backend-Änderungen isoliert, selbst wenn diese erheblich sind, da Ihre Codebasis nur Felder und Funktionen in Modellen erwartet, keine JSON-, XML- oder YAML-Darstellung des Modells.

In diesem Tutorial stellen wir die wichtigsten Funktionen von Ember Data vor und demonstrieren, wie es hilft, die Codeänderung zu minimieren, indem wir uns auf ein Beispiel aus der realen Welt konzentrieren.

Es wird auch ein Anhang bereitgestellt, der eine Reihe fortgeschrittener Ember Data-Themen und -Beispiele behandelt.

Hinweis: Dieser Artikel setzt eine gewisse grundlegende Vertrautheit mit Ember.js voraus. Wenn Sie mit Ember.js nicht vertraut sind, sehen Sie sich unser beliebtes Ember.js-Tutorial für eine Einführung an. Wir haben auch einen Full-Stack-JavaScript-Leitfaden in Russisch, Portugiesisch und Spanisch.

Das Ember Data Value Proposition

Ein Beispiel dafür, wie die Ember-Datenbibliothek dazu beitragen kann, Kundenanforderungen zu erfüllen.

Beginnen wir mit einem einfachen Beispiel.

Angenommen, wir haben eine funktionierende Codebasis für ein einfaches Blogsystem. Das System enthält Posts und Tags, die in einer Viele-zu-Viele-Beziehung zueinander stehen.

Alles ist in Ordnung, bis wir eine Anforderung erhalten, Seiten zu unterstützen. Die Anforderung besagt auch, dass wir dies auch können sollten, da es möglich ist, eine Seite in WordPress zu taggen.

Daher gelten Tags jetzt nicht mehr nur für Beiträge, sie können auch für Seiten gelten. Infolgedessen reicht unsere einfache Zuordnung zwischen Tags und Posts nicht mehr aus. Stattdessen benötigen wir eine viele-zu-viele einseitige polymorphe Beziehung, wie die folgende:

  • Jeder Post ist ein Taggable und hat viele Tags
  • Jede Seite ist ein Taggable und hat viele Tags
  • Jedes Tag hat viele polymorphe Taggables

Der Übergang zu diesem neuen, komplexeren Assoziationssatz wird wahrscheinlich erhebliche Auswirkungen auf unseren gesamten Code haben, was zu einer großen Abwanderung führt. Da wir keine Ahnung haben, wie man eine polymorphe Assoziation zu JSON serialisiert, werden wir wahrscheinlich einfach mehr API-Endpunkte wie GET /posts/:id/tags und GET /pages/:id/tags . Und dann werfen wir alle unsere vorhandenen JSON-Parserfunktionen weg und schreiben neue für die neu hinzugefügten Ressourcen. Pfui. Mühsam und schmerzhaft.

Lassen Sie uns nun überlegen, wie wir dies mit Ember Data angehen würden.

In Ember Data würde die Anpassung an diesen modifizierten Satz von Assoziationen einfach den Wechsel von:

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

zu:

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

Die resultierende Abwanderung im Rest unseres Codes wäre minimal und wir könnten die meisten unserer Vorlagen wiederverwenden. Beachten Sie insbesondere, dass der Name der tags Zuordnung auf Post unverändert bleibt. Darüber hinaus stützt sich der Rest unserer Codebasis nur auf die Existenz der tags -Zuordnung und ist sich ihrer Details nicht bewusst.

Eine Ember Data Primer

Bevor wir in ein Beispiel aus der realen Welt eintauchen, wollen wir uns einige Grundlagen von Ember Data ansehen.

Routen und Modelle

In Ember.js ist der Router für das Anzeigen von Vorlagen, das Laden von Daten und das sonstige Einrichten des Anwendungsstatus verantwortlich. Der Router gleicht die aktuelle URL mit den von Ihnen definierten Routen ab, sodass eine Route für die Angabe des Modells verantwortlich ist, das eine Vorlage anzeigen soll (Ember erwartet, dass dieses Modell eine Unterklasse von Ember.Object ):

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

Ember Data stellt DS.Model das eine Unterklasse von Ember.Object ist und Funktionen wie das Speichern oder Aktualisieren eines einzelnen Datensatzes oder mehrerer Datensätze zur Vereinfachung hinzufügt.

Um ein neues Modell zu erstellen, erstellen wir eine Unterklasse von DS.Model (z. B. App.User = DS.Model.extend({}) ).

Ember Data erwartet eine klar definierte, intuitive JSON-Struktur vom Server und serialisiert neu erstellte Datensätze in dieselbe strukturierte JSON.

Ember Data bietet auch eine Reihe von Array-Klassen wie DS.RecordArray für die Arbeit mit Modellen. Diese haben Verantwortlichkeiten wie die Handhabung von Eins-zu-Vielen- oder Viele-zu-Viele-Beziehungen, die Handhabung des asynchronen Abrufs von Daten und so weiter.

Modellattribute

Grundlegende Modellattribute werden mit DS.attr definiert; z.B:

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

Nur von DS.attr erstellte Felder werden in die Nutzdaten aufgenommen, die zum Erstellen oder Aktualisieren von Datensätzen an den Server übergeben werden.

DS.attr akzeptiert vier Datentypen: string , number , boolean und date .

RESTSerializer

Standardmäßig:

  • Ember Data verwendet RESTSerializer zum Erstellen von Objekten aus API-Antworten (Deserialisierung) und zum Generieren von JSON für API-Anforderungen (Serialisierung).
  • RESTSerializer erwartet von DS.belongsTo erstellte Felder, dass ein Feld namens user in der JSON-Antwort vom Server enthalten ist. Dieses Feld enthält die ID des referenzierten Datensatzes.
  • RESTSerializer fügt der an die API übergebenen Nutzlast ein user mit der ID der zugehörigen Bestellung hinzu.

Eine Antwort könnte beispielsweise so aussehen:

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

Und eine von RESTSerializer erstellte HTTP-Anfrage zum Speichern einer Bestellung könnte so aussehen:

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

Eins-zu-eins-Beziehungen

Angenommen, jeder Benutzer hat ein eindeutiges Profil. Wir können diese Beziehung in Ember-Daten mithilfe von DS.belongsTo sowohl im Benutzer- als auch im Profil darstellen:

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

Wir können dann die Zuordnung mit user.get('profile') abrufen oder mit user.set('profile', aProfile) .

RESTSerializer erwartet, dass die ID des zugeordneten Modells für jedes Modell bereitgestellt wird; z.B:

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

In ähnlicher Weise enthält es die ID des zugehörigen Modells in einer Anforderungsnutzlast:

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

Eins-zu-viele- und Viele-zu-eins-Beziehungen

Angenommen, wir haben ein Modell, bei dem ein Beitrag viele Kommentare enthält. In Ember Data können wir diese Beziehung mit DS.hasMany('comment', {async: true}) bei Post und DS.belongsTo('post', {async: true}) bei Comment darstellen:

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

Wir können dann zugeordnete Elemente mit post.get('comments', {async: true}) abrufen und eine neue Zuordnung mit post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

Der Server antwortet dann mit einem Array von IDs für die entsprechenden Kommentare zu einem Beitrag:

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

… und mit einer ID für jeden Kommentar:

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

RESTSerializer fügt die ID des zugehörigen Beitrags zum Kommentar hinzu:

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

Beachten Sie jedoch, dass RESTSerializer standardmäßig keine mit DS.hasMany verknüpften IDs zu den Objekten hinzufügt, die es serialisiert, da diese Verknüpfungen auf der „Viele“-Seite angegeben sind (dh diejenigen, die eine DS.belongsTo Verknüpfung haben). Obwohl ein Beitrag in unserem Beispiel viele Kommentare enthält, werden diese IDs dem Beitragsobjekt nicht hinzugefügt:

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

Um auch die Serialisierung von DS.hasMany IDs zu „erzwingen“, können Sie das Embedded Records Mixin verwenden.

Viele-zu-viele-Beziehungen

Angenommen, in unserem Modell kann ein Autor mehrere Beiträge haben und ein Beitrag kann mehrere Autoren haben.

Um diese Beziehung in Ember Data darzustellen, können wir DS.hasMany('author', {async: true}) für Post und DS.hasMany('post', {async: true}) für Author verwenden:

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

Wir können dann verknüpfte Elemente mit author.get('posts') und eine neue Verknüpfung mit author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

Der Server antwortet dann mit einem Array von IDs für die entsprechenden Objekte; z.B:

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

Da es sich um eine Viele-zu-Viele-Beziehung handelt, fügt RESTSerializer ein Array von IDs zugeordneter Objekte hinzu; z.B:

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

Ein Beispiel aus der Praxis: Verbesserung eines bestehenden Bestellsystems

In unserem bestehenden Bestellsystem hat jeder Benutzer viele Bestellungen und jede Bestellung hat viele Artikel. Unser System verfügt über mehrere Anbieter (dh Anbieter), bei denen Produkte bestellt werden können, aber jede Bestellung kann nur Artikel von einem einzigen Anbieter enthalten.

Neue Anforderung Nr. 1: Aktivieren Sie eine einzelne Bestellung, um Artikel von mehreren Anbietern aufzunehmen.

In dem bestehenden System gibt es eine Eins-zu-Viele-Beziehung zwischen Anbietern und Aufträgen. Sobald wir jedoch eine Bestellung auf Artikel von mehreren Anbietern erweitern, reicht diese einfache Beziehung nicht mehr aus.

Insbesondere wenn ein Anbieter mit einer gesamten Bestellung verbunden ist, kann diese Bestellung in dem erweiterten System sehr gut auch Artikel enthalten, die bei anderen Anbietern bestellt wurden. Es muss daher eine Möglichkeit geben, anzugeben, welcher Teil jeder Bestellung für jeden Anbieter relevant ist. Darüber hinaus sollte ein Anbieter, wenn er auf seine Bestellungen zugreift, nur die bei ihm bestellten Artikel einsehen können, keine anderen Artikel, die der Kunde möglicherweise bei anderen Anbietern bestellt hat.

Ein Ansatz könnte darin bestehen, zwei neue Many-to-Many-Assoziationen einzuführen; eine zwischen Bestellung und Artikel und eine andere zwischen Bestellung und Anbieter.

Der Einfachheit halber führen wir jedoch ein neues Konstrukt in das Datenmodell ein, das wir als „ProviderOrder“ bezeichnen.

Beziehungen gestalten

Das erweiterte Datenmodell muss die folgenden Assoziationen berücksichtigen:

  • Eins-zu-viele- Beziehung zwischen Benutzern und Bestellungen (jeder Benutzer kann mit 0 bis n Bestellungen verknüpft sein) und eine Eins-zu-viele- Beziehung zwischen Benutzern und Anbietern (jeder Benutzer kann mit 0 bis n Anbietern verknüpft sein)

     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}) });
  • Eins-zu-viele- Beziehung zwischen Orders und ProviderOrders (jede Order besteht aus 1 bis n ProviderOrders):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • Eins-zu-viele- Beziehung zwischen Anbietern und Anbieteraufträgen (jedem Anbieter können 0 bis n Anbieteraufträge zugeordnet werden):

     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}) });
  • Eins-zu-viele- Beziehung zwischen ProviderOrders und Items (jede ProviderOrder besteht aus 1 bis n Items):

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

Und vergessen wir nicht unsere Routendefinition :

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

Jetzt hat jede ProviderOrder einen Provider, was unser Hauptziel war. Artikel werden von Order zu ProviderOrder verschoben, und es wird davon ausgegangen, dass alle Artikel in einem ProviderOrder zu einem einzigen Anbieter gehören.

Minimierung von Code-Churn

Leider gibt es hier einige Breaking Changes. Sehen wir uns also an, wie Ember Data uns helfen kann, die resultierende Codeänderung in unserer Codebasis zu minimieren.

Zuvor haben wir Elemente mit items.pushObject(item) . Jetzt müssen wir zuerst die entsprechende ProviderOrder finden und ein Item dorthin schieben:

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

Da dies viel Abwanderung bedeutet und mehr die Aufgabe von Order als die des Controllers ist, ist es besser, wenn wir diesen Code 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); }); }); } });

Jetzt können wir Artikel wie order.pushItem(item) direkt zur Bestellung hinzufügen.

Und zum Auflisten von Artikeln jeder Bestellung:

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

Polymorphe Beziehungen

Lassen Sie uns nun eine zusätzliche Verbesserungsanfrage in unser System einführen, die die Dinge noch komplizierter macht:

Neue Anforderung Nr. 2: Unterstützung mehrerer Arten von Anbietern.

Nehmen wir für unser einfaches Beispiel an, dass zwei Arten von Anbietern („Shop“ und „Buchhandlung“) definiert sind:

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

Hier wird sich die Unterstützung von Ember Data für polymorphe Beziehungen als nützlich erweisen. Ember Data unterstützt polymorphe Eins-zu-eins-, Eins-zu-viele- und Viele-zu-viele-Beziehungen. Dies geschieht einfach durch Hinzufügen des Attributs polymorphic: true zur Spezifikation der Assoziation. Zum Beispiel:

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

Das obige polymorphic Flag gibt an, dass es verschiedene Arten von Anbietern gibt, die mit einer Anbieterbestellung verknüpft werden können (in unserem Fall entweder ein Shop oder ein Buchladen).

Wenn eine Beziehung polymorph ist, sollte die Serverantwort sowohl die ID als auch den Typ des zurückgegebenen Objekts angeben (der RESTSerializer tut dies standardmäßig); z.B:

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

Den neuen Anforderungen gerecht werden

Wir brauchen polymorphe Provider und Items, um die Anforderungen zu erfüllen. Da ProviderOrder Anbieter mit Artikeln verbindet, können wir seine Assoziationen in polymorphe Assoziationen ändern:

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

Es gibt jedoch noch ein Problem: Provider hat eine nicht-polymorphe Assoziation zu Items, aber Item ist ein abstrakter Typ. Wir haben daher zwei Möglichkeiten, dem entgegenzuwirken:

  1. Fordern Sie, dass alle Anbieter demselben Artikeltyp zugeordnet sind (d. h. deklarieren Sie einen bestimmten Artikeltyp für die Zuordnung mit dem Anbieter).
  2. Deklarieren Sie die items für Provider als polymorph

In unserem Fall müssen wir uns für Option 2 entscheiden und die items Assoziation auf Provider als polymorph deklarieren:

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

Beachten Sie, dass dies keine Codeänderung einführt; Alle Assoziationen funktionieren einfach so wie vor dieser Änderung. Die Leistung von Ember Data in seiner besten Form!

Kann Ember Data wirklich alle meine Daten modellieren?

Es gibt natürlich Ausnahmen, aber ich betrachte ActiveRecord-Konventionen als eine standardmäßige und flexible Methode zum Strukturieren und Modellieren von Daten. Lassen Sie mich Ihnen also zeigen, wie ActiveRecord-Konventionen auf Ember Data abgebildet werden:

has_many :users through: :ownerships oder Representing Intermediate Models

Dadurch wird ein Pivot -Modell namens Ownership konsultiert, um zugeordnete Benutzer zu finden. Wenn das Pivot -Modell im Grunde eine Pivot -Tabelle ist, können Sie die Erstellung eines Zwischenmodells in Ember Data vermeiden und die Beziehung mit DS.hasMany auf beiden Seiten darstellen.

Wenn Sie jedoch diese Pivot-Beziehung in Ihrem Front-End benötigen, richten Sie ein Eigentumsmodell ein, das DS.belongsTo('user', {async: true}) und DS.belongsTo('provider', {async: true}) . und fügen Sie dann sowohl Benutzern als auch Anbietern eine Eigenschaft hinzu, die der Zuordnung mithilfe von Ownership zugeordnet ist; z.B:

 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, als: lokalisierbar

In unserem ActiveRecord-Objekt haben wir eine typische polymorphe Beziehung:

 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

Dies ist eine viele (polymorphe) zu vielen (normale nicht-polymorphe) Beziehung. In Ember Data können wir dies mit einem polymorphen DS.hasMany('locatable', {polymorphic: true, async: true}) und einem statischen 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}) });

Für Locatables wie Benutzer sollte der Server IDs für die zugehörigen Standorte zurückgeben:

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

Für Location sollte der Server sowohl die ID als auch den Typ von Locatable im Array von Objekten zurückgeben:

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

Außerdem können Sie Beziehungen nach Typ mit einer statischen Viele-zu-Viele-Beziehung darstellen:

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

Und was ist mit Echtzeitdaten?

Ember Data hat push , pushPayload und update . Sie können neue/aktualisierte Datensätze jederzeit manuell in den lokalen Cache von Ember Data (Speicher genannt) verschieben, und er erledigt den Rest.

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

Ich persönlich nutze Sockets nur für Events mit sehr kleinen Payloads. Ein typisches Ereignis ist „recordUpdated“ mit der Nutzlast {"type": "shop", "id": "14"} und dann überprüfe ich in ApplicationRoute, ob sich dieser Datensatz im lokalen Cache (Speicher) befindet und ob es sich um I handelt werde es einfach nachholen.

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

Auf diese Weise können wir aktualisierte Ereignisse ohne inakzeptablen Overhead an alle Clients senden.

Es gibt in Ember Data im Wesentlichen zwei Ansätze für den Umgang mit Echtzeitdaten:

  1. Schreiben Sie einen Adapter für Ihren Echtzeit-Kommunikationskanal und verwenden Sie ihn anstelle von RESTAdapter.
  2. Übertragen Sie Datensätze an den Hauptspeicher, wann immer sie verfügbar sind.

Der Nachteil bei der ersten Option ist, dass es so etwas wie die Neuerfindung des Rades ist. Für die zweite Option müssen wir auf den Hauptspeicher zugreifen, der auf allen Routen als route#store verfügbar ist.

Einpacken

In diesem Artikel haben wir Ihnen die wichtigsten Konstrukte und Paradigmen von Ember Data vorgestellt und den Wert demonstriert, den es Ihnen als Entwickler bieten kann. Ember Data bietet einen flexibleren und optimierten Entwicklungsworkflow und minimiert die Codeänderung als Reaktion auf Änderungen, die ansonsten schwerwiegende Auswirkungen hätten.

Die Vorabinvestition (Zeit und Lernkurve), die Sie in die Verwendung von Ember Data für Ihr Projekt tätigen, wird sich zweifellos als lohnend erweisen, da sich Ihr System zwangsläufig weiterentwickelt und erweitert, modifiziert und verbessert werden muss.


ANHANG: Fortgeschrittene Ember-Datenthemen

Dieser Anhang stellt eine Reihe von fortgeschritteneren Ember Data-Themen vor, darunter:

  • Embers modulares Design
  • Seite lädt
  • Verknüpfungen
  • Serialisierer und Adapter für aktive Modelle
  • Eingebettete Aufzeichnungen Mixin
  • Zuordnungsmodifikatoren (asynchron, invers und polymorph)
  • 'ids'-Parameter in GET-Anforderungen

Modulares Design

Ember Data hat ein modulares Design unter der Haube. Zu den Schlüsselkomponenten gehören:

  1. Adapters sind für die Abwicklung der Kommunikation zuständig, derzeit nur REST über HTTP.
  2. Serializers verwalten die Erstellung von Modellen aus JSON oder umgekehrt.
  3. Store Sie Caches erstellte Aufzeichnungen.
  4. Container klebt all dies zusammen.

Zu den Vorteilen dieses Designs gehören:

  1. Die Deserialisierung und Speicherung der Daten funktioniert unabhängig vom verwendeten Kommunikationskanal und der angeforderten Ressource.
  2. Funktionierende Konfigurationen wie ActiveModelSerializer oder EmbeddedRecordsMixin werden standardmäßig bereitgestellt.
  3. Datenquellen (z. B. LocalStorage, CouchDB-Implementierung usw.) können durch Adapterwechsel ein- und ausgelagert werden.
  4. Trotz vieler Konventionen über die Konfiguration ist es möglich, alles zu konfigurieren und Ihre Konfiguration/Implementierung mit der Community zu teilen.

Seite lädt

Ember Data unterstützt das „Sideloading“ von Daten; dh Angabe von Zusatzdaten, die (zusammen mit den angeforderten Primärdaten) abgerufen werden sollten, um dabei zu helfen, mehrere verwandte HTTP-Anforderungen zu konsolidieren.

Ein häufiger Anwendungsfall ist das Querladen von verknüpften Modellen. Beispielsweise hat jeder Shop viele Lebensmittel, sodass wir alle zugehörigen Lebensmittel in die /shops Antwort aufnehmen können:

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

Wenn auf den Lebensmittelverband zugegriffen wird, gibt Ember Data Folgendes aus:

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

Wenn wir jedoch stattdessen zugehörige Lebensmittel im /shops -Endpunkt zurückgeben, muss Ember Data keine weitere Anfrage stellen:

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

Verknüpfungen

Ember Data akzeptiert Links anstelle von Zuordnungs-IDs. Wenn auf eine als Link angegebene Assoziation zugegriffen wird, sendet Ember Data eine GET-Anforderung an diesen Link, um die zugehörigen Datensätze abzurufen.

Beispielsweise könnten wir einen Link für einen Lebensmittelverein zurückgeben:

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

Und Ember Data würde dann eine Anfrage an /shops/14/groceries stellen

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

Denken Sie daran, dass Sie die Assoziation immer noch in den Daten darstellen müssen; Links schlagen lediglich eine neue HTTP-Anforderung vor und wirken sich nicht auf die Zuordnungen aus.

Serialisierer und Adapter für aktive Modelle

Wahrscheinlich werden ActiveModelSerializer und ActiveModelAdapter in der Praxis häufiger verwendet als RESTSerializer und RESTAdapter . Insbesondere wenn das Backend Ruby on Rails und das ActiveModel::Serializers -Gem verwendet, ist die beste Option die Verwendung von ActiveModelSerializer und ActiveModelAdapter , da sie ActiveModel::Serializers unterstützen.

Glücklicherweise sind die Nutzungsunterschiede zwischen ActiveModelSerializer / ActiveModelAdapter und RESTSerializer / RESTAdapter jedoch ziemlich begrenzt; nämlich:

  1. ActiveModelSerializer verwendet snake_case-Feldnamen, während RESTSerializer camelCased-Feldnamen erfordert.
  2. ActiveModelAdapter gibt Anforderungen an snake_case-API-Methoden aus, während RESTSerializer an camelCased-API-Methoden ausgibt.
  3. ActiveModelSerializer erwartet, dass zuordnungsbezogene Feldnamen mit _id oder _ids während RESTSerializer erwartet, dass zuordnungsbezogene Feldnamen mit dem Zuordnungsfeld identisch sind.

Unabhängig von der Wahl des Adapters und des Serializers sind Ember Data-Modelle genau gleich. Nur die JSON-Darstellung und die API-Endpunkte sind unterschiedlich.

Nehmen Sie als Beispiel unseren finalen ProviderOrder:

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

Mit Active Model Serializer und Adapter sollte der Server Folgendes erwarten:

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

… und sollte antworten mit:

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

Eingebettete Aufzeichnungen Mixin

DS.EmbeddedRecordsMixin ist eine Erweiterung für DS.ActiveModelSerializer , mit der konfiguriert werden kann, wie Zuordnungen serialisiert oder deserialisiert werden. Obwohl noch nicht vollständig (insbesondere im Hinblick auf polymorphe Assoziationen), ist es dennoch faszinierend.

Du kannst wählen:

  1. Assoziationen nicht serialisieren oder deserialisieren.
  2. Zum Serialisieren oder Deserialisieren von Zuordnungen mit ID oder IDs.
  3. Zum Serialisieren oder Deserialisieren von Zuordnungen mit eingebetteten Modellen.

Dies ist besonders nützlich in 1:n-Beziehungen, in denen DS.hasMany zugehörige IDs standardmäßig nicht zu den serialisierten Objekten hinzugefügt werden. Nehmen Sie als Beispiel einen Einkaufswagen mit vielen Artikeln. In diesem Beispiel wird der Warenkorb erstellt, während die Artikel bekannt sind. Wenn Sie jedoch den Warenkorb speichern, fügt Ember Data die IDs der zugehörigen Artikel nicht automatisch in die Anforderungsnutzlast ein.

Mit DS.EmbeddedRecordsMixin ist es jedoch möglich, Ember Data anzuweisen, die Artikel-IDs auf Cart wie folgt zu serialisieren:

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

Wie im obigen Beispiel gezeigt, ermöglicht EmbeddedRecordsMixin die explizite Angabe, welche Assoziationen über das attrs Objekt serialisiert und/oder deserialisiert werden sollen. Gültige Werte für serialize und deserialize sind: - 'no' : Zuordnung nicht in serialisierte/deserialisierte Daten einschließen - 'id' oder 'ids' : zugeordnete ID(s) in serialisierte/deserialisierte Daten einschließen - 'records ': tatsächliche Eigenschaften einschließen (dh Feldwerte aufzeichnen) als Array in serialisierten/deserialisierten Daten

Zuordnungsmodifikatoren (asynchron, invers und polymorph)

Die folgenden Zuordnungsmodifikatoren werden unterstützt: polymorphic , inverse und async

Polymorpher Modifikator

In einer polymorphen Assoziation repräsentieren eine oder beide Seiten der Assoziation eher eine Klasse von Objekten als ein spezifisches Objekt.

Erinnern Sie sich an unser früheres Beispiel eines Blogs, in dem wir die Fähigkeit unterstützen mussten, sowohl Beiträge als auch Seiten zu taggen. Um dies zu unterstützen, sind wir zu folgendem Modell gekommen:

  • Jeder Post ist ein Taggable und hat viele Tags
  • Jede Seite ist ein Taggable und hat viele Tags
  • Jedes Tag hat viele polymorphe Taggables

Nach diesem Modell kann ein polymorphic Modifikator verwendet werden, um zu deklarieren, dass Tags mit jeder Art von „Taggable“ (bei dem es sich entweder um einen Beitrag oder eine Seite handeln kann) wie folgt verbunden sind:

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

Inverser Modifikator

Normalerweise sind Assoziationen bidirektional. Beispielsweise wäre „Beitrag hat viele Kommentare“ eine Richtung einer Assoziation, während „Kommentar gehört zu einem Beitrag“ die andere (dh „umgekehrte“) Richtung dieser Assoziation wäre.

In Fällen, in denen die Assoziation nicht mehrdeutig ist, muss nur eine Richtung angegeben werden, da Ember Data den inversen Teil der Assoziation ableiten kann.

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; z.B:

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