Ember Data: подробное руководство по библиотеке ember-data

Опубликовано: 2022-03-11

Ember Data (также известная как ember-data или ember.data) — это библиотека для надежного управления данными модели в приложениях Ember.js. Разработчики Ember Data заявляют, что он спроектирован так, чтобы не зависеть от базового механизма сохраняемости, поэтому он работает так же хорошо с JSON API через HTTP, как и с потоковыми WebSockets или локальным хранилищем IndexedDB. Он предоставляет многие возможности, которые вы найдете в реляционных сопоставлениях объектов (ORM) на стороне сервера, таких как ActiveRecord, но разработан специально для уникальной среды JavaScript в браузере.

Несмотря на то, что Ember Data может занять некоторое время, как только вы это сделаете, вы, вероятно, обнаружите, что это стоит вложений. В конечном итоге это значительно упростит разработку, усовершенствование и обслуживание вашей системы.

Когда API представлен с использованием моделей Ember Data, адаптеров и сериализаторов, каждая ассоциация становится просто именем поля. Это инкапсулирует внутренние детали каждой ассоциации, тем самым изолируя остальную часть вашего кода от изменений самих ассоциаций. Остальной части вашего кода все равно, например, является ли конкретная ассоциация полиморфной или является результатом карты многих ассоциаций.

Более того, ваша кодовая база в значительной степени изолирована от внутренних изменений, даже если они значительны, поскольку все, что ваша кодовая база ожидает, — это поля и функции в моделях, а не JSON, XML или YAML-представление модели.

В этом руководстве мы познакомим вас с наиболее важными функциями Ember Data и продемонстрируем, как это помогает минимизировать отток кода, сосредоточив внимание на реальном примере.

Также предоставляется приложение, в котором обсуждается ряд более сложных тем и примеров Ember Data.

Примечание. Эта статья предполагает некоторое базовое знакомство с Ember.js. Если вы не знакомы с Ember.js, ознакомьтесь с нашим популярным руководством по Ember.js. У нас также есть полное руководство по JavaScript на русском, португальском и испанском языках.

Преимущества данных Ember

Пример того, как библиотека данных Ember может помочь удовлетворить потребности клиентов.

Начнем с рассмотрения простого примера.

Скажем, у нас есть рабочая кодовая база для базовой системы блогов. Система содержит сообщения и теги, которые связаны друг с другом по принципу «многие ко многим».

Все в порядке, пока мы не получим требование о поддержке Pages. В требовании также говорится, что, поскольку в WordPress можно пометить страницу, мы также должны иметь возможность сделать это.

Так что теперь теги больше не будут применяться только к сообщениям, они также могут применяться к страницам. В результате наша простая ассоциация между тегами и сообщениями больше не будет адекватной. Вместо этого нам понадобится одностороннее полиморфное отношение «многие ко многим», например следующее:

  • Каждое сообщение является тегируемым и имеет много тегов
  • Каждая страница является тегируемой и имеет много тегов
  • Каждый тег имеет много полиморфных тегов.

Переход к этому новому, более сложному набору ассоциаций, вероятно, будет иметь значительные последствия для всего нашего кода, что приведет к большому оттоку. Поскольку мы понятия не имеем, как сериализовать полиморфную ассоциацию в JSON, мы, вероятно, просто создадим дополнительные конечные точки API, такие как GET /posts/:id/tags и GET /pages/:id/tags . А затем мы отбросим все наши существующие функции парсера JSON и напишем новые для новых добавленных ресурсов. Фу. Утомительно и больно.

Теперь давайте рассмотрим, как мы подойдем к этому, используя Ember Data.

В Ember Data приспособиться к этому измененному набору ассоциаций будет просто означать переход от:

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

к:

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

В результате изменение остальной части нашего кода будет минимальным, и мы сможем повторно использовать большинство наших шаблонов. В частности, обратите внимание, что имя ассоциации tags в Post остается неизменным. Кроме того, остальная часть нашей кодовой базы полагается только на существование ассоциации tags и не обращает внимания на ее детали.

Учебник по данным Ember

Прежде чем погрузиться в реальный пример, давайте рассмотрим некоторые основы Ember Data.

Маршруты и модели

В Ember.js маршрутизатор отвечает за отображение шаблонов, загрузку данных и другие настройки состояния приложения. Маршрутизатор сопоставляет текущий URL с определенными вами маршрутами, поэтому Route отвечает за указание модели, которую должен отображать шаблон (Ember ожидает, что эта модель будет подклассом Ember.Object ):

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

Ember Data предоставляет DS.Model , который является подклассом Ember.Object и добавляет такие возможности, как сохранение или обновление одной записи или нескольких записей для удобства.

Чтобы создать новую модель, мы создаем подкласс DS.Model (например, App.User = DS.Model.extend({}) ).

Ember Data ожидает от сервера четко определенную, интуитивно понятную структуру JSON и сериализует вновь созданные записи в такой же структурированный JSON.

Ember Data также предоставляет набор классов массивов, таких как DS.RecordArray для работы с моделями. У них есть такие обязанности, как обработка отношений «один ко многим» или «многие ко многим», обработка асинхронного извлечения данных и так далее.

Атрибуты модели

Атрибуты базовой модели определяются с помощью DS.attr ; например:

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

Только поля, созданные DS.attr , будут включены в полезные данные, передаваемые на сервер для создания или обновления записей.

DS.attr принимает четыре типа данных: string , number , boolean и date .

RESTSerializer

По умолчанию:

  • Ember Data использует RESTSerializer для создания объектов из ответов API (десериализация) и для генерации JSON для запросов API (сериализация).
  • RESTSerializer ожидает, что поля, созданные DS.belongsTo , будут иметь поле с именем user , включенное в ответ JSON от сервера. Это поле содержит идентификатор записи, на которую ссылаются.
  • RESTSerializer добавляет в полезные данные, передаваемые в API, user поле с идентификатором связанного заказа.

Ответ может, например, выглядеть так:

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

А HTTP-запрос, созданный RESTSerializer для сохранения заказа, может выглядеть так:

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

Отношения один к одному

Скажем, например, что у каждого пользователя есть уникальный профиль. Мы можем представить эту связь в данных Ember, используя DS.belongsTo как для пользователя, так и для профиля:

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

Затем мы можем получить ассоциацию с помощью user.get('profile') или установить ее с помощью user.set('profile', aProfile) .

RESTSerializer ожидает, что идентификатор связанной модели будет предоставлен для каждой модели; например:

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

Точно так же он включает идентификатор связанной модели в полезную нагрузку запроса:

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

Отношения «один ко многим» и «многие к одному»

Скажем, у нас есть модель, в которой пост имеет много комментариев. В Ember Data мы можем представить эту связь с помощью DS.hasMany('comment', {async: true}) для Post и DS.belongsTo('post', {async: true}) для 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}) });

Затем мы можем получить связанные элементы с помощью post.get('comments', {async: true}) и добавить новую ассоциацию с помощью post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .

Затем сервер ответит массивом идентификаторов для соответствующих комментариев к сообщению:

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

… и с идентификатором для каждого комментария:

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

RESTSerializer добавляет идентификатор связанного сообщения в комментарий:

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

Однако обратите внимание, что по умолчанию RESTSerializer не будет добавлять связанные идентификаторы DS.hasMany к объектам, которые он сериализует, поскольку эти ассоциации указаны на стороне «многих» (т. е. те, которые имеют ассоциацию DS.belongsTo ). Итак, в нашем примере, несмотря на то, что запись содержит много комментариев, эти идентификаторы не будут добавлены в объект записи:

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

Чтобы «принудительно» сериализовать идентификаторы DS.hasMany , вы можете использовать миксин Embedded Records.

Отношения «многие ко многим»

Скажем, в нашей модели у автора может быть несколько сообщений, а у сообщения может быть несколько авторов.

Чтобы представить эту связь в Ember Data, мы можем использовать DS.hasMany('author', {async: true}) для Post и DS.hasMany('post', {async: true}) для 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}) });

Затем мы можем получить связанные элементы с помощью author.get('posts') и добавить новую ассоциацию с author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .

Затем сервер ответит массивом идентификаторов соответствующих объектов; например:

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

Поскольку это отношение «многие ко многим», RESTSerializer добавляет массив идентификаторов связанных объектов; например:

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

Пример из реальной жизни: улучшение существующей системы заказов

В нашей существующей системе заказов у ​​каждого Пользователя есть много Заказов, а у каждого Заказа много Товаров. В нашей системе есть несколько поставщиков (то есть поставщиков), у которых можно заказывать продукты, но каждый заказ может содержать товары только от одного поставщика.

Новое требование № 1. Включите в один заказ товары от нескольких поставщиков.

В существующей системе существует связь «один ко многим» между поставщиками и заказами. Однако как только мы расширим заказ, включив в него товары от нескольких поставщиков, эта простая взаимосвязь перестанет быть адекватной.

В частности, если поставщик связан со всем заказом, в усовершенствованной системе этот заказ вполне может включать товары, заказанные у других поставщиков. Поэтому должен быть способ указать, какая часть каждого заказа относится к каждому поставщику. Более того, когда поставщик получает доступ к своим заказам, он должен видеть только товары, заказанные у него, а не какие-либо другие товары, которые клиент мог заказать у других поставщиков.

Одним из подходов может быть введение двух новых ассоциаций «многие ко многим»; один между Order и Item, а другой между Order и Provider.

Однако для простоты мы вводим в модель данных новую конструкцию, которую мы называем «ProviderOrder».

Составление отношений

Расширенная модель данных должна учитывать следующие ассоциации:

  • Связь «один ко многим» между пользователями и заказами (каждый пользователь может быть связан с 0–n заказами) и связь « один ко многим» между пользователями и поставщиками (каждый пользователь может быть связан с 0–n поставщиками)

     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}) });
  • Связь «один ко многим» между Orders и ProviderOrders (каждый Order состоит из 1-n ProviderOrders):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • Связь «один ко многим» между Providers и ProviderOrders (каждый Provider может быть связан с 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}) });
  • Связь «один ко многим» между ProviderOrders и Items (каждый ProviderOrder состоит из 1-n элементов):

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

И давайте не будем забывать наше определение маршрута :

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

Теперь у каждого ProviderOrder есть один Provider, что и было нашей основной целью. Элементы перемещаются из Order в ProviderOrder, и предполагается, что все элементы в одном ProviderOrder принадлежат одному Provider.

Минимизация оттока кода

К сожалению, здесь есть некоторые критические изменения. Итак, давайте посмотрим, как Ember Data может помочь нам свести к минимуму любое изменение кода в нашей кодовой базе.

Ранее мы отправляли элементы с помощью items.pushObject(item) . Теперь нам нужно сначала найти соответствующий ProviderOrder и отправить ему Item:

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

Поскольку это большая текучка и больше работы Order, чем контроллера, будет лучше, если мы переместим этот код в 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); }); }); } });

Теперь мы можем добавлять товары прямо в заказ, например order.pushItem(item) .

И для перечисления предметов каждого заказа:

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

Полиморфные отношения

Давайте теперь добавим в нашу систему дополнительный запрос на улучшение, который еще больше усложнит ситуацию:

Новое требование №2: поддержка нескольких типов провайдеров.

Для нашего простого примера допустим, что определены два типа Провайдеров («Магазин» и «Книжный магазин»):

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

Вот здесь-то и пригодится поддержка Ember Data полиморфных отношений. Ember Data поддерживает полиморфные отношения «один к одному», «один ко многим» и «многие ко многим». Это делается простым добавлением атрибута polymorphic: true в спецификацию ассоциации. Например:

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

Приведенный выше polymorphic флаг указывает на то, что существуют различные типы поставщиков, которые могут быть связаны с ProviderOrder (в нашем случае это либо магазин, либо книжный магазин).

Когда отношение является полиморфным, ответ сервера должен указывать как идентификатор, так и тип возвращаемого объекта (по умолчанию это делает RESTSerializer ); например:

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

Соответствие новым требованиям

Нам нужны полиморфные Providers и Items для удовлетворения требований. Поскольку ProviderOrder связывает Providers с Items, мы можем изменить его ассоциации на полиморфные ассоциации:

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

Однако остается проблема: Provider имеет неполиморфную ассоциацию с Items, но Item является абстрактным типом. Поэтому у нас есть два варианта решения этой проблемы:

  1. Требовать, чтобы все поставщики были связаны с одним и тем же типом элемента (т. е. объявлять определенный тип элемента для ассоциации с поставщиком).
  2. Объявите ассоциацию items на Provider как полиморфную

В нашем случае нам нужно выбрать вариант № 2 и объявить ассоциацию items на Provider как полиморфную:

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

Обратите внимание, что это не приводит к оттоку кода; все ассоциации просто работают так же, как и до этого изменения. Мощь Ember Data во всей красе!

Может ли Ember Data моделировать все мои данные?

Конечно, есть исключения, но я считаю соглашения ActiveRecord стандартным и гибким способом структурирования и моделирования данных, поэтому позвольте мне показать вам, как соглашения ActiveRecord сопоставляются с данными Ember:

has_many :users через: :owners или представление промежуточных моделей

Это позволит обратиться к сводной модели под названием «Владение», чтобы найти связанных пользователей. Если сводная модель по сути является сводной таблицей , можно не создавать промежуточную модель в Ember Data и представлять связь с DS.hasMany с обеих сторон.

Однако, если вам нужна эта сводная связь внутри вашего внешнего интерфейса, настройте модель владения, которая включает DS.belongsTo('user', {async: true}) и DS.belongsTo('provider', {async: true}) , а затем добавьте свойство как для пользователей, так и для поставщиков, которое сопоставляется с ассоциацией с использованием права собственности; например:

 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: locatable

В нашем объекте ActiveRecord у нас типичная полиморфная связь:

 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

Это отношение многие (полиморфные) ко многим (нормальные неполиморфные). В Ember Data мы можем выразить это с помощью полиморфного DS.hasMany('locatable', {polymorphic: true, async: true}) и статического DS.hasMany('location', {async: true}) :

 App.Locatable = DS.Model.extend({ locations: DS.hasMany('location', {async: true}) }); App.User = App.Locatable.extend({ userName: DS.attr('string') }); App.Location = DS.Model.extend({ locatables: DS.hasMany('locatable', {polymorphic: true, async: true}) });

Для Locatables, таких как User, сервер должен возвращать идентификаторы для связанных местоположений:

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

Для Location сервер должен вернуть как ID, так и тип Locatable в массиве объектов:

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

Кроме того, вы можете представить отношения по типу со статическим отношением «многие ко многим»:

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

А как насчет данных в реальном времени?

Ember Data поддерживает push , pushPayload и update . Вы всегда можете вручную поместить новые/обновленные записи в локальный кеш Ember Data (называемый хранилищем), а все остальное он обработает.

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

Я лично использую сокеты только для событий с очень небольшими полезными нагрузками. Типичным событием является «recordUpdated» с полезной нагрузкой {"type": "shop", "id": "14"} а затем в ApplicationRoute я проверю, находится ли эта запись в локальном кеше (хранилище), и если это так, я Я просто обновлю его.

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

Таким образом, мы можем отправлять обновленные записи событий всем клиентам без неприемлемых накладных расходов.

По сути, в Ember Data есть два подхода к работе с данными в реальном времени:

  1. Напишите адаптер для вашего канала связи в реальном времени и используйте его вместо RESTAdapter.
  2. Отправляйте записи в главное хранилище всякий раз, когда они доступны.

Недостатком первого варианта является то, что он чем-то похож на изобретение велосипеда. Для второго варианта нам нужно получить доступ к основному хранилищу, которое доступно на всех маршрутах как route#store .

Заворачивать

В этой статье мы познакомили вас с ключевыми конструкциями и парадигмами Ember Data, продемонстрировав ценность, которую они могут предоставить вам как разработчику. Ember Data обеспечивает более гибкий и оптимизированный рабочий процесс разработки, сводя к минимуму отток кода в ответ на то, что в противном случае было бы значительным изменением.

Первоначальные инвестиции (время и кривая обучения), которые вы делаете при использовании Ember Data для своего проекта, несомненно, окажутся полезными, поскольку ваша система неизбежно развивается и нуждается в расширении, модификации и улучшении.


ПРИЛОЖЕНИЕ: Расширенные темы Ember Data

В этом приложении представлен ряд более продвинутых тем Ember Data, в том числе:

  • Модульная конструкция Ember
  • Неопубликованная загрузка
  • Ссылки
  • Сериализатор и адаптер активной модели
  • Миксин встроенных записей
  • Модификаторы ассоциации (асинхронные, инверсные и полиморфные)
  • Параметр ids в запросах GET

Модульная конструкция

Ember Data имеет модульную структуру. Ключевые компоненты включают в себя:

  1. Adapters отвечают за обработку связи, в настоящее время только REST через HTTP.
  2. Serializers управляют созданием моделей из JSON или наоборот.
  3. Store в кэше созданные записи.
  4. Container склеивает все это вместе.

К преимуществам данной конструкции относятся:

  1. Десериализация и хранение данных работают независимо от используемого канала связи и запрошенных ресурсов.
  2. Рабочие конфигурации, такие как ActiveModelSerializer или EmbeddedRecordsMixin, предоставляются «из коробки».
  3. Источники данных (например, LocalStorage, реализация CouchDB и т. д.) можно заменять и отключать, меняя адаптеры.
  4. Несмотря на большое количество соглашений о настройке, можно настроить все и поделиться своей конфигурацией/реализацией с сообществом.

Неопубликованная загрузка

Ember Data поддерживает «загрузку» данных; т. е. указание вспомогательных данных, которые следует извлечь (вместе с запрошенными первичными данными), чтобы помочь консолидировать несколько связанных HTTP-запросов.

Обычный вариант использования — загрузка связанных моделей. Например, в каждом магазине есть много продуктов, поэтому мы можем включить все связанные продукты в ответ /shops :

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

При доступе к продуктовой ассоциации Ember Data выдаст:

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

Однако, если вместо этого мы вернем связанные продукты в конечной точке /shops , Ember Data не нужно будет выдавать еще один запрос:

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

Ссылки

Ember Data принимает ссылки вместо идентификаторов ассоциации. При доступе к ассоциации, указанной как ссылка, Ember Data отправит запрос GET к этой ссылке, чтобы получить связанные записи.

Например, мы могли бы вернуть ссылку на ассоциацию бакалейных товаров:

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

Затем Ember Data отправит запрос к /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" } ] }

Имейте в виду, что вам все еще нужно представить ассоциацию в данных; ссылки просто предлагают новый HTTP-запрос и не влияют на ассоциации.

Сериализатор и адаптер активной модели

Возможно, ActiveModelSerializer и ActiveModelAdapter на практике используются чаще, чем RESTSerializer и RESTAdapter . В частности, когда серверная часть использует Ruby on Rails и гем ActiveModel::Serializers , лучшим вариантом является использование ActiveModelSerializer и ActiveModelAdapter , поскольку они поддерживают ActiveModel::Serializers из коробки.

К счастью, различия в использовании между ActiveModelSerializer / ActiveModelAdapter и RESTSerializer / RESTAdapter довольно ограничены; а именно:

  1. ActiveModelSerializer будет использовать имена полей в змеином регистре, в то время как RESTSerializer требует имена полей в верблюжьем регистре.
  2. ActiveModelAdapter выдает запросы к методам API-интерфейса змеи, в то время как RESTSerializer выдает запросы к методам API-интерфейса camelCase.
  3. ActiveModelSerializer ожидает, что имена полей, связанных с ассоциацией, будут заканчиваться на _id или _ids , в то время как RESTSerializer ожидает, что имена полей, связанных с ассоциацией, будут такими же, как поле ассоциации.

Независимо от выбора адаптера и сериализатора модели Ember Data будут абсолютно одинаковыми. Только представление JSON и конечные точки API будут отличаться.

Возьмите наш окончательный ProviderOrder в качестве примера:

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

С Active Model Serializer and Adapter сервер должен ожидать:

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

… и должен ответить:

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

Миксин встроенных записей

DS.EmbeddedRecordsMixin — это расширение для DS.ActiveModelSerializer , которое позволяет настраивать способ сериализации или десериализации ассоциаций. Хотя она еще не завершена (особенно в отношении полиморфных ассоциаций), тем не менее она интригует.

Ты можешь выбрать:

  1. Не сериализовать или десериализовать ассоциации.
  2. Чтобы сериализовать или десериализовать ассоциации с идентификатором или идентификаторами.
  3. Для сериализации или десериализации ассоциаций со встроенными моделями.

Это особенно полезно в отношениях «один ко многим», когда по умолчанию связанные идентификаторы DS.hasMany не добавляются к сериализуемым объектам. Возьмем, к примеру, корзину, в которой много товаров. В этом примере корзина создается, когда известны товары. Однако, когда вы сохраняете корзину, Ember Data не будет автоматически помещать идентификаторы связанных товаров в полезную нагрузку запроса.

Однако с помощью DS.EmbeddedRecordsMixin можно указать Ember Data сериализовать идентификаторы товаров в корзине следующим образом:

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

Как показано в приведенном выше примере, EmbeddedRecordsMixin позволяет явно указать, какие ассоциации сериализовать и/или десериализовать через объект attrs . Допустимые значения для serialize и deserialize : - 'no' : не включать ассоциацию в сериализованные/десериализованные данные - 'id' или 'ids' : включать связанные идентификаторы в сериализованные/десериализованные данные - 'records ': включать фактические свойства (т. е. значения полей записи) в виде массива в сериализованных/десериализованных данных

Модификаторы ассоциации (асинхронные, инверсные и полиморфные)

Поддерживаются следующие модификаторы ассоциации: polymorphic , inverse и async .

Полиморфный модификатор

В полиморфной ассоциации одна или обе стороны ассоциации представляют класс объектов, а не конкретный объект.

Вспомните наш более ранний пример блога, в котором нам нужно было поддерживать возможность помечать как посты, так и страницы. Чтобы поддержать это, мы пришли к следующей модели:

  • Каждое сообщение является тегируемым и имеет много тегов
  • Каждая страница является тегируемой и имеет много тегов
  • Каждый тег имеет много полиморфных тегов.

Следуя этой модели, можно использовать polymorphic модификатор, чтобы объявить, что теги связаны с любым типом «Tagable» (который может быть либо публикацией, либо страницей), следующим образом:

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

Обратный модификатор

Обычно ассоциации двунаправлены. Например, «У публикации много комментариев» будет одним направлением ассоциации, а «Комментарий принадлежит публикации» будет другим (то есть «обратным») направлением этой ассоциации.

В тех случаях, когда в ассоциации нет неоднозначности, необходимо указать только одно направление, поскольку Ember Data может вывести обратную часть ассоциации.

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; например:

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