Ember Data: un tutorial completo para la biblioteca ember-data

Publicado: 2022-03-11

Ember Data (también conocido como ember-data o ember.data) es una biblioteca para administrar de manera sólida los datos del modelo en las aplicaciones Ember.js. Los desarrolladores de Ember Data afirman que está diseñado para ser independiente del mecanismo de persistencia subyacente, por lo que funciona tan bien con las API JSON a través de HTTP como con la transmisión de WebSockets o el almacenamiento IndexedDB local. Proporciona muchas de las funciones que encontraría en las asignaciones relacionales de objetos (ORM) del lado del servidor como ActiveRecord, pero está diseñado específicamente para el entorno único de JavaScript en el navegador.

Si bien Ember Data puede tomar algún tiempo para asimilar, una vez que lo haya hecho, es probable que descubra que valió la pena la inversión. En última instancia, hará que el desarrollo, la mejora y el mantenimiento de su sistema sean mucho más fáciles.

Cuando una API se representa utilizando modelos, adaptadores y serializadores de Ember Data, cada asociación simplemente se convierte en un nombre de campo. Esto encapsula los detalles internos de cada asociación, aislando así el resto de su código de los cambios en las propias asociaciones. Al resto de su código no le importará, por ejemplo, si una asociación en particular es polimórfica o es el resultado de un mapa de muchas asociaciones.

Además, su base de código está en gran medida aislada de los cambios de back-end, incluso si son significativos, ya que todo lo que su base de código espera son campos y funciones en los modelos, no una representación JSON, XML o YAML del modelo.

En este tutorial, presentaremos las características más destacadas de Ember Data y demostraremos cómo ayuda a minimizar la rotación de código, centrándonos en un ejemplo del mundo real.

También se proporciona un apéndice que analiza una serie de temas y ejemplos más avanzados de Ember Data.

Nota: este artículo presupone cierta familiaridad básica con Ember.js. Si no está familiarizado con Ember.js, consulte nuestro popular tutorial de Ember.js para obtener una introducción. También tenemos una guía completa de JavaScript disponible en ruso, portugués y español.

La propuesta de valor de Ember Data

Un ejemplo de cómo la biblioteca Ember Data puede ayudar a satisfacer las necesidades del cliente.

Comencemos considerando un ejemplo simple.

Digamos que tenemos una base de código de trabajo para un sistema de blog básico. El sistema contiene Publicaciones y Etiquetas, que tienen una relación de muchos a muchos entre sí.

Todo está bien hasta que obtengamos un requisito para admitir páginas. El requisito también establece que, dado que es posible etiquetar una página en WordPress, también deberíamos poder hacerlo.

Entonces, ahora, las etiquetas ya no se aplicarán solo a las publicaciones, también pueden aplicarse a las páginas. Como resultado, nuestra simple asociación entre etiquetas y publicaciones ya no será adecuada. En su lugar, necesitaremos una relación polimórfica unilateral de muchos a muchos, como la siguiente:

  • Cada publicación es etiquetable y tiene muchas etiquetas
  • Cada página es etiquetable y tiene muchas etiquetas
  • Cada Tag tiene muchos Taggables polimórficos

Es probable que la transición a este conjunto de asociaciones nuevo y más complejo tenga ramificaciones significativas en todo nuestro código, lo que resultará en una gran cantidad de abandonos. Dado que no tenemos idea de cómo serializar una asociación polimórfica a JSON, probablemente crearemos más puntos finales de API como GET /posts/:id/tags y GET /pages/:id/tags . Y luego, descartaremos todas nuestras funciones de analizador JSON existentes y escribiremos otras nuevas para los nuevos recursos agregados. Puaj. Tedioso y doloroso.

Ahora consideremos cómo abordaríamos esto usando Ember Data.

En Ember Data, acomodar este conjunto modificado de asociaciones simplemente implicaría pasar de:

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

para:

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

La rotación resultante en el resto de nuestro código sería mínima y podríamos reutilizar la mayoría de nuestras plantillas. Tenga en cuenta en particular que el nombre de la asociación de tags en la publicación permanece sin cambios. Además, el resto de nuestra base de código se basa únicamente en la existencia de la asociación de tags y no tiene en cuenta sus detalles.

Una cartilla de datos de Ember

Antes de sumergirnos en un ejemplo del mundo real, repasemos algunos fundamentos de Ember Data.

Rutas y Modelos

En Ember.js, el enrutador es responsable de mostrar plantillas, cargar datos y configurar el estado de la aplicación. El enrutador hace coincidir la URL actual con las rutas que ha definido, por lo que una ruta es responsable de especificar el modelo que se mostrará en una plantilla (Ember espera que este modelo sea una subclase de Ember.Object ):

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

Ember Data proporciona DS.Model , que es una subclase de Ember.Object y agrega capacidades como guardar o actualizar un solo registro o varios registros para mayor comodidad.

Para crear un nuevo Modelo, creamos una subclase de DS.Model (p. ej., App.User = DS.Model.extend({}) ).

Ember Data espera una estructura JSON intuitiva y bien definida del servidor y serializa los registros recién creados en el mismo JSON estructurado.

Ember Data también proporciona un conjunto de clases de matriz como DS.RecordArray para trabajar con modelos. Estos tienen responsabilidades como el manejo de relaciones de uno a muchos o de muchos a muchos, el manejo de la recuperación asíncrona de datos, etc.

Atributos del modelo

Los atributos básicos del modelo se definen mediante DS.attr ; p.ej:

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

Solo los campos creados por DS.attr se incluirán en la carga útil que se pasa al servidor para crear o actualizar registros.

DS.attr acepta cuatro tipos de datos: string , number , boolean y date .

RESTSerializador

Por defecto:

  • Ember Data emplea RESTSerializer para crear objetos a partir de respuestas de API (deserialización) y para generar JSON para solicitudes de API (serialización).
  • RESTSerializer espera que los campos creados por DS.belongsTo tengan un campo llamado user incluido en la respuesta JSON del servidor. Ese campo contiene la identificación del registro al que se hace referencia.
  • RESTSerializer agrega un campo de user a la carga útil que se pasa a la API con la identificación del pedido asociado.

Una respuesta podría, por ejemplo, verse así:

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

Y una solicitud HTTP creada por RESTSerializer para guardar un pedido podría verse así:

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

Relaciones uno a uno

Digamos, por ejemplo, que cada Usuario tiene un Perfil único. Podemos representar esta relación en Ember Data usando DS.belongsTo tanto en Usuario como en Perfil:

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

Luego podemos obtener la asociación con user.get('profile') o establecerla con user.set('profile', aProfile) .

RESTSerializer espera que se proporcione el ID del modelo asociado para cada modelo; p.ej:

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

Del mismo modo, incluye el ID del modelo asociado en una carga de solicitud:

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

Relaciones uno a muchos y muchos a uno

Digamos que tenemos un modelo en el que una publicación tiene muchos comentarios. En Ember Data, podemos representar esta relación con DS.hasMany('comment', {async: true}) en Post y DS.belongsTo('post', {async: true}) en 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}) });

Luego podemos obtener elementos asociados con post.get('comments', {async: true}) y agregar una nueva asociación con post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

El servidor luego responderá con una serie de ID para los comentarios correspondientes en una publicación:

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

… y con un ID para cada Comentario:

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

RESTSerializer agrega la identificación de la publicación asociada al comentario:

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

Sin embargo, tenga en cuenta que, de forma predeterminada, RESTSerializer no agregará DS.hasMany ID asociados a los objetos que serializa, ya que esas asociaciones se especifican en el lado "varios" (es decir, aquellas que tienen una asociación DS.belongsTo ). Entonces, en nuestro ejemplo, aunque una publicación tiene muchos comentarios, esos ID no se agregarán al objeto de la publicación:

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

Para "forzar" la serialización de DS.hasMany ID también, puede usar Embedded Records Mixin.

Relaciones de muchos a muchos

Digamos que en nuestro modelo, un autor puede tener varias publicaciones y una publicación puede tener varios autores.

Para representar esta relación en Ember Data, podemos usar DS.hasMany('author', {async: true}) en Post y DS.hasMany('post', {async: true}) en 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}) });

Luego podemos obtener elementos asociados con author.get('posts') y agregar una nueva asociación con author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

El servidor luego responderá con una matriz de ID para los objetos correspondientes; p.ej:

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

Dado que esta es una relación de muchos a muchos, RESTSerializer agrega una matriz de ID de objetos asociados; p.ej:

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

Un ejemplo del mundo real: mejorar un sistema de pedidos existente

En nuestro sistema de pedidos existente, cada Usuario tiene muchos Pedidos y cada Pedido tiene muchos Artículos. Nuestro sistema tiene varios proveedores (es decir, vendedores) a los que se pueden pedir productos, pero cada pedido solo puede contener artículos de un único proveedor.

Nuevo requisito n.° 1: habilite un solo pedido para incluir artículos de varios proveedores.

En el sistema existente, existe una relación de uno a muchos entre Proveedores y Pedidos. Sin embargo, una vez que extendemos un pedido para incluir artículos de múltiples proveedores, esta simple relación ya no será adecuada.

Específicamente, si un proveedor está asociado con un pedido completo, en el sistema mejorado ese pedido también puede incluir artículos pedidos a otros proveedores. Debe haber una manera, por lo tanto, de indicar qué parte de cada pedido es relevante para cada proveedor. Además, cuando un proveedor accede a sus pedidos, solo debe tener visibilidad de los artículos que le han pedido, no de otros artículos que el cliente haya pedido a otros proveedores.

Un enfoque podría ser introducir dos nuevas asociaciones de muchos a muchos; uno entre Pedido y Ítem y otro entre Pedido y Proveedor.

Sin embargo, para simplificar las cosas, introducimos una nueva construcción en el modelo de datos a la que nos referimos como "Pedido de proveedor".

Redacción de relaciones

El modelo de datos mejorado deberá adaptarse a las siguientes asociaciones:

  • Relación uno a muchos entre Usuarios y Pedidos (cada Usuario puede estar asociado con 0 a n Pedidos) y una relación Uno a muchos entre Usuarios y Proveedores (cada Usuario puede estar asociado con 0 a n Proveedores)

     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}) });
  • Relación de uno a muchos entre pedidos y ProviderOrders (cada pedido consta de 1 a n ProviderOrders):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • Relación de uno a muchos entre proveedores y ProviderOrders (cada proveedor puede estar asociado con 0 a n ProviderOrders):

     App.Provider = DS.Model.extend({ link: DS.attr('string'), admin: DS.belongsTo('user', {async: true}), orders: DS.belongsTo('providerOrder', {async: true}), items: DS.hasMany('items', {async: true}) });
  • Relación de uno a muchos entre ProviderOrders y Items (cada ProviderOrder consta de 1 a n elementos):

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

Y no olvidemos nuestra definición de ruta :

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

Ahora cada ProviderOrder tiene un Proveedor, que era nuestro principal objetivo. Los artículos se mueven de Order a ProviderOrder y se supone que todos los artículos en un ProviderOrder pertenecen a un único proveedor.

Minimizar la rotación de código

Desafortunadamente, hay algunos cambios importantes aquí. Entonces, veamos cómo Ember Data puede ayudarnos a minimizar cualquier cambio de código resultante en nuestra base de código.

Anteriormente, enviábamos elementos con items.pushObject(item) . Ahora debemos encontrar primero el ProviderOrder adecuado y enviarle un elemento:

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

Dado que esto es una gran rotación, y más trabajo de Order que del controlador, es mejor si movemos este código a 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); }); }); } });

Ahora podemos agregar artículos directamente en el pedido como order.pushItem(item) .

Y para listar Artículos de cada Orden:

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

Relaciones polimórficas

Ahora introduzcamos una solicitud de mejora adicional en nuestro sistema que complica aún más las cosas:

Nuevo requisito n.º 2: admite varios tipos de proveedores.

