Концепции, идеи, стратегии
Концепции, идеи, стратегииОбъяснение вложенных мутаций

Объяснение вложенных мутаций

Мутации — это операции, которые могут изменять данные на сервере 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, однако соответствующие предложения были поданы в: