Ember Data : un didacticiel complet pour la bibliothèque ember-data

Publié: 2022-03-11

Ember Data (alias ember-data ou ember.data) est une bibliothèque permettant de gérer de manière robuste les données de modèle dans les applications Ember.js. Les développeurs d'Ember Data déclarent qu'il est conçu pour être indépendant du mécanisme de persistance sous-jacent, il fonctionne donc aussi bien avec les API JSON sur HTTP qu'avec le streaming WebSockets ou le stockage IndexedDB local. Il fournit de nombreuses fonctionnalités que vous trouverez dans les mappages relationnels d'objets (ORM) côté serveur comme ActiveRecord, mais il est conçu spécifiquement pour l'environnement unique de JavaScript dans le navigateur.

Bien qu'Ember Data puisse prendre un certain temps à se développer, une fois que vous l'aurez fait, vous constaterez probablement que l'investissement en valait la peine. Cela facilitera en fin de compte le développement, l'amélioration et la maintenance de votre système.

Lorsqu'une API est représentée à l'aide de modèles, d'adaptateurs et de sérialiseurs Ember Data, chaque association devient simplement un nom de champ. Cela encapsule les détails internes de chaque association, isolant ainsi le reste de votre code des modifications apportées aux associations elles-mêmes. Le reste de votre code ne se souciera pas, par exemple, si une association particulière est polymorphe ou est le résultat d'une carte de plusieurs associations.

De plus, votre base de code est largement isolée des modifications du backend, même si elles sont importantes, puisque tout ce que votre base de code attend, ce sont des champs et des fonctions sur les modèles, et non une représentation JSON ou XML ou YAML du modèle.

Dans ce didacticiel, nous présenterons les fonctionnalités les plus importantes d'Ember Data et démontrerons comment cela aide à minimiser l'attrition du code, en nous concentrant sur un exemple concret.

Une annexe est également fournie qui traite d'un certain nombre de sujets et d'exemples Ember Data plus avancés.

Remarque : cet article suppose une certaine connaissance de base d'Ember.js. Si vous n'êtes pas familier avec Ember.js, consultez notre populaire tutoriel Ember.js pour une introduction. Nous avons également un guide JavaScript complet disponible en russe, portugais et espagnol.

La proposition de valeur des données Ember

Un exemple de la façon dont la bibliothèque Ember Data peut aider à répondre aux besoins des clients.

Commençons par considérer un exemple simple.

Supposons que nous ayons une base de code fonctionnelle pour un système de blog de base. Le système contient des publications et des balises, qui ont une relation plusieurs à plusieurs les unes avec les autres.

Tout va bien jusqu'à ce que nous obtenions une exigence de prise en charge des pages. L'exigence stipule également que, puisqu'il est possible de baliser une page dans WordPress, nous devrions également pouvoir le faire.

Désormais, les balises ne s'appliqueront plus uniquement aux publications, elles peuvent également s'appliquer aux pages. De ce fait, notre simple association entre Tags et Posts ne suffira plus. Au lieu de cela, nous aurons besoin d'une relation polymorphe unilatérale plusieurs à plusieurs, telle que la suivante :

  • Chaque message est un taggable et a de nombreuses balises
  • Chaque page est un taggable et a de nombreuses balises
  • Chaque Tag a de nombreux Taggables polymorphes

La transition vers ce nouvel ensemble d'associations plus complexe est susceptible d'avoir des ramifications importantes dans notre code, ce qui entraînera de nombreux désabonnements. Comme nous ne savons pas comment sérialiser une association polymorphe en JSON, nous allons probablement créer plus de points de terminaison d'API comme GET /posts/:id/tags et GET /pages/:id/tags . Et puis, nous allons jeter toutes nos fonctions d'analyseur JSON existantes et en écrire de nouvelles pour les nouvelles ressources ajoutées. Pouah. Ennuyeux et douloureux.

Voyons maintenant comment nous aborderions cela en utilisant Ember Data.

Dans Ember Data, la prise en compte de cet ensemble modifié d'associations impliquerait simplement de passer de :

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

pour:

 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'attrition résultante dans le reste de notre code serait minime et nous serions en mesure de réutiliser la plupart de nos modèles. Notez en particulier que le nom de l'association des tags sur Post reste inchangé. De plus, le reste de notre base de code repose uniquement sur l'existence de l'association de tags et ignore ses détails.

Une introduction aux données Ember

Avant de plonger dans un exemple concret, passons en revue certains principes fondamentaux d'Ember Data.

Itinéraires et modèles

Dans Ember.js, le routeur est responsable de l'affichage des modèles, du chargement des données et de la configuration de l'état de l'application. Le routeur fait correspondre l'URL actuelle aux routes que vous avez définies, donc une Route est chargée de spécifier le modèle qu'un modèle doit afficher (Ember s'attend à ce que ce modèle soit une sous-classe de Ember.Object ) :

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

Ember Data fournit DS.Model qui est une sous-classe d' Ember.Object et ajoute des fonctionnalités telles que la sauvegarde ou la mise à jour d'un seul enregistrement ou de plusieurs enregistrements pour plus de commodité.

Pour créer un nouveau modèle, nous créons une sous-classe de DS.Model (par exemple, App.User = DS.Model.extend({}) ).

Ember Data attend une structure JSON bien définie et intuitive de la part du serveur et sérialise les enregistrements nouvellement créés dans le même JSON structuré.

Ember Data fournit également une suite de classes de tableaux comme DS.RecordArray pour travailler avec des modèles. Ceux-ci ont des responsabilités telles que la gestion des relations un-à-plusieurs ou plusieurs-à-plusieurs, la gestion de la récupération asynchrone des données, etc.

Attributs du modèle

Les attributs de base du modèle sont définis à l'aide DS.attr ; par exemple:

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

Seuls les champs créés par DS.attr seront inclus dans la charge utile transmise au serveur pour la création ou la mise à jour des enregistrements.

DS.attr accepte quatre types de données : string , number , boolean et date .

Sérialiseur REST

Par défaut:

  • Ember Data utilise RESTSerializer pour créer des objets à partir des réponses API (désérialisation) et pour générer du JSON pour les requêtes API (sérialisation).
  • RESTSerializer s'attend à ce que les champs créés par DS.belongsTo aient un champ nommé user inclus dans la réponse JSON du serveur. Ce champ contient l'identifiant de l'enregistrement référencé.
  • RESTSerializer ajoute un champ user à la charge utile transmise à l'API avec l'identifiant de la commande associée.

Une réponse pourrait, par exemple, ressembler à ceci :

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

Et une requête HTTP créée par RESTSerializer pour enregistrer une commande pourrait ressembler à ceci :

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

Relations individuelles

Supposons, par exemple, que chaque utilisateur possède un profil unique. Nous pouvons représenter cette relation dans Ember Data en utilisant DS.belongsTo à la fois sur l'utilisateur et sur le profil :

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

Nous pouvons ensuite obtenir l'association avec user.get('profile') ou la définir avec user.set('profile', aProfile) .

RESTSerializer s'attend à ce que l'ID du modèle associé soit fourni pour chaque modèle ; par exemple:

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

De même, il inclut l'ID du modèle associé dans une charge utile de requête :

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

Relations un-à-plusieurs et plusieurs-à-un

Supposons que nous ayons un modèle dans lequel une publication contient de nombreux commentaires. Dans Ember Data, nous pouvons représenter cette relation avec DS.hasMany('comment', {async: true}) sur Post et DS.belongsTo('post', {async: true}) sur 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}) });

Nous pouvons ensuite obtenir les éléments associés avec post.get('comments', {async: true}) et ajouter une nouvelle association avec post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

Le serveur répondra alors avec un tableau d'ID pour les commentaires correspondants sur un article :

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

… et avec un ID pour chaque Commentaire :

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

RESTSerializer ajoute l'identifiant de la publication associée au commentaire :

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

Notez cependant que, par défaut, RESTSerializer n'ajoutera pas les ID associés à DS.hasMany aux objets qu'il sérialise, puisque ces associations sont spécifiées du côté "plusieurs" (c'est-à-dire celles qui ont une association DS.belongsTo ). Ainsi, dans notre exemple, bien qu'un article contienne de nombreux commentaires, ces identifiants ne seront pas ajoutés à l'objet Article :

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

Pour "forcer" la sérialisation des ID DS.hasMany également, vous pouvez utiliser le mixin Embedded Records.

Relations plusieurs à plusieurs

Supposons que dans notre modèle, un auteur peut avoir plusieurs publications et qu'une publication peut avoir plusieurs auteurs.

Pour représenter cette relation dans Ember Data, nous pouvons utiliser DS.hasMany('author', {async: true}) sur Post et DS.hasMany('post', {async: true}) sur 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}) });

Nous pouvons ensuite obtenir les éléments associés avec author.get('posts') et ajouter une nouvelle association avec author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

Le serveur répondra alors avec un tableau d'ID pour les objets correspondants ; par exemple:

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

Puisqu'il s'agit d'une relation plusieurs à plusieurs, RESTSerializer ajoute un tableau d'ID d'objets associés ; par exemple:

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

Un exemple concret : améliorer un système de commande existant

Dans notre système de commande existant, chaque Utilisateur a de nombreuses Commandes et chaque Commande a de nombreux Articles. Notre système compte plusieurs fournisseurs (c'est-à-dire des fournisseurs) auprès desquels des produits peuvent être commandés, mais chaque commande ne peut contenir que des articles provenant d'un seul fournisseur.

Nouvelle exigence n° 1 : permettre à une seule commande d'inclure des articles provenant de plusieurs fournisseurs.

Dans le système existant, il existe une relation un-à-plusieurs entre les fournisseurs et les commandes. Cependant, une fois que nous avons étendu une commande pour inclure des articles de plusieurs fournisseurs, cette simple relation ne sera plus adéquate.

Plus précisément, si un fournisseur est associé à une commande entière, dans le système amélioré, cette commande peut très bien inclure également des articles commandés auprès d'autres fournisseurs. Il doit donc y avoir un moyen d'indiquer quelle partie de chaque commande est pertinente pour chaque fournisseur. De plus, lorsqu'un fournisseur accède à ses commandes, il ne devrait avoir une visibilité que sur les articles commandés auprès d'eux, et non sur les autres articles que le client aurait pu commander auprès d'autres fournisseurs.

Une approche pourrait consister à introduire deux nouvelles associations plusieurs-à-plusieurs ; un entre la commande et l'article et un autre entre la commande et le fournisseur.

Cependant, pour simplifier les choses, nous introduisons une nouvelle construction dans le modèle de données que nous appelons "ProviderOrder".

Rédaction de relations

Le modèle de données amélioré devra tenir compte des associations suivantes :

  • Relation un-à-plusieurs entre Utilisateurs et Commandes (chaque Utilisateur peut être associé à 0 à n Commandes) et une relation Un-à-plusieurs entre Utilisateurs et Fournisseurs (chaque Utilisateur peut être associé à 0 à n Fournisseurs)

     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}) });
  • Relation un-à-plusieurs entre Orders et ProviderOrders (chaque commande se compose de 1 à n ProviderOrders) :

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • Relation un-à-plusieurs entre les Providers et les ProviderOrders (chaque Provider peut être associé à 0 à 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}) });
  • Relation un-à-plusieurs entre les ProviderOrders et les Items (chaque ProviderOrder se compose de 1 à 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}) });

Et n'oublions pas notre définition de Route :

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

Désormais, chaque ProviderOrder a un fournisseur, ce qui était notre objectif principal. Les éléments sont déplacés de Order vers ProviderOrder et l'hypothèse est que tous les éléments d'un ProviderOrder appartiennent à un seul fournisseur.

Minimiser l'attrition du code

Malheureusement, il y a quelques changements de rupture ici. Voyons donc comment Ember Data peut nous aider à minimiser tout roulement de code résultant dans notre base de code.

Auparavant, nous poussions des éléments avec items.pushObject(item) . Maintenant, nous devons d'abord trouver le ProviderOrder approprié et lui envoyer un Item :

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

Comme il s'agit de beaucoup de désabonnement et que le travail de Order est plus important que celui du contrôleur, il est préférable de déplacer ce code dans 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); }); }); } });

Maintenant, nous pouvons ajouter des articles directement sur la commande comme order.pushItem(item) .

Et pour répertorier les articles de chaque commande :

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

Relations polymorphes

Introduisons maintenant une demande d'amélioration supplémentaire à notre système qui complique encore les choses :

Nouvelle exigence n° 2 : prendre en charge plusieurs types de fournisseurs.

Pour notre exemple simple, disons que deux types de Prestataires (« Boutique » et « Librairie ») sont définis :

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

C'est ici que la prise en charge par Ember Data des relations polymorphes sera utile. Ember Data prend en charge les relations polymorphes un-à-un, un-à-plusieurs et plusieurs-à-plusieurs. Cela se fait simplement en ajoutant l'attribut polymorphic: true à la spécification de l'association. Par exemple:

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

L'indicateur polymorphic ci-dessus indique qu'il existe différents types de fournisseurs pouvant être associés à un ProviderOrder (dans notre cas, soit une boutique, soit une librairie).

Lorsqu'une relation est polymorphe, la réponse du serveur doit indiquer à la fois l'ID et le type de l'objet renvoyé (le RESTSerializer le fait par défaut) ; par exemple:

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

Répondre aux nouvelles exigences

Nous avons besoin de fournisseurs et d'éléments polymorphes pour répondre aux exigences. Étant donné que ProviderOrder connecte les fournisseurs aux éléments, nous pouvons modifier ses associations en associations polymorphes :

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

Il reste cependant un problème : Provider a une association non polymorphe avec Items, mais Item est un type abstrait. Nous avons donc deux options pour y remédier :

  1. Exiger que tous les fournisseurs soient associés au même type d'élément (c'est-à-dire déclarer un type d'élément spécifique pour l'association avec le fournisseur)
  2. Déclarer l'association d' items sur le fournisseur comme polymorphe

Dans notre cas, nous devons utiliser l'option 2 et déclarer l'association d' items sur le fournisseur comme polymorphe :

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

Notez que cela n'introduit aucune rotation de code ; toutes les associations fonctionnent simplement comme elles le faisaient avant ce changement. La puissance d'Ember Data à son meilleur !

Ember Data peut-il vraiment modéliser toutes mes données ?

Il y a bien sûr des exceptions, mais je considère les conventions ActiveRecord comme un moyen standard et flexible de structurer et de modéliser les données, alors laissez-moi vous montrer comment les conventions ActiveRecord correspondent aux données Ember :

has_many :users via : :ownerships ou représentant des modèles intermédiaires

Cela consultera un modèle pivot appelé Propriété pour trouver les utilisateurs associés. Si le modèle croisé est essentiellement un tableau croisé dynamique, vous pouvez éviter de créer un modèle intermédiaire dans Ember Data et représenter la relation avec DS.hasMany des deux côtés.

Cependant, si vous avez besoin de cette relation pivot dans votre front-end, configurez un modèle de propriété qui inclut DS.belongsTo('user', {async: true}) et DS.belongsTo('provider', {async: true}) , puis ajoutez une propriété sur les utilisateurs et les fournisseurs qui correspond à l'association à l'aide de la propriété ; par exemple:

 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 : mappages, as : localisable

Dans notre objet ActiveRecord, nous avons une relation polymorphe typique :

 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

Il s'agit d'une relation plusieurs (polymorphe) à plusieurs (normale non polymorphe). Dans Ember Data, nous pouvons exprimer cela avec un DS.hasMany('locatable', {polymorphic: true, async: true}) et un DS.hasMany('location', {async: true}) statique :

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

Pour Locatables, comme User, le serveur doit renvoyer les ID des emplacements associés :

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

Pour Location, le serveur doit renvoyer à la fois l'ID et le type de Locatable dans le tableau d'objets :

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

Vous pouvez également représenter les relations par type avec une relation plusieurs à plusieurs statique :

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

Et qu'en est-il des données en temps réel ?

Ember Data a push , pushPayload et update . Vous pouvez toujours pousser manuellement les enregistrements nouveaux/mis à jour dans le cache local d'Ember Data (appelé magasin) et il se chargera de tout le reste.

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

Personnellement, je n'utilise les sockets que pour les événements avec de très petites charges utiles. Un événement typique est 'recordUpdated' avec une charge utile de {"type": "shop", "id": "14"} , puis dans ApplicationRoute, je vérifierai si cet enregistrement est dans le cache local (magasin) et si c'est I Je vais juste le récupérer.

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

