En En

Настройки сериализаторов


В Ember Data сериализаторы форматируют данные, переданные или полученные из хранилища backend'а. По умолчанию Ember Data сериализует данные по формату JSON API. Если ваш backend использует другой формат, то Ember Data позволяет настроить сериализатор или использовать другой.

Ember Data изначально включает 3 сериализатора. JSONAPISerializer — исходный сериализатор, который работает с backend'ами JSON API. JSONSerializer — простой сериализатор для работы с одним объектом json или массивом записей. RESTSerializer — более сложный сериализатор, который поддерживает загрузку дополнительных записей, связанных с записью в запросе. Он был исходным сериализатором до версии 2.0.

Соглашения JSONAPISerializer

При запросе записи JSONAPISerializer ожидает, пока сервер вернет JSON-представление записи, которое соответствует следующим соглашениям.

Документ JSON API

Сериализатор JSONAPI ожидает, пока backend вернет документ JSON API, который соответствует спецификации JSON API и соглашениям по примерам на сайте http://jsonapi.org/format/. Это значит, что все имена типов должны быть преобразованы во множественное число, а имена атрибутов и связей должны иметь тире. Например, если вы запрашиваете запись из /people/123, ответ будет выглядеть таким образом:

{
  "data": {
    "type": "people",
    "id": "123",
    "attributes": {
      "first-name": "Jeff",
      "last-name": "Atwood"
    }
  }
}

Ответ, который включает несколько записей, может иметь массив в свойстве data.

{
  "data": [{
    "type": "people",
    "id": "123",
    "attributes": {
      "first-name": "Jeff",
      "last-name": "Atwood"
    }
  }, {
    "type": "people",
    "id": "124",
    "attributes": {
      "first-name": "Yehuda",
      "last-name": "Katz"
    }
  }]
}

Загрузка связанных данных

Данные, которые не являются частью исходного запроса, но включают относящиеся к нему связи, должны быть размещены в массиве под ключом included. Например, если вы запросили запись из /articles/1, а backend вернул вместе с ней какие-либо комментарии, имеющие к ней отношение, ответ должен выглядеть так:

{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!"
    },
    "links": {
      "self": "http://example.com/articles/1"
    },
    "relationships": {
      "comments": {
        "data": [
          { "type": "comments", "id": "5" },
          { "type": "comments", "id": "12" }
        ]
      }
    }
  },
  "included": [{
    "type": "comments",
    "id": "5",
    "attributes": {
      "body": "First!"
    },
    "links": {
      "self": "http://example.com/comments/5"
    }
  }, {
    "type": "comments",
    "id": "12",
    "attributes": {
      "body": "I like XML better"
    },
    "links": {
      "self": "http://example.com/comments/12"
    }
  }]
}

Настройка сериализаторов

Ember Data по умолчанию использует JSONAPISerializer, но вы можете переопределить его, назначив свой сериализатор. Есть два способа определить индивидуально настроенный сериализатор. Вы можете назначить его для всего приложения, определив сериализатор "application".

app/serializers/application.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({});

Вы также можете определить сериализатор для отдельной модели. Например, если у вас есть модель post, то вы можете определить сериализатор post:

app/serializers/post.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({});

Чтобы изменить формат данных, отправляемых в хранилище backend'а, можно использовать hook serialize(). Например, у нас есть такой ответ JSON API от Ember Data:

{
  "data": {
    "id": "1",
    "type": "product",
    "attributes": {
      "name": "My Product",
      "amount": 100,
      "currency": "SEK"
    }
  }
}

Но наш сервер ожидает данные в таком формате:

{
  "data": {
    "id": "1",
    "type": "product"
    "attributes": {
      "name": "My Product",
      "cost": {
        "amount": 100,
        "currency": "SEK"
      }
    }
  }
}

Вот так можно изменить данные:

app/serializers/application.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  serialize(snapshot, options) {
    var json = this._super(...arguments);

    json.data.attributes.cost = {
      amount: json.data.attributes.amount,
      currency: json.data.attributes.currency
    };

    delete json.data.attributes.amount;
    delete json.data.attributes.currency;

    return json;
  },
});

Похожим образом, если хранилище backend'а предоставляет данные в отличном от JSON API формате, можно использовать hook normalizeResponse(). Возьмем тот же пример выше. Если сервер предоставляет данные, которые выглядят так:

{
  "data": {
    "id": "1",
    "type": "product",
    "attributes": {
      "name": "My Product",
      "cost": {
        "amount": 100,
        "currency": "SEK"
      }
    }
  }
}

а нам нужно изменить их таким образом:

{
  "data": {
    "id": "1",
    "type": "product",
    "attributes": {
      "name": "My Product",
      "amount": 100,
      "currency": "SEK"
    }
  }
}

то это можно сделать так:

app/serializers/application.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  normalizeResponse(store, primaryModelClass, payload, id, requestType) {
    payload.data.attributes.amount = payload.data.attributes.cost.amount;
    payload.data.attributes.currency = payload.data.attributes.cost.currency;

    delete payload.data.attributes.cost;

    return this._super(...arguments);
  },
});

Чтобы стандартизировать только одну модель, можно точно так же использовать hook normalize().

Больше информации о hook'ах вы найдете в документации по сериализатору.

Идентификаторы

Чтобы отслеживать уникальные записи в хранилище, Ember Data ожидает, что каждая запись будет иметь свойство id в полезной нагрузке. Идентификаторы должны быть уникальными для каждой отдельной записи конкретного типа. Если backend использовал отличный от id ключ, то вы можете применить свойство сериализатора primaryKey, чтобы корректно преобразовывать свойство идентификатора в id при сериализации и десериализации данных.

app/serializers/application.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  primaryKey: '_id'
});

Имена атрибутов

В Ember Data принято использовать camelCase с именами атрибутов в модели. Например:

app/models/person.js

import DS from 'ember-data';

export default DS.Model.extend({
  firstName: DS.attr('string'),
  lastName:  DS.attr('string'),
  isPersonOfTheYear: DS.attr('boolean')
});

Но JSONAPISerializer ожидает, что имена атрибутов в полезной нагрузке документа, возвращаемой сервером, будут иметь тире:

{
  "data": {
    "id": "44",
    "type": "people",
    "attributes": {
      "first-name": "Barack",
      "last-name": "Obama",
      "is-person-of-the-year": true
    }
  }
}

Если возвращаемые сервером атрибуты используют другое соглашение, вы можете применить метод сериализатора keyForAttribute(). Так вы преобразуете имя атрибута в модели в ключ полезной нагрузки JSON. Например, если backend вернул атрибуты, которые under_scored вместо dash-cased, то вы можете переопределить метод keyForAttribute так:

app/serializers/application.js

import Ember from 'ember';
import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  keyForAttribute: function(attr) {
    return Ember.String.underscore(attr);
  }
});

Нестандартные ключи может сопоставить индивидуально настроенный сериализатор. Объект attrs можно использовать, чтобы объявить простое сопоставление между именами свойств в записях DS.Model и ключами полезной нагрузки в сериализованном объекте JSON, который представляет запись. Объект с ключом свойства также может использоваться, чтобы объявлять ключ атрибута в полезной нагрузке ответа.

Если JSON для person имеет ключ lastNameOfPerson, а желаемое имя атрибута — просто lastName, то можно создать индивидуально настроенный сериализатор для модели и переопределить свойство attrs.

app/models/person.js

import DS from 'ember-data';

export default DS.Model.extend({
  lastName: DS.attr('string')
});

app/serializers/person.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  attrs: {
    lastName: 'lastNameOfPerson'
  }
});

Связи

Ссылаться на другие записи следует по идентификатору. Например, если у вас есть модель со связью hasMany:

app/models/post.js

import DS from 'ember-data';

export default DS.Model.extend({
  comments: DS.hasMany('comment', { async: true })
});

JSON должен кодировать связь как массив типов и идентификаторов:

{
  "data": {
    "type": "posts",
    "id": "1",
    "relationships": {
      "comments": {
        "data": [
          { "type": "comments", "id": "1" },
          { "type": "comments", "id": "2" },
          { "type": "comments", "id": "3" }
        ]
      }
    }
  }
}

post.get('comments') может загрузить comments для post. Адаптер JSON API отправит 3 запроса GET в /comments/1/, /comments/2/ и /comments/3/.

Любые связи belongsTo в представлении JSON должны быть именами свойств с тире. Например, если у вас есть модель:

app/models/comment.js

import DS from 'ember-data';

export default DS.Model.extend({
  originalPost: DS.belongsTo('post')
});

JSON должен кодировать связь как идентификатор к еще одной записи:

{
  "data": {
    "type": "comment",
    "id": "1",
    "relationships": {
      "original-post": {
        "data": { "type": "post", "id": "5" },
      }
    }
  }
}

При необходимости эти соглашения по наименованиям можно переписать с помощью метода keyForRelationship().

app/serializers/application.js

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  keyForRelationship: function(key, relationship) {
    return key + 'Ids';
  }
});

Создание индивидуальных преобразований

Иногда встроенные типы атрибутов (string, number, boolean и date) могут не соответствовать требованиям. Например, сервер может вернуть дату в нестандартном формате.

В Ember Data могут быть новые преобразования JSON, зарегистрированные для использования в качестве атрибутов:

app/transforms/coordinate-point.js

import DS from 'ember-data';

export default DS.Transform.extend({
  serialize: function(value) {
    return [value.get('x'), value.get('y')];
  },
  deserialize: function(value) {
    return Ember.Object.create({ x: value[0], y: value[1] });
  }
});

app/models/cursor.js

import DS from 'ember-data';

export default DS.Model.extend({
  position: DS.attr('coordinate-point')
});

Когда coordinatePoint приходит от API, он должен быть массивом:

{
  cursor: {
    position: [4,9]
  }
}

Но он загружен в экземпляре модели и будет вести себя как объект:

var cursor = store.findRecord('cursor', 1);
cursor.get('position.x'); //=> 4
cursor.get('position.y'); //=> 9

Если position меняется и сохраняется, он преобразуется через функцию serialize и снова представляется в качестве массива в JSON.

JSONSerializer

Не все API следуют соглашениям, которые JSONAPISerializer использует с пространством имен данных и загружаемыми записями об отношениях. Некоторые традиционные API могут возвращать простую полезную нагрузку JSON, которая представляет собой только запрошенный ресурс или массив сериализованных записей. JSONSerializer — сериализатор, который изначально включен в Ember Data. Его можно использовать вместе с RESTAdapter, чтобы сериализовывать эти упрощенные API.

Чтобы использовать его в приложении, вам нужно будет определить serializer:application, который расширяет JSONSerializer.

app/serializers/application.js

import DS from 'ember-data';

export default DS.JSONSerializer.extend({
  // ...
});

При запросах, которые возвращают только одну запись (например, store.findRecord('post', 1)), JSONSerializer ожидает получить в ответ объект JSON, похожий на это:

{
  "id": "1",
  "title": "Rails is omakase",
  "tag": "rails",
  "comments": ["1", "2"]
}

При запросах, которые возвращают больше записей или ни одной (например, store.findAll('post') или store.query('post', { filter: { status: 'draft' } })), JSONSerializer ожидает получить в ответ массив JSON. Он выглядит примерно так:

[{
  "id": "1",
  "title": "Rails is omakase",
  "tag": "rails",
  "comments": ["1", "2"]
}, {
  "id": "2",
  "title": "I'm Running to Reform the W3C's Tag",
  "tag": "w3c",
  "comments": ["3"]
}]

JSONAPISerializer базируется на JSONSerializer, поэтому они используют многие одинаковые hook'и для настройки поведения процесса сериализации. Проверьте документацию по API, чтобы ознакомиться с полным списком методов и свойств.

EmbeddedRecordMixin

Хотя Ember Data поддерживает загрузку дополнительных связей, иногда при работе с традиционными API вы можете столкнуться с JSON, который содержит связи, вложенные внутри других записей. EmbeddedRecordsMixin помогает справиться с этой проблемой.

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

Например, если бы модель post содержала вложенную запись author, она выглядела так:

{
  "id": "1",
  "title": "Rails is omakase",
  "tag": "rails",
  "authors": [
    {
      "id": "2",
      "name": "Steve"
    }
  ]
}

Вы могли бы определить связь так:

app/serializers/post.js

import DS from 'ember-data';

export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    authors: {
      serialize: 'records',
      deserialize: 'records'
    }
  }
});

Если вам нужно сериализовать и десериализовать вложенную связь, можно использовать сокращенный вариант — { embedded: 'always' }. Следующий пример равноценен примеру выше:

app/serializers/post.js

import DS from 'ember-data';

export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    authors: { embedded: 'always' }
  }
});

Ключи serialize и deserialize поддерживают 3 варианта: records используется для подачи сигнала о том, что ожидается вся запись; ids используется для подачи сигнала о том, что ожидается только идентификатор записи; false используется для подачи сигнала о том, что запись не ожидается.

Например, вам нужно считать вложенную запись при извлечении полезной нагрузки JSON, но при сериализации записи включить только идентификатор связи. Это возможно сделать с помощью варианта serialize: 'ids'. Вы можете также отказаться от сериализации связи, установив serialize: false.

app/serializers/post.js

import DS from 'ember-data';

export default DS.JSONSerializer.extend(DS.EmbeddedRecordsMixin, {
  attrs: {
    author: {
      serialize: false,
      deserialize: 'records'
    },
    comments: {
      deserialize: 'records',
      serialize: 'ids'
    }
  }
});

Исходные установки EmbeddedRecordsMixin

Если вы не переписываете attrs для конкретной связи, EmbeddedRecordsMixin будет вести себя следующим образом:

BelongsTo: { serialize: 'id', deserialize: 'id' } HasMany: { serialize: false, deserialize: 'ids' }

Можно не вкладывать JSON в сериализованную полезную нагрузку с помощью serialize: 'ids'. Если вы не хотите отправлять связь, то можете использовать serialize: false.

Авторские сериализаторы

Если вы хотите создать индивидуальный сериализатор, то рекомендуем начать с JSONAPISerializer или JSONSerializer и расширить один из них так, чтобы он соответствовал вашим нуждам. Но если ваша полезная нагрузка сильно отличается от одного из этих сериализаторов, то вы можете создать собственный, если расширите базовый класс DS.Serializer.

Сериализатор в Ember Data отвечает за приведение полезной нагрузки из адаптера к формату, который понимает Ember Data. Он также отвечает за преобразование текущего состояния записи в полезную нагрузку, которую адаптер отправит backend'у.

Сериализатор исполняет две важные роли в Ember Data. Он принимает ответ от адаптера и сериализует его в стандартный формат JSON, который понятен для Ember Data. И он преобразует текущие состояния записей в полезную нагрузку, которую адаптер отправляет серверу при создании, обновлении или удалении записи.

Стандартизированный формат JSON для Ember Data

Стандартизированный формат JSON для Ember Data — это документ JSON API с несколькими дополнительными условиями.

Важно убедиться, что имя type записи в стандартизированном объекте JSON точно соответствует имени файла модели, определенной для этого типа записи. По соглашению имена модели имеют форму единственного числа в Ember Data, но имена типов в примерах из спецификации JSON API имеют форму множественного числа. В самой спецификации JSON API нет зависимости от правил изменения формы слова, но JSONAPISerializer в Ember Data считает, что типы имеют форму множественного числа, и будет автоматически переводить их в форму единственного числа.