Para nuestro ejemplo simple, digamos que se definen dos tipos de Proveedores ("Tienda" y "Librería"):

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

Aquí es donde el soporte de Ember Data para las relaciones polimórficas será útil. Ember Data admite relaciones polimórficas uno a uno, uno a muchos y muchos a muchos. Esto se hace simplemente agregando el atributo polymorphic: true a la especificación de la asociación. Por ejemplo:

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

La bandera polymorphic anterior indica que hay varios tipos de Proveedores que se pueden asociar con un Pedido de Proveedor (en nuestro caso, ya sea una Tienda o una Librería).

Cuando una relación es polimórfica, la respuesta del servidor debe indicar tanto el ID como el tipo del objeto devuelto (el RESTSerializer lo hace de forma predeterminada); p.ej:

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

Cumplir con los nuevos requisitos

Necesitamos proveedores polimórficos y artículos para cumplir con los requisitos. Dado que ProviderOrder conecta proveedores con artículos, podemos cambiar sus asociaciones a asociaciones polimórficas:

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

Sin embargo, queda un problema pendiente: el proveedor tiene una asociación no polimórfica con los elementos, pero el elemento es un tipo abstracto. Por lo tanto, tenemos dos opciones para abordar esto:

  1. Requerir que todos los proveedores estén asociados con el mismo tipo de elemento (es decir, declarar un tipo específico de elemento para la asociación con el proveedor)
  2. Declarar la asociación de items en Provider como polimórfica

En nuestro caso, necesitamos ir con la opción #2 y declarar la asociación de items en el Proveedor como polimórfica:

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

Tenga en cuenta que esto no introduce ninguna rotación de código; todas las asociaciones simplemente funcionan de la misma manera que lo hacían antes de este cambio. ¡El poder de Ember Data en su máxima expresión!

¿Puede Ember Data realmente modelar todos mis datos?

Hay excepciones, por supuesto, pero considero las convenciones de ActiveRecord como una forma estándar y flexible de estructurar y modelar datos, así que permítame mostrarle cómo las convenciones de ActiveRecord se asignan a Ember Data:

tiene_muchos :usuarios a través de: :propiedades o Representando Modelos Intermedios

Esto consultará un modelo dinámico llamado Propiedad para encontrar Usuarios asociados. Si el modelo dinámico es básicamente una tabla dinámica, puede evitar crear un modelo intermedio en Ember Data y representar la relación con DS.hasMany en ambos lados.

Sin embargo, si necesita esa relación dinámica dentro de su front-end, configure un modelo de Propiedad que incluya DS.belongsTo('user', {async: true}) y DS.belongsTo('provider', {async: true}) , y luego agregue una propiedad tanto en Usuarios como en Proveedores que se mapee a través de la asociación usando Propiedad; p.ej:

 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: asignaciones, como: localizable

En nuestro objeto ActiveRecord, tenemos una relación polimórfica típica:

 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

Esta es una relación de muchos (polimórficos) a muchos (normales no polimórficos). En Ember Data podemos expresar esto con un DS.hasMany('locatable', {polymorphic: true, async: true}) polimórfico y un DS.hasMany('location', {async: true}) estático:

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

Para Localizables, como Usuario, el servidor debe devolver ID para las ubicaciones asociadas:

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

Para Ubicación, el servidor debe devolver tanto el ID como el tipo de Localizable en la matriz de objetos:

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

Además, puede representar relaciones por tipo con una relación estática de muchos a muchos:

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

¿Y qué pasa con los datos en tiempo real?

Ember Data tiene push , pushPayload y update . Siempre puede insertar manualmente registros nuevos/actualizados en la memoria caché local de Ember Data (llamada tienda) y se encargará de todo el resto.

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

Personalmente, solo uso sockets para eventos con cargas útiles muy pequeñas. Un evento típico es 'recordUpdated' con carga útil de {"type": "shop", "id": "14"} y luego en ApplicationRoute verificaré si ese registro está en el caché local (tienda) y si es I Lo recuperaré.

 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 esta manera, podemos enviar eventos actualizados de registro a todos los clientes sin una sobrecarga inaceptable.

Básicamente, existen dos enfoques en Ember Data para tratar con datos en tiempo real:

  1. Escriba un adaptador para su canal de comunicación en tiempo real y utilícelo en lugar de RESTAdapter.
  2. Empuje los registros a la tienda principal siempre que estén disponibles.

La desventaja de la primera opción es que es algo así como reinventar la rueda. Para la segunda opción, debemos acceder a la tienda principal, que está disponible en todas las rutas como route#store .

Envolver

En este artículo, le presentamos las construcciones y paradigmas clave de Ember Data, demostrando el valor que puede brindarle como desarrollador. Ember Data proporciona un flujo de trabajo de desarrollo más flexible y optimizado, minimizando la rotación de código en respuesta a lo que de otro modo serían cambios de gran impacto.

La inversión inicial (tiempo y curva de aprendizaje) que realiza al usar Ember Data para su proyecto sin duda valdrá la pena a medida que su sistema evolucione inevitablemente y necesite ser ampliado, modificado y mejorado.


APÉNDICE: Temas de datos avanzados de Ember

Este apéndice presenta una serie de temas más avanzados de Ember Data, que incluyen:

  • Diseño modular de Ember
  • Carga lateral
  • Enlaces
  • Adaptador y serializador de modelo activo
  • Mezcla de registros integrados
  • Modificadores de asociación (asincrónicos, inversos y polimórficos)
  • Parámetro 'ids' en solicitudes GET

Diseño modular

Ember Data tiene un diseño modular bajo el capó. Los componentes clave incluyen:

  1. Los Adapters son responsables de manejar la comunicación, actualmente solo REST sobre HTTP.
  2. Los Serializers gestionan la creación de modelos desde JSON o viceversa.
  3. Store cachés de registros creados.
  4. El Container pega todo esto.

Los beneficios de este diseño incluyen:

  1. La deserialización y el almacenamiento de datos funcionan independientemente del canal de comunicación empleado y el recurso solicitado.
  2. Las configuraciones de trabajo, como ActiveModelSerializer o EmbeddedRecordsMixin, se proporcionan listas para usar.
  3. Las fuentes de datos (p. ej., LocalStorage, implementación de CouchDB, etc.) se pueden intercambiar de entrada y salida cambiando los adaptadores.
  4. A pesar de muchas convenciones sobre la configuración, es posible configurar todo y compartir su configuración/implementación con la comunidad.

Carga lateral

Ember Data admite la "carga lateral" de datos; es decir, indicar datos auxiliares que deben recuperarse (junto con los datos primarios solicitados) para ayudar a consolidar múltiples solicitudes HTTP relacionadas.

Un caso de uso común es la transferencia local de modelos asociados. Por ejemplo, cada tienda tiene muchos comestibles, por lo que podemos incluir todos los comestibles relacionados en la respuesta de /shops :

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

Cuando se accede a la asociación de comestibles, Ember Data emitirá:

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

Sin embargo, si en su lugar devolvemos los comestibles asociados en el punto final de /shops , Ember Data no necesitará emitir otra solicitud:

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

Enlaces

Ember Data acepta enlaces en lugar de ID de asociación. Cuando se accede a una asociación especificada como enlace, Ember Data emitirá una solicitud GET a ese enlace para obtener los registros asociados.

Por ejemplo, podríamos devolver un enlace para una asociación de comestibles:

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

Y Ember Data luego enviaría una solicitud a /shops/14/groceries

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

Tenga en cuenta que aún necesita representar la asociación en los datos; los enlaces solo sugieren una nueva solicitud HTTP y no afectarán a las asociaciones.

Adaptador y serializador de modelo activo

Podría decirse que ActiveModelSerializer y ActiveModelAdapter se usan más en la práctica que RESTSerializer y RESTAdapter . En particular, cuando el backend usa Ruby on Rails y la gema ActiveModel::Serializers , la mejor opción es usar ActiveModelSerializer y ActiveModelAdapter , ya que son compatibles con ActiveModel::Serializers para usar.

Sin embargo, afortunadamente, las diferencias de uso entre ActiveModelSerializer / ActiveModelAdapter y RESTSerializer / RESTAdapter son bastante limitadas; a saber:

  1. ActiveModelSerializer usará nombres de campo de serpiente_caso, mientras que RESTSerializer requiere nombres de campo en camelCased.
  2. ActiveModelAdapter emite solicitudes a los métodos API de snake_case, mientras que RESTSerializer emite solicitudes a los métodos API camelCased.
  3. ActiveModelSerializer espera que los nombres de los campos relacionados con la asociación terminen en _id o _ids , mientras que RESTSerializer espera que los nombres de los campos relacionados con la asociación sean los mismos que el campo de la asociación.

Independientemente de la elección del adaptador y el serializador, los modelos de Ember Data serán exactamente iguales. Solo la representación JSON y los extremos de la API serán diferentes.

Tome nuestro ProviderOrder final como ejemplo:

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

Con Active Model Serializer and Adapter, el servidor debe esperar:

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

… y debe responder con:

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

Mezcla de registros integrados

DS.EmbeddedRecordsMixin es una extensión para DS.ActiveModelSerializer que permite configurar cómo se serializan o deserializan las asociaciones. Aunque aún no está completo (especialmente con respecto a las asociaciones polimórficas), es intrigante.

Tu puedes elegir:

  1. No serializar o deserializar asociaciones.
  2. Para serializar o deserializar asociaciones con id o ids.
  3. Para serializar o deserializar asociaciones con modelos incrustados.

Esto es especialmente útil en las relaciones de uno a varios en las que, de forma predeterminada, los ID asociados de DS.hasMany no se agregan a los objetos que se serializan. Tome un carrito de compras que tiene muchos artículos como ejemplo. En este ejemplo, el carrito se crea mientras se conocen los artículos. Sin embargo, cuando guarde el carrito, Ember Data no colocará automáticamente las ID de los artículos asociados en la carga útil de la solicitud.

Sin embargo, al usar DS.EmbeddedRecordsMixin , es posible decirle a Ember Data que serialice las ID de artículos en el carrito de la siguiente manera:

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

Como se muestra en el ejemplo anterior, EmbeddedRecordsMixin permite la especificación explícita de qué asociaciones serializar y/o deserializar a través del objeto attrs . Los valores válidos para serialize y deserialize son: - 'no' : no ​​incluir asociación en datos serializados/deserializados - 'id' o 'ids' : incluir ID(s) asociados en datos serializados/deserializados - 'records ': incluir propiedades reales (es decir, registrar valores de campo) como una matriz en datos serializados/deserializados

Modificadores de asociación (asincrónicos, inversos y polimórficos)

Se admiten los siguientes modificadores de asociación: polymorphic , inverse y async

Modificador polimórfico

En una asociación polimórfica, uno o ambos lados de la asociación representan una clase de objetos, en lugar de un objeto específico.

Recuerde nuestro ejemplo anterior de un blog en el que necesitábamos admitir la capacidad de etiquetar publicaciones y páginas. Para apoyar esto, habíamos llegado al siguiente modelo:

  • Cada publicación es etiquetable y tiene muchas etiquetas
  • Cada página es etiquetable y tiene muchas etiquetas
  • Cada Tag tiene muchos Taggables polimórficos

Siguiendo ese modelo, se puede usar un modificador polymorphic para declarar que las etiquetas están relacionadas con cualquier tipo de "etiquetable" (que puede ser una publicación o una página), de la siguiente manera:

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

Modificador Inverso

Por lo general, las asociaciones son bidireccionales. Por ejemplo, "La publicación tiene muchos comentarios" sería una dirección de una asociación, mientras que "El comentario pertenece a una publicación" sería la otra dirección (es decir, "inversa") de esa asociación.

En los casos en que no haya ambigüedad en la asociación, solo se debe especificar una dirección, ya que Ember Data puede deducir la parte inversa de la asociación.

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; p.ej:

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