До сих пор мы только читали данные. Но почти любое приложение должно их и менять: создать пост, исправить опечатку, удалить комментарий. В GraphQL для этого есть мутации (mutations). Представьте магазин. Query — это осмотр витрины: вы смотрите и ничего не меняете. Mutation — это касса: вы что-то меняете в мире (покупаете, оформляете, возвращаете) и сразу получаете чек — подтверждение с тем, что получилось. В мутации GraphQL «чек» — это объект, который вы просите вернуть в ответе. А касса, в отличие от витрины, обслуживает по очереди — об этом тоже поговорим. В этом уроке мы напишем мутации для CRUD постов нашего блога.
Чем мутация отличается от query
Синтаксически мутация почти не отличается от запроса: вместо query пишется mutation, а внутри — те же поля, аргументы, переменные, алиасы и фрагменты. Различия — в смысле и в одной технической детали:
query | mutation | |
|---|---|---|
| Для чего | читать данные | создавать, менять, удалять |
| Корневой тип | Query | Mutation |
| Поля верхнего уровня | могут выполняться параллельно | выполняются строго по очереди |
| Типичные имена | существительные: posts, user | глагол и существительное: createPost |
| Повтор и кеш | безопасно повторять и кешировать | повтор повторит и изменение |
Договорённость «query читает, mutation пишет» сервер технически не проверяет: резолвер запроса тоже мог бы что-то записать. Но на неё рассчитывает вся экосистема — клиенты, кеши, инструменты.
⚠️ Не меняйте данные внутри
query. Клиент вправе перезапросить query в любой момент, закешировать её результат или выполнить поля параллельно — и «безобидное» чтение вдруг начнёт что-то менять, причём в непредсказуемом порядке.
Первая мутация: создаём пост
Построим мутацию по шагам.
Шаг 1. Входные данные. У мутации обычно много параметров: заголовок, текст, автор. Вместо россыпи аргументов их собирают в один объект input — типы input мы встречали в уроке про схему. Для создания поста и для его частичного изменения нужны два типа:
input CreatePostInput {
title: String!
body: String!
published: Boolean = false
authorId: ID!
}
input UpdatePostInput {
title: String
body: String
published: Boolean
}В CreatePostInput обязательные поля помечены !, а published по умолчанию равно false. В UpdatePostInput все поля необязательны: клиент отправляет только то, что хочет изменить, а остальное сервер не трогает. Поле authorId пока приходит от клиента; в уроке про авторизацию автора будут определять по вошедшему пользователю — доверять клиенту в вопросе «кто я» нельзя.
Шаг 2. Результат. Что вернуть из мутации? Можно сам Post, но тогда негде сообщить об ошибке валидации. Поэтому принято возвращать специальный тип — payload («результат операции»):
type UserError {
field: String
message: String!
}
type CreatePostPayload {
post: Post
errors: [UserError!]!
}
type UpdatePostPayload {
post: Post
errors: [UserError!]!
}
type DeletePostPayload {
deletedPostId: ID
errors: [UserError!]!
}