В прошлом материале, мы рассмотрели неудобные моменты в системе типов GraphQL.
А теперь мы попробуем победить некоторые из них. Всех заинтересованных, прошу под кат.
Нумерация разделов соответствует тем проблемам, с которыми мне удалось справится.
1.2 NON_NULL INPUT
В этом пункте, мы рассмотрели неоднозначность, которую порождает особенность реализации nullable в GraphQL.
А проблема в том, что это не позволяет с наскока реализовать концепцию частичного обновления (partial update) — аналог HTTP-метода PATCH в архитектуре REST. В комментариях к прошлому материалу меня сильно критиковали за "REST"-мышление. Я же скажу лишь то, что к этому меня обязывает CRUD архитектура. И я не был готов отказываться от преимуществ REST, просто потому, что "не делай так". Да и решение данной проблемы нашлось.
И так, вернемся к проблеме. Как мы все знаем, сценарий работы CRUD, при обновлении записи выглядит так:
- Получили запись с бэка.
- Отредактировали поля записи.
- Отправили запись на бэк.
Концепция partial update, в этом случае, должна позволять нам отправлять назад только те поля, которые были изменены.
Итак, если мы определим модель ввода таким образом
input ExampleInput {
foo: String!
bar: String
} то при маппинге переменной типа ExampleInput с таким значением
{
"foo": "bla-bla-bla"
}на DTO с такой структурой:
ExampleDTO {
foo: String # обязательное поле
bar: ?String # необязательное поле
}мы получим объект DTO c таким значением:
{
foo: "bla-bla-bla",
bar: null
}а при маппинге переменной с таким значением
{
"foo": "bla-bla-bla",
"bar": null
}мы получим объект DTO c таким же значением, как в прошлый раз:
{
foo: "bla-bla-bla",
bar: null
}То есть, происходит энтропия — мы теряем информацию, о том было передано поле от клиента, или нет.
В этом случае не понятно, что нужно сделать с полем конечного объекта: не трогать его потому, что клиент не передал поле, или установить ему значение null, потому что клиент передал null.
Строго говоря, GraphQL — это RPC протокол. И я стал размышлять о том, как я делаю такие вещи на бэке и какие процедуры я должен вызывать, чтобы сделать именно так, как мне хочется. А на бэкенде я делаю частичное обновление полей так:
$repository->find(42)->setFoo('bla-bla-lba');То есть, я буквально не трогаю сеттер свойства сущности, если мне не нужно изменять значение этого свойства. Если переложить это на схему GraphQL, то получится вот такой результат:
type Mutation {
entityRepository: EntityManager!
}
type EntityManager {
update(id: ID!): PersitedEntity
}
type PersitedEntity {
setFoo(foo: String!): String!
setBar(foo: String): String
}теперь, если захотим, мы можем вызвать метод setBar, и установить его значение в null, или не трогать этот метод, и тогда значение не будет изменено. Таким образом, выходит недурная реализация partial update. Не хуже, чем PATCH из пресловутого REST.
В комментариях к прошлому материалу, summerwind спрашивал: зачем нужен partial update? Отвечаю: бывают ОЧЕНЬ большие поля.3. Полиморфизм
Часто бывает, что нужно подавать на ввод сущности, которые вроде "одно и то же" но не совсем. Я воспользуюсь примером с созданием аккаунта из прошлого материала.
# аккаунт организации
AccountInput {
login: "Acme",
password: "***",
subject: OrganiationInput {
title: "Acme Inc"
}
}# аккаунт частного лица
AccountInput {
login: "Acme",
password: "***",
subject: PersonInput {
firstName: "Vasya",
lastName: "Pupkin",
}
}Очевидно, что мы не можем подать данные с такой структурой на один аргумент — GraphQL просто не разрешит нам это сделать. Значит, нужно как-то решить эту проблему.
Способ 0 — в лоб
Первое, что приходит в голову — это разделение вариативной части ввода:
input AccountInput {
login: String!
password: Password!
subjectOrganization: OrganiationInput
subjectPerson: PersonInput
}Мда… когда я вижу такой код, я часто вспоминаю Жозефину Павловну. Мне это не подходит.
Способ 1 — не в лоб, а по лбу
Тут мне на помощь пришел тот факт, что для идентификации сущностей, я использую я использую UUID (вообще всем рекомендую — не один раз выручит). А это значит, что я могу создавать валидные сущности прямо на клиенте, связывать их между собой по идентификатору, и отправлять на бэк, по отдельности.
Тогда мы можем сделать что-то в духе:
input AccountInput {
login: String!
password: Password!
subject: SubjectSelectInput!
}
input SubjectSelectInput {
id: ID!
}
type Mutation {
createAccount(
organization: OrganizationInput,
person: PersonInput,
account: AccountInput!
): Account!
}или, что оказалось еще удобнее (почему это удобнее, я расскажу, когда мы доберемся до генерации пользовательских интерфейсов), разделить это на разные методы:
type Mutation {
createAccount(account: AccountInput!): Account!
createOrganization(organization: OrganizationInput!): Organization!
createPerson(person: PersonInput!) : Person!
}Тогда, нам нужно будет отправить запрос на createAccount и createOrganization/createPerson
одним батчем. Стоит отметить, что тогда обработку батча нужно обязательно обернуть в транзакцию.
Способ 2 — волшебный скаляр
Фишка в том, что скаляр в GraphQL, это не только Int, Sting, Float и т.д. Это вообще всё что угодно (ну, пока с этим может справится JSON, конечно).
Тогда мы можем просто объявить скаляр:
scalar SubjectInputПотом, написать на него свой обработчик, и не парится. Тогда мы сможем без проблем подсовывать вариативные поля на ввод.
Какой из способов выбрать? Я использую оба, и выработал для себя такое правило:
Если родительская сущность является Aggregate Root для дочерней, то я выбираю второй способ, иначе — первый.
4. Дженерики.
Тут всё банально и ничего лучше генерации кода я не придумал. И без Рельсы (пакет railt/sdl) я бы не справился (точнее, сделал бы тоже самое но с костылями). Фишка в том, что Рельса позволяет определять директивы уровня документа (в спеке нет такой позиции для директив).
directive @example on DOCUMENTТо есть, директивы непривязанные, к чему либо, кроме документа, в котором они вызваны.
Я ввел такие директивы:
directive @defineMacro(name: String!, template: String!) on DOCUMENT
directive @macro(name: String!, arguments: [String]) on DOCUMENTДумаю, что объяснять суть макросов никому не нужно...
На этом пока всё. Не думаю, что этот материал вызовет столько же шума, как прошлый. Всё таки заголовок там был довольно "желтым" )
В комментариях к прошлому материалу хабровчане топили за разделение доступа… значит следующий материал будет об авторизации.