Имена атрибутов и связей в документе JSON API должны точно соответствовать имени и стилю DS.attr(), DS.belongsTo() и DS.hasMany(), свойств, определенных в модели.

По соглашению в моделях Ember Data эти имена свойств имеют camelCase. Как и в случае с именами type, они отличаются от имен атрибутов и связей в примерах из спецификации JSON API. В примерах из спецификации для имен атрибутов и связей используется dash-case, но спецификация не требует, чтобы имена атрибутов или связей соответствовали каким-либо конкретным соглашениям по стилю. Если вы используете JSONAPISerializer из Ember Data, он будет считать, что имена атрибутов и связей из вашего API используют dash-case и автоматически преобразует их по стилю camelCase при создании стандартизированного объекта JSON.

В остальном стандартизированный объект JSON для Ember Data соответствует спецификации JSON API.

Например, возьмем эту модель post.

app/models/post.js

import DS from 'ember-data';

export default DS.Model.extend({
  title: DS.attr('string'),
  tag: DS.attr('string'),
  comments: hasMany('comment', { async: false }),
  relatedPosts: hasMany('post')
});

Стандартизированный объект JSON, который Ember Data ожидает получить от сериализатора, выглядит примерно так:

{
  data: {
    id: "1",
    type: "post",
    attributes: {
      title: "Rails is omakase",
      tag: "rails",
    },
    relationships: {
      comments: {
        data: [{ id: "1", type: 'comment' },
               { id: "2", type: 'comment' }],
      },
      relatedPosts: {
        links: {
          related: "/api/v1/posts/1/related-posts/"
        }
      }
    }
}

Обратите внимание, что тип "post" соответствует модели post, а связь relatedPosts в документе соответствует relatedPosts: hasMany('post') в модели.

Стандартизация ответов адаптера

При создании индивидуально настроенного сериализатора вам нужно будет определить метод normalizeResponse для преобразования ответа от адаптера в стандартизированный объект JSON, описанный выше.

Этот метод получает store, класс модели для запроса, полезную нагрузку, идентификатор запроса записи (или null, если нет идентификатора, связанного с запросом) и тип запроса (строку с возможными значениями: 'findRecord', 'queryRecord', 'findAll', 'findBelongsTo', 'findHasMany', 'findMany', 'query', 'createRecord', 'deleteRecord' и 'updateRecord') в качестве аргументов.

Индивидуально настроенному сериализатору также нужно будет определить метод normalize. Этот метод вызывается store.normalize(type, payload) и часто используется для стандартизации запросов, сделанных вне Ember Data, так как они не относятся к нормальному циклу CRUD, который предоставляет адаптер.

Сериализация записей

Наконец, сериализатору нужно будет реализовать метод serialize. Ember Data предоставит текущее состояние записи и хеш с вариантами, а этот метод должен вернуть объект, который адаптер отправит серверу при создании, обновлении или удалении записи.

Сериализаторы сообщества

Если ни один из исходных сериализаторов Ember Data не работает с вашим backend'ом, посмотрите варианты от сообщества. Лучше всего их искать на Ember Observer.


Комментарии (2)

  1. Сергей 02 января 2017, 19:33 # 0
    Почему-то нигде подробно не описано, где взять нормальный рабочий backend для Ember. Я, конечно, далеко не профи, и поэтому испытал большие трудности с этим моментом. Мне бы хотелось подключить MySQL к Ember data.
    Поделитесь ссылками на русском, пожалуйста. Желательно написанном на node.js. Потому что идея писать фронт и бэк на одном языке кажется гениальной)
    1. Сергей 02 января 2017, 19:35 # 0
      Вот плод моих мучений
      github.com/justerest/jsonapi-ember
    Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.