Ember Data: um tutorial abrangente para a biblioteca ember-data

Publicados: 2022-03-11

Ember Data (também conhecido como ember-data ou ember.data) é uma biblioteca para gerenciamento robusto de dados de modelo em aplicativos Ember.js. Os desenvolvedores do Ember Data afirmam que ele foi projetado para ser independente do mecanismo de persistência subjacente, portanto, funciona tão bem com APIs JSON sobre HTTP quanto com streaming de WebSockets ou armazenamento IndexedDB local. Ele fornece muitos dos recursos que você encontraria em mapeamentos relacionais de objetos (ORMs) do lado do servidor, como ActiveRecord, mas foi projetado especificamente para o ambiente exclusivo de JavaScript no navegador.

Embora o Ember Data possa levar algum tempo para ser desenvolvido, depois de fazer isso, você provavelmente descobrirá que valeu a pena o investimento. Em última análise, tornará o desenvolvimento, aprimoramento e manutenção do seu sistema muito mais fácil.

Quando uma API é representada usando modelos, adaptadores e serializadores Ember Data, cada associação se torna simplesmente um nome de campo. Isso encapsula os detalhes internos de cada associação, isolando assim o restante do código das alterações nas próprias associações. O resto do seu código não se importará, por exemplo, se uma determinada associação for polimórfica ou for o resultado de um mapa de muitas associações.

Além disso, sua base de código é amplamente isolada das alterações de back-end, mesmo que sejam significativas, pois tudo que sua base de código espera são campos e funções em modelos, não uma representação JSON ou XML ou YAML do modelo.

Neste tutorial, apresentaremos os recursos mais importantes do Ember Data e demonstraremos como ele ajuda a minimizar a rotatividade de código, com foco em um exemplo do mundo real.

Também é fornecido um apêndice que discute vários tópicos e exemplos mais avançados de Ember Data.

Observação: este artigo pressupõe alguma familiaridade básica com o Ember.js. Se você não estiver familiarizado com o Ember.js, confira nosso popular tutorial do Ember.js para uma introdução. Também temos um guia completo de JavaScript disponível em russo, português e espanhol.

A proposta de valor dos dados Ember

Um exemplo de como a biblioteca Ember Data pode ajudar a atender às necessidades do cliente.

Vamos começar considerando um exemplo simples.

Digamos que temos uma base de código funcional para um sistema básico de blog. O sistema contém Posts e Tags, que possuem um relacionamento muitos-para-muitos entre si.

Tudo está bem até que tenhamos um requisito para oferecer suporte ao Pages. O requisito também afirma que, como é possível marcar uma página no WordPress, também deveríamos fazê-lo.

Portanto, agora, as Tags não serão mais aplicadas apenas a Posts, elas também poderão ser aplicadas a Páginas. Como resultado, nossa simples associação entre Tags e Posts não será mais adequada. Em vez disso, precisaremos de um relacionamento polimórfico unilateral de muitos para muitos, como o seguinte:

  • Cada Post é um Taggable e tem muitas Tags
  • Cada página é um Taggable e tem muitas Tags
  • Cada Tag tem muitos Taggables polimórficos

A transição para esse novo e mais complexo conjunto de associações provavelmente terá ramificações significativas em todo o nosso código, resultando em muita rotatividade. Como não temos ideia de como serializar uma associação polimórfica para JSON, provavelmente criaremos mais endpoints de API como GET /posts/:id/tags e GET /pages/:id/tags . E, em seguida, descartaremos todas as nossas funções de analisador JSON existentes e escreveremos novas para os novos recursos adicionados. ECA. Cansativo e doloroso.

Agora vamos considerar como abordaríamos isso usando Ember Data.

Em Ember Data, acomodar esse conjunto modificado de associações envolveria simplesmente mudar 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}) });

O churn resultante no resto do nosso código seria mínimo e poderíamos reutilizar a maioria dos nossos modelos. Observe em particular que o nome da associação de tags em Post permanece inalterado. Além disso, o restante de nossa base de código depende apenas da existência da associação de tags e ignora seus detalhes.

Uma cartilha de dados Ember

Antes de mergulhar em um exemplo do mundo real, vamos revisar alguns fundamentos do Ember Data.

Rotas e Modelos

Em Ember.js, o roteador é responsável por exibir modelos, carregar dados e configurar o estado do aplicativo. O roteador corresponde à URL atual para as rotas que você definiu, portanto, uma Rota é responsável por especificar o modelo que um modelo deve exibir (o Ember espera que esse modelo seja uma subclasse de Ember.Object ):

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

Ember Data fornece DS.Model que é uma subclasse de Ember.Object e adiciona recursos como salvar ou atualizar um único registro ou vários registros por conveniência.

Para criar um novo Model, criamos uma subclasse de DS.Model (por exemplo, App.User = DS.Model.extend({}) ).

A Ember Data espera uma estrutura JSON intuitiva e bem definida do servidor e serializa os registros recém-criados para o mesmo JSON estruturado.

O Ember Data também fornece um conjunto de classes de array como DS.RecordArray para trabalhar com Modelos. Eles têm responsabilidades como lidar com relacionamentos um-para-muitos ou muitos-para-muitos, lidar com recuperação assíncrona de dados e assim por diante.

Atributos do modelo

Os atributos básicos do modelo são definidos usando DS.attr ; por exemplo:

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

Somente os campos criados pelo DS.attr serão incluídos na carga que é passada ao servidor para criar ou atualizar registros.

DS.attr aceita quatro tipos de dados: string , number , boolean e date .

RESTSerializer

Por padrão:

  • A Ember Data emprega RESTSerializer para criar objetos a partir de respostas de API (desserialização) e para gerar JSON para solicitações de API (serialização).
  • RESTSerializer espera que os campos criados por DS.belongsTo tenham um campo chamado user incluído na resposta JSON do servidor. Esse campo contém o id do registro referenciado.
  • RESTSerializer adiciona um campo de user ao payload passado para a API com o id do pedido associado.

Uma resposta pode, por exemplo, ser assim:

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

E uma solicitação HTTP criada pelo RESTSerializer para salvar um pedido pode ser assim:

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

Relacionamentos um a um

Digamos, por exemplo, que cada usuário tenha um perfil único. Podemos representar esse relacionamento em Ember Data usando DS.belongsTo no usuário e no perfil:

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

Podemos então obter a associação com user.get('profile') ou defini-la com user.set('profile', aProfile) .

RESTSerializer espera que o ID do modelo associado seja fornecido para cada modelo; por exemplo:

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

Da mesma forma, inclui o ID do modelo associado em uma carga de solicitação:

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

Relacionamentos um-para-muitos e muitos-para-um

Digamos que temos um modelo onde um Post tem muitos Comentários. Em Ember Data, podemos representar esse relacionamento com DS.hasMany('comment', {async: true}) em Post e DS.belongsTo('post', {async: true}) em 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}) });

Podemos então obter itens associados com post.get('comments', {async: true}) e adicionar uma nova associação com post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

O servidor então responderá com uma matriz de IDs para os comentários correspondentes em uma postagem:

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

… e com um ID para cada Comentário:

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

RESTSerializer adiciona o id do Post associado ao Comentário:

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

Observe que, por padrão, RESTSerializer, não adicionará IDs associados DS.hasMany aos objetos que ele serializa, pois essas associações são especificadas no lado “muitos” (ou seja, aquelas que possuem uma associação DS.belongsTo ). Assim, em nosso exemplo, embora um Post tenha muitos comentários, esses IDs não serão adicionados ao objeto Post:

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

Para “forçar” DS.hasMany IDs a serem serializados também, você pode usar o Embedded Records Mixin.