De cette façon, nous pouvons envoyer des événements d'enregistrement mis à jour à tous les clients sans surcharge inacceptable.

Il existe essentiellement deux approches dans Ember Data pour traiter les données en temps réel :

  1. Écrivez un adaptateur pour votre canal de communication en temps réel et utilisez-le à la place de RESTAdapter.
  2. Poussez les enregistrements vers le magasin principal dès qu'ils sont disponibles.

L'inconvénient de la première option est qu'elle revient un peu à réinventer la roue. Pour la deuxième option, nous devons accéder au magasin principal, qui est disponible sur toutes les routes en tant que route#store .

Emballer

Dans cet article, nous vous avons présenté les principaux concepts et paradigmes d'Ember Data, démontrant la valeur qu'il peut vous apporter en tant que développeur. Ember Data fournit un flux de travail de développement plus flexible et rationalisé, minimisant l'attrition du code en réponse à ce qui serait autrement des changements à fort impact.

L'investissement initial (temps et courbe d'apprentissage) que vous faites en utilisant Ember Data pour votre projet s'avérera sans aucun doute utile car votre système évolue inévitablement et doit être étendu, modifié et amélioré.


ANNEXE : Rubriques de données Ember avancées

Cette annexe présente un certain nombre de sujets Ember Data plus avancés, notamment :

  • Conception modulaire d'Ember
  • Chargement latéral
  • Liens
  • Sérialiseur et adaptateur de modèle actif
  • Mixage d'enregistrements intégrés
  • Modificateurs d'association (asynchrone, inverse et polymorphe)
  • Paramètre 'ids' dans les requêtes GET

Conception modulaire

Ember Data a une conception modulaire sous le capot. Les composants clés incluent :

  1. Les Adapters sont responsables de la gestion des communications, actuellement uniquement REST sur HTTP.
  2. Serializers gèrent la création de modèles à partir de JSON ou l'inverse.
  3. Les Store de stockage ont créé des enregistrements.
  4. Container colle tout cela ensemble.

Les avantages de cette conception incluent :

  1. La désérialisation et le stockage des données fonctionnent indépendamment du canal de communication utilisé et de la ressource demandée.
  2. Les configurations de travail, telles que ActiveModelSerializer ou EmbeddedRecordsMixin, sont fournies prêtes à l'emploi.
  3. Les sources de données (par exemple, LocalStorage, l'implémentation de CouchDB, etc.) peuvent être échangées en changeant d'adaptateur.
  4. Malgré de nombreuses conventions sur la configuration, il est possible de tout configurer et de partager votre configuration/implémentation avec la communauté.

Chargement latéral

Ember Data prend en charge le « chargement latéral » des données ; c'est-à-dire, indiquant les données auxiliaires qui doivent être récupérées (avec les données primaires demandées) afin d'aider à consolider plusieurs requêtes HTTP connexes.

Un cas d'utilisation courant est le chargement indépendant des modèles associés. Par exemple, chaque magasin a de nombreux produits d'épicerie, nous pouvons donc inclure tous les produits d'épicerie associés dans la réponse /shops :

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

Lors de l'accès à l'association des épiceries, Ember Data émettra :

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

Cependant, si nous renvoyons à la place le point de terminaison Groceries associé dans /shops , Ember Data n'aura pas besoin d'émettre une autre requête :

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

Liens

Ember Data accepte les liens à la place des identifiants d'association. Lorsqu'une association spécifiée en tant que lien est accessible, Ember Data envoie une requête GET à ce lien afin d'obtenir les enregistrements associés.

Par exemple, nous pourrions renvoyer un lien pour une association d'épicerie :

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

Et Ember Data enverrait alors une requête à /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" } ] }

Gardez à l'esprit que vous devez toujours représenter l'association dans les données ; les liens suggèrent simplement une nouvelle requête HTTP et n'affecteront pas les associations.

Sérialiseur et adaptateur de modèle actif

On peut dire que ActiveModelSerializer et ActiveModelAdapter sont plus utilisés dans la pratique que RESTSerializer et RESTAdapter . En particulier, lorsque le backend utilise Ruby on Rails et la gemme ActiveModel::Serializers , la meilleure option consiste à utiliser ActiveModelSerializer et ActiveModelAdapter , car ils prennent en charge ActiveModel::Serializers à l'emploi.

Heureusement, cependant, les différences d'utilisation entre ActiveModelSerializer / ActiveModelAdapter et RESTSerializer / RESTAdapter sont assez limitées ; à savoir:

  1. ActiveModelSerializer utilisera les noms de champ snake_case tandis que RESTSerializer nécessite des noms de champ camelCased.
  2. ActiveModelAdapter émet des requêtes vers les méthodes de l'API snake_case tandis que RESTSerializer émet vers les méthodes de l'API camelCased.
  3. ActiveModelSerializer s'attend à ce que les noms de champ liés à l'association se terminent par _id ou _ids tandis que RESTSerializer s'attend à ce que les noms de champ liés à l'association soient identiques au champ d'association.

Quel que soit le choix de l'adaptateur et du sérialiseur, les modèles Ember Data seront exactement les mêmes. Seuls la représentation JSON et les points de terminaison de l'API seront différents.

Prenons notre dernier ProviderOrder comme exemple :

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

Avec Active Model Serializer and Adapter, le serveur doit s'attendre à :

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

… et devrait répondre par :

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

Mixage d'enregistrements intégrés

DS.EmbeddedRecordsMixin est une extension pour DS.ActiveModelSerializer qui permet de configurer la façon dont les associations sont sérialisées ou désérialisées. Bien qu'elle ne soit pas encore complète (notamment en ce qui concerne les associations polymorphes), elle n'en demeure pas moins intrigante.

Tu peux choisir:

  1. Ne pas sérialiser ou désérialiser les associations.
  2. Pour sérialiser ou désérialiser les associations avec id ou ids.
  3. Pour sérialiser ou désérialiser des associations avec des modèles intégrés.

Ceci est particulièrement utile dans les relations un-à-plusieurs où, par défaut, les ID associés DS.hasMany ne sont pas ajoutés aux objets qui sont sérialisés. Prenons l'exemple d'un panier contenant de nombreux articles. Dans cet exemple, Cart est créé alors que les éléments sont connus. Cependant, lorsque vous enregistrez le panier, Ember Data ne mettra pas automatiquement les identifiants des articles associés sur la charge utile de la demande.

En utilisant DS.EmbeddedRecordsMixin , cependant, il est possible de dire à Ember Data de sérialiser les ID d'articles sur le panier comme suit :

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

Comme illustré dans l'exemple ci-dessus, EmbeddedRecordsMixin permet de spécifier explicitement les associations à sérialiser et/ou à désérialiser via l'objet attrs . Les valeurs valides pour serialize et deserialize sont : - 'no' : n'inclut pas l'association dans les données sérialisées/désérialisées - 'id' ou 'ids' : inclut les identifiants associés dans les données sérialisées/désérialisées - 'records ' : inclut les propriétés réelles (c'est-à-dire, enregistrer les valeurs de champ) sous forme de tableau dans des données sérialisées/désérialisées

Modificateurs d'association (asynchrone, inverse et polymorphe)

Les modificateurs d'association suivants sont pris en charge : polymorphic , inverse et async

Modificateur polymorphe

Dans une association polymorphe, un ou les deux côtés de l'association représentent une classe d'objets, plutôt qu'un objet spécifique.

Rappelez-vous notre exemple précédent d'un blog où nous devions prendre en charge la possibilité de baliser à la fois les articles et les pages. Pour soutenir cela, nous étions arrivés au modèle suivant :

  • Chaque message est un taggable et a de nombreuses balises
  • Chaque page est un taggable et a de nombreuses balises
  • Chaque Tag a de nombreux Taggables polymorphes

Suivant ce modèle, un modificateur polymorphic peut être utilisé pour déclarer que les balises sont liées à tout type de « taggable » (qui peut être soit une publication, soit une page), comme suit :

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

Modificateur inverse

Généralement, les associations sont bidirectionnelles. Par exemple, "La publication a de nombreux commentaires" serait une direction d'une association, tandis que "Le commentaire appartient à une publication" serait l'autre direction (c'est-à-dire "inverse") de cette association.

Dans les cas où il n'y a pas d'ambiguïté dans l'association, une seule direction doit être spécifiée car Ember Data peut déduire la partie inverse de l'association.

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; par exemple:

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