Мутации: изменяем данные

Мутации — это операции записи в GraphQL: создать, обновить, удалить. Они описываются в корневом типе Mutation и почти всегда возвращают изменённый объект.

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

Всё, что меняет данные на сервере, описывают в корневом типе Mutation — он устроен так же, как Query, только его поля несут побочные эффекты. По соглашению имена мутаций — глаголы: createUser, updatePost, deleteComment.

type Mutation {
  createPost(title: String!, body: String!): Post!
  deletePost(id: ID!): Boolean!
}

Ключевая деталь: мутация почти всегда возвращает результат — обычно созданный или изменённый объект. Это позволяет клиенту одним запросом и изменить данные, и сразу получить актуальное состояние для обновления интерфейса. Сравни с REST, где после POST часто приходит лишь статус 201 и заголовок Location, и за свежими данными нужен отдельный GET. В GraphQL такой второй круг не нужен: ты прямо в мутации указываешь, какие поля результата хочешь увидеть, и сервер вернёт их в том же ответе. Более того, клиентские библиотеки умеют автоматически вписать возвращённый объект в свой кэш — и интерфейс обновится сам, без ручного перезапроса. Поэтому хорошая мутация — это не «выполни и забудь», а «выполни и верни ровно то, что нужно экрану».

Как выглядит вызов

mutation CreatePost($title: String!, $body: String!) {
  createPost(title: $title, body: $body) {
    id          # сервер вернул сгенерированный id
    title
    createdAt
  }
}

Обрати внимание: после вызова мутации в фигурных скобках мы выбираем поля результата — ровно как в запросе. То есть мутация = «выполни действие и верни мне вот эти поля результата».

Как работает под капотом

Сервер видит ключевое слово mutation, берёт корневой тип Mutation и вызывает резолвер поля. Важная гарантия спецификации: поля верхнего уровня мутации выполняются последовательно, одно за другим, в порядке их перечисления — чтобы избежать гонок при нескольких записях в одном запросе (поля же обычного запроса резолвятся параллельно). Смоделируем «хранилище» и резолверы мутаций:

// простое in-memory хранилище
const store = { posts: [], nextId: 1 };

function createPost(_parent, args) {
  const post = { id: store.nextId++, title: args.title, body: args.body };
  store.posts.push(post);
  return post;          // возвращаем созданный объект
}

function deletePost(_parent, args) {
  const before = store.posts.length;
  store.posts = store.posts.filter(p => p.id !== args.id);
  return store.posts.length < before;  // true, если удалили
}

console.log(createPost(null, { title: "Привет", body: "..." })); // {id:1,...}
console.log(createPost(null, { title: "Второй", body: "..." }));  // {id:2,...}
console.log(deletePost(null, { id: 1 }));                          // true
console.log(store.posts);                                          // остался id:2

Попробуй сам ▶ — вызови createPost ещё раз и проверь, как растёт nextId и наполняется store.posts. Это в точности логика серверного резолвера записи.

Частые ошибки

  • Не возвращать ничего полезного. Мутация, возвращающая только Boolean, заставляет клиента делать второй запрос за свежими данными. Возвращай изменённый объект.
  • Класть запись в тип Query. Любые побочные эффекты — только в Mutation. Поля Query должны быть «чистыми» (read-only).
  • Рассчитывать на параллельность. Несколько мутаций верхнего уровня в одном запросе выполнятся строго по очереди — это и плюс (нет гонок), и то, о чём надо помнить.

Best practices

  • Имена мутаций — глаголы в стиле «действие+сущность»: createOrder, publishPost, archiveUser.
  • Возвращай изменённый объект целиком, чтобы клиентский кэш обновился без дополнительного запроса.
  • Для сложных входных данных используй input-типы (следующий урок) вместо длинного списка отдельных аргументов.

Итоги

Мутации — операции записи в корневом типе Mutation: создать, обновить, удалить. Они выполняются последовательно и почти всегда возвращают изменённый объект, чтобы клиент сразу обновил состояние. Дальше структурируем входные данные мутаций через input-типы и научимся отдавать понятные ошибки.

Проверьте себя
1. Где описываются операции изменения данных?
AВ типе Query
BВ корневом типе Mutation
CВ типе Subscription
DВ кастомных скалярах
2. Почему мутация обычно возвращает изменённый объект?
AЭто требование HTTP
BЧтобы клиент сразу получил актуальные данные и не делал второй запрос
CИначе мутация не выполнится
DЧтобы скрыть ошибки