Relacionamentos muitos-para-muitos

Digamos que em nosso modelo um autor pode ter vários posts e um post pode ter vários autores.

Para representar esse relacionamento em Ember Data, podemos usar DS.hasMany('author', {async: true}) em Post e DS.hasMany('post', {async: true}) em 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}) });

Podemos então obter itens associados com author.get('posts') e adicionar uma nova associação com author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

O servidor então responderá com um array de IDs para os objetos correspondentes; por exemplo:

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

Como esse é um relacionamento muitos-para-muitos, RESTSerializer adiciona uma matriz de IDs de objetos associados; por exemplo:

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

Um exemplo do mundo real: aprimorando um sistema de pedidos existente

Em nosso sistema de pedidos existente, cada usuário tem muitos pedidos e cada pedido tem muitos itens. Nosso sistema tem vários fornecedores (ou seja, fornecedores) de quem os produtos podem ser encomendados, mas cada pedido pode conter apenas itens de um único fornecedor.

Novo requisito nº 1: habilitar um único pedido para incluir itens de vários fornecedores.

No sistema existente, existe uma relação um-para-muitos entre Provedores e Pedidos. Uma vez que estendemos um pedido para incluir itens de vários fornecedores, esse relacionamento simples não será mais adequado.

Especificamente, se um fornecedor estiver associado a um pedido inteiro, no sistema aprimorado esse pedido também pode incluir itens pedidos de outros fornecedores. É preciso haver uma maneira, portanto, de indicar qual parte de cada pedido é relevante para cada provedor. Além disso, quando um fornecedor acessa seus pedidos, ele só deve ter visibilidade dos itens solicitados dele, e não de quaisquer outros itens que o cliente possa ter solicitado a outros fornecedores.

Uma abordagem poderia ser a introdução de duas novas associações de muitos para muitos; um entre Pedido e Item e outro entre Pedido e Fornecedor.

No entanto, para manter as coisas mais simples, introduzimos uma nova construção no modelo de dados que chamamos de “ProviderOrder”.

Elaboração de relacionamentos

O modelo de dados aprimorado precisará acomodar as seguintes associações:

  • Relacionamento um-para-muitos entre Usuários e Pedidos (cada Usuário pode estar associado a 0 a n Pedidos) e um relacionamento um-para-muitos entre Usuários e Provedores (cada Usuário pode estar associado a 0 a n Provedores)

     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}) });
  • Relacionamento um-para-muitos entre Pedidos e ProviderOrders (cada Pedido consiste em 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}) });
  • Relacionamento um-para-muitos entre Provedores e ProviderOrders (cada Provedor pode estar associado a 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}) });
  • Relacionamento um-para-muitos entre ProviderOrders e Items (cada ProviderOrder consiste em 1 a n itens):

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

E não vamos esquecer nossa definição de rota :

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

Agora cada ProviderOrder tem um Provider, que era o nosso principal objetivo. Os itens são movidos de Order para ProviderOrder e supõe-se que todos os itens em um ProviderOrder pertencem a um único provedor.

Minimizando a rotatividade de código

Infelizmente, há algumas mudanças de ruptura aqui. Então, vamos ver como o Ember Data pode nos ajudar a minimizar qualquer variação de código resultante em nossa base de código.

Anteriormente, estávamos enviando itens com items.pushObject(item) . Agora precisamos primeiro encontrar o ProviderOrder apropriado e enviar um Item para ele:

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

Como isso é muito churn e mais trabalho do Order do que do controller, é melhor movermos este código para 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); }); }); } });

Agora podemos adicionar itens diretamente no pedido como order.pushItem(item) .

E para listar os itens de cada pedido:

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

Relações polimórficas

Vamos agora apresentar uma solicitação de aprimoramento adicional ao nosso sistema que complica ainda mais as coisas:

Novo requisito nº 2: Suporte a vários tipos de provedores.

Para nosso exemplo simples, digamos que dois tipos de Provedores (“Loja” e “Livraria”) são definidos:

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

É aqui que o suporte do Ember Data para relacionamentos polimórficos será útil. O Ember Data oferece suporte a relacionamentos polimórficos um para um, um para muitos e muitos para muitos. Isso é feito simplesmente adicionando o atributo polymorphic: true à especificação da associação. Por exemplo:

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

O sinalizador polymorphic acima indica que existem vários tipos de Provedores que podem ser associados a um ProviderOrder (no nosso caso, uma Loja ou uma Livraria).

Quando um relacionamento é polimórfico, a resposta do servidor deve indicar tanto o ID quanto o tipo do objeto retornado (o RESTSerializer faz isso por padrão); por exemplo:

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

Atendendo aos Novos Requisitos

Precisamos de Provedores e Itens polimórficos para atender aos requisitos. Como ProviderOrder conecta Providers com Items, podemos alterar suas associações para associações polimórficas:

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

No entanto, há um problema restante: Provider tem uma associação não polimórfica a Items, mas Item é um tipo abstrato. Portanto, temos duas opções para resolver isso:

  1. Exigir que todos os provedores estejam associados ao mesmo tipo de item (ou seja, declare um tipo específico de Item para a associação com o Provedor)
  2. Declare a associação de items no Provedor como polimórfica

No nosso caso, precisamos ir com a opção #2 e declarar a associação de items no Provider como polimórfica:

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

Observe que isso não introduz nenhuma variação de código; todas as associações simplesmente funcionam da mesma forma que funcionavam antes dessa mudança. O poder do Ember Data no seu melhor!

O Ember Data pode realmente modelar todos os meus dados?

Claro que há exceções, mas eu considero as convenções do ActiveRecord como uma forma padrão e flexível de estruturar e modelar dados, então deixe-me mostrar como as convenções do ActiveRecord são mapeadas para Ember Data:

has_many :users através de: :ownerships ou Representando Modelos Intermediários

Isso consultará um modelo de pivô chamado Propriedade para encontrar Usuários associados. Se o modelo dinâmico for basicamente uma tabela dinâmica , você poderá evitar criar um modelo intermediário no Ember Data e representar o relacionamento com DS.hasMany em ambos os lados.

No entanto, se você precisar desse relacionamento dinâmico dentro de seu front-end, configure um modelo de propriedade que inclua DS.belongsTo('user', {async: true}) e DS.belongsTo('provider', {async: true}) , e, em seguida, adicionar uma propriedade em Usuários e Provedores que mapeie para a associação usando Propriedade; por exemplo:

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

has_many :mappings, as: localizável

Em nosso objeto ActiveRecord, temos um relacionamento polimórfico típico:

 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 é uma relação muitos (polimórfico) para muitos (não polimórfico normal). Em Ember Data podemos expressar isso com um DS.hasMany('locatable', {polymorphic: true, async: true}) polimórfico e um 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 Locatables, como User, o servidor deve retornar IDs para os locais associados:

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

Para Localização, o servidor deve retornar o ID e o tipo de Localizável na matriz de objetos:

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

Além disso, você pode representar relacionamentos por tipo com um relacionamento estático de muitos para muitos:

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

E os dados em tempo real?

Ember Data tem push , pushPayload e update . Você sempre pode enviar manualmente registros novos/atualizados para o cache local do Ember Data (chamado armazenamento) e ele cuidará de todo o 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); }); } });

Eu pessoalmente só uso soquetes para eventos com cargas muito pequenas. Um evento típico é 'recordUpdated' com payload de {"type": "shop", "id": "14"} e depois no ApplicationRoute vou verificar se esse registro está no cache local (store) e se é I 'll apenas recarregá-lo.

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

Dessa forma, podemos enviar eventos atualizados de registro para todos os clientes sem sobrecarga inaceitável.

Existem essencialmente duas abordagens no Ember Data para lidar com dados em tempo real:

  1. Escreva um Adaptador para seu canal de comunicação em tempo real e use-o em vez de RESTAdapter.
  2. Envie registros para o armazenamento principal sempre que estiverem disponíveis.

A desvantagem com a primeira opção é que é um pouco semelhante a reinventar a roda. Para a segunda opção, precisamos acessar a loja principal, que está disponível em todas as rotas como route#store .

Embrulhar

Neste artigo, apresentamos as principais construções e paradigmas da Ember Data, demonstrando o valor que ela pode fornecer a você como desenvolvedor. O Ember Data fornece um fluxo de trabalho de desenvolvimento mais flexível e simplificado, minimizando a rotatividade de código em resposta ao que de outra forma seriam alterações de alto impacto.

O investimento inicial (tempo e curva de aprendizado) que você faz ao usar o Ember Data para seu projeto, sem dúvida, valerá a pena, pois seu sistema inevitavelmente evolui e precisa ser estendido, modificado e aprimorado.


APÊNDICE: Tópicos avançados de dados Ember

Este apêndice apresenta vários tópicos mais avançados de Ember Data, incluindo:

  • Projeto Modular da Ember
  • Carregamento lateral
  • Links
  • Serializador e adaptador de modelo ativo
  • Mixagem de Registros Incorporados
  • Modificadores de associação (assíncronos, inversos e polimórficos)
  • Parâmetro 'ids' em solicitações GET

Design modular

A Ember Data tem um design modular sob o capô. Os principais componentes incluem:

  1. Os Adapters são responsáveis ​​por manipular a comunicação, atualmente apenas REST sobre HTTP.
  2. Os Serializers gerenciam a criação de modelos a partir de JSON ou vice-versa.
  3. Store os registros criados em cache.
  4. Container cola tudo isso junto.

Os benefícios deste design incluem:

  1. A desserialização e armazenamento de dados funciona independentemente do canal de comunicação empregado e do recurso solicitado.
  2. Configurações de trabalho, como ActiveModelSerializer ou EmbeddedRecordsMixin, são fornecidas prontas para uso.
  3. As fontes de dados (por exemplo, LocalStorage, implementação do CouchDB, etc.) podem ser trocadas por meio da troca de adaptadores.
  4. Apesar de muita convenção sobre configuração, é possível configurar tudo e compartilhar sua configuração/implementação com a comunidade.

Carregamento lateral

Ember Data suporta “sideload” de dados; ou seja, indicando dados auxiliares que devem ser recuperados (junto com os dados primários solicitados) para ajudar a consolidar várias solicitações HTTP relacionadas.

Um caso de uso comum é o sideload de modelos associados. Por exemplo, cada loja tem muitos mantimentos, então podemos incluir todos os mantimentos relacionados na resposta /shops :

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

Quando a associação de mantimentos for acessada, a 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" } ] }

No entanto, se retornarmos Mercearias associadas no terminal /shops , o Ember Data não precisará emitir outra solicitação:

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

Links

A Ember Data aceita links no lugar de IDs de associação. Quando uma associação especificada como link for acessada, a Ember Data emitirá uma solicitação GET para esse link para obter os registros associados.

Por exemplo, poderíamos retornar um link para uma associação de mantimentos:

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

E a Ember Data então emitiria uma solicitação para /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" } ] }

Tenha em mente que você ainda precisa representar a associação nos dados; links apenas sugerem uma nova solicitação HTTP e não afetarão as associações.

Serializador e adaptador de modelo ativo

Indiscutivelmente, ActiveModelSerializer e ActiveModelAdapter são mais usados ​​na prática do que RESTSerializer e RESTAdapter . Em particular, quando o backend usa Ruby on Rails e a gem ActiveModel::Serializers , a melhor opção é usar ActiveModelSerializer e ActiveModelAdapter , já que eles suportam ActiveModel::Serializers fora da caixa.

Felizmente, porém, as diferenças de uso entre ActiveModelSerializer / ActiveModelAdapter e RESTSerializer / RESTAdapter são bastante limitadas; nomeadamente:

  1. ActiveModelSerializer usará nomes de campo snake_case enquanto RESTSerializer requer nomes de campo camelCased.
  2. ActiveModelAdapter emite solicitações para métodos de API snake_case enquanto RESTSerializer emite métodos de API camelCased.
  3. ActiveModelSerializer espera que os nomes de campos relacionados à associação terminem em _id ou _ids , enquanto RESTSerializer espera que os nomes de campos relacionados à associação sejam os mesmos que o campo de associação.

Independentemente da escolha do Adaptador e do Serializador, os modelos Ember Data serão exatamente os mesmos. Somente a representação JSON e os endpoints da API serão diferentes.

Veja nosso ProviderOrder final como exemplo:

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

Com o Active Model Serializer and Adapter, o servidor deve esperar:

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

… e deve responder com:

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

Mixagem de Registros Incorporados

DS.EmbeddedRecordsMixin é uma extensão para DS.ActiveModelSerializer que permite configurar como as associações são serializadas ou desserializadas. Embora ainda não completo (especialmente no que diz respeito às associações polimórficas), é intrigante.

Você pode escolher:

  1. Não serializar ou desserializar associações.
  2. Para serializar ou desserializar associações com id ou ids.
  3. Para serializar ou desserializar associações com modelos incorporados.

Isso é particularmente útil em relacionamentos um-para-muitos em que, por padrão, os IDs associados DS.hasMany não são adicionados aos objetos que são serializados. Pegue um carrinho de compras que tenha muitos itens como exemplo. Neste exemplo, o Carrinho está sendo criado enquanto os Itens são conhecidos. No entanto, quando você estiver salvando o Carrinho, os Ember Data não colocarão automaticamente os IDs dos Itens associados na carga útil da solicitação.

Usando DS.EmbeddedRecordsMixin , no entanto, é possível dizer ao Ember Data para serializar os IDs do item no carrinho da seguinte forma:

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

Conforme mostrado no exemplo acima, o EmbeddedRecordsMixin permite a especificação explícita de quais associações serializar e/ou desserializar por meio do objeto attrs . Os valores válidos para serialize e deserialize são: - 'no' : não inclui associação em dados serializados/desserializados - 'id' ou 'ids' : inclui ID(s) associado(s) em dados serializados/desserializados - 'records ': inclui propriedades reais (ou seja, registrar valores de campo) como uma matriz em dados serializados/desserializados

Modificadores de associação (assíncronos, inversos e polimórficos)

Os seguintes modificadores de associação são suportados: polymorphic , inverse e async

Modificador polimórfico

Em uma associação polimórfica, um ou ambos os lados da associação representam uma classe de objetos, em vez de um objeto específico.

Lembre-se de nosso exemplo anterior de um blog em que precisávamos oferecer suporte à capacidade de marcar postagens e páginas. Para suportar isso, chegamos ao seguinte modelo:

  • Cada Post é um Taggable e tem muitas Tags
  • Cada página é um Taggable e tem muitas Tags
  • Cada Tag tem muitos Taggables polimórficos

Seguindo esse modelo, um modificador polymorphic pode ser usado para declarar que as Tags estão relacionadas a qualquer tipo de “Tagable” (que pode ser um Post ou uma Página), como segue:

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

Modificador inverso

Geralmente as associações são bidirecionais. Por exemplo, “Post tem muitos comentários” seria uma direção de uma associação, enquanto “Comentário pertence a um Post” seria a outra direção (ou seja, “inversa”) dessa associação.

Nos casos em que não há ambiguidade na associação, apenas uma direção precisa ser especificada, pois o Ember Data pode deduzir a parte inversa da associação.

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; por exemplo:

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