Объяснение вложенных мутаций
Мутации — это операции, которые могут изменять данные на сервере GraphQL: например, создавать запись, обновлять имя пользователя, добавлять комментарий к записи и т. д.
В GraphQL мутации доступны только через тип MutationRoot, вот так:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(Схема GraphQL в этом руководстве приводится для иллюстрации примеров; она отличается от схемы, предоставляемой плагином.)
С этой схемой изменить имя пользователя можно следующим образом:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Мутации доступны только через mutation root object type, чтобы гарантировать их последовательное выполнение, как поясняет спецификация GraphQL:
It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.
Термин «последовательное выполнение» противопоставляется «параллельному выполнению», которое в остальных случаях является рекомендуемым поведением при разрешении полей.
Например, в приведённом ниже query не важно, какое поле (name или email) сервер GraphQL разрешит первым, — они могут разрешаться параллельно:
query {
user(by: { id: 37 }) {
name
email
}
}Мутации же изменяют данные, поэтому порядок разрешения полей важен — следовательно, они должны выполняться последовательно (иначе могут возникнуть race conditions).
Например, два приведённых ниже queries дадут разные результаты:
# Query 1: after execution, user name will be "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Query 2: after execution, user name will be "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}Следствием доступности мутаций только через MutationRoot является то, что этот тип становится крайне перегруженным: он содержит поля, не имеющие между собой ничего общего, кроме необходимости последовательного выполнения (что является техническим требованием, а не решением в области проектирования интерфейса).
Аргументы в пользу вложенных мутаций
Из приведённых выше мутаций только createPost действительно принадлежит типу MutationRoot, поскольку создаёт новый элемент из ничего. Мутации updateUserName и addCommentToPost, однако, вполне могут иметь эквивалентные операции, применяемые к существующей сущности другого типа:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}С такой схемой изменить имя пользователя можно было бы следующим образом:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}Эта возможность называется «nested mutations» (вложенные мутации): применение мутации к результату другой операции — будь то query или мутация.
Обратите внимание, как использование вложенных мутаций делает схему GraphQL более элегантной:
- Тогда как операция
MutationRoot.updateUserNameдолжна получатьIDпользователя, её эквивалентUser.updateName— нет, поскольку она уже выполняется в контексте сущности пользователя - Имя поля сокращается с
updateUserNameдоupdateName
Кроме того, сервис GraphQL становится проще и понятнее: мы можем перемещаться между сущностями в графе для изменения их данных так же, как и для их запроса.
Вложенные мутации могут уходить вглубь на несколько уровней. Например, можно добавить комментарий к только что созданной записи — всё в рамках одного query:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}Помимо этого, вложенные мутации могут повысить производительность, сократив задержки при передаче данных: вместо выполнения нескольких queries для изменения ряда элементов достаточно одного query.
Почему вложенные мутации не входят в спецификацию
Спецификация GraphQL создана для работы со всеми реализациями серверов GraphQL на любом языке. Однако её главной движущей силой является JavaScript через graphql-js — эталонную реализацию.
Иными словами, любая функциональность, которую невозможно поддержать в graphql-js, не войдёт в спецификацию.
Поскольку JavaScript поддерживает promises, параллельное разрешение полей оказалось возможным, и параллелизм стал одним из фундаментальных принципов при первоначальном проектировании graphql-js, что нашло отражение в DataLoader (уровне получения данных), чьи функции пакетной обработки возвращают JavaScript promises.
Преимущества параллельного выполнения для производительности слишком значительны, а вложенные мутации несовместимы с параллелизмом. Было принято решение, что обмен параллельного выполнения на вложенные мутации нецелесообразен.
Вложенные мутации и производительность
В плагине Gato GraphQL поля всегда разрешаются последовательно, а порядок их разрешения детерминирован. (Эта особенность не влияет на производительность разрешения query, поскольку сервер сначала преобразует граф в query в компонентную модель, которая разрешается за оптимальное линейное время.)
Это означает, что плагин может поддерживать вложенные мутации, предоставляя все их преимущества и не испытывая ни одного из их недостатков.
Спецификация GraphQL
В настоящее время эта функциональность не является частью спецификации GraphQL, однако соответствующие предложения были поданы в: