Остерегайтесь вложенных мутаций GraphQL!

osteregajtes vlozhennyh mutaczij graphql

«У меня есть хитрый план…»

Когда-то я столкнулся с идеей организации мутаций GraphQL путем вложения операций в тип возврата. Идея заключалась в том, что эти операции затем мутируют родительскую сущность.

Основная идея была такова:

input AddBookInput {
            ISBN: String!
            title: String!
        }
        
input RemoveBookInput {
            bookId: Int!
        }
        
input UpdateBookInput {
          ISBN: String!
          title: String!
      }
      
type AuthorOps {
          addBook(input: AddBookInput!): Int
          removeBook(input: RemoveBookInput! ): Boolean
          updateBook(input: UpdateBookInput!): Book
      }
      
type Mutation {
        Author(id: Int!): AuthorOps
      }

И я пользовался этой техникой несколько раз без дурного эффекта, но мне повезло. Где проблема?

Читатель указал мне на проблему на сайте GraphQL GitHub, где было указано, что порядок выполнения вложенные мутации не гарантируется. Ой-ой. В вышеприведенном случае я точно хочу addBook() возникла мутация перед попыткой updateBook() операция над той же книгой. К сожалению, только т.н. корень мутации гарантированно производятся в порядке.

Иллюстрация проблемы

Скажем, у меня есть черед сообщений, где я хочу, чтобы сообщения хранились в том порядке, в котором они были получены. Некоторые сообщения обрабатываются дольше, поэтому я использую мутацию, чтобы гарантировать, что сообщения обрабатываются последовательно:

type Query {
  noop: String!
}

type Mutation {
  message(id: ID!, wait: Int!): String!
}

Резолвер регистрирует при поступлении сообщения, затем ждет определенное время, прежде чем вернуть результат мутации:

const msg = (id, wait) => new Promise(resolve => {
  setTimeout(() => {
    
console.log({id, wait})
    let message = `response to message ${id}, wait is ${wait} seconds`;
    
resolve(message);
  }, wait)
})

const resolvers = {
  Mutation: {
    message: (_, {id, wait}) => msg(id, wait),
  }
}

Теперь пробный запуск. Я хочу убедиться, что сообщения журнала консоли расположены в том же порядке, что и запросы на мутацию. Вот запрос:

mutation root {
  message1: message(id: 1, wait: 3000)
  message2: message(id: 2, wait: 1000)
  message3: message(id: 3, wait: 500)
  message4: message(id: 4, wait: 100)
}

Ответ таков:

{
  "data": {
    "message1": "response to message 1, wait is 3000 seconds",
    "message2": "response to message 2, wait is 1000 seconds",
    "message3": "response to message 3, wait is 500 seconds",
    "message4": "response to message 4, wait is 100 seconds"
  }
}

А в журнале консоли написано:

{ id: '1', wait: 3000 }
{ id: '2', wait: 1000 }
{ id: '3', wait: 500 }
{ id: '4', wait: 100 }

Прекрасно! Сообщения обрабатываются в том порядке, в котором они получены, даже если второе и последующие сообщения занимают меньше времени, чем предыдущие. Другими словами, мутации выполняются последовательно.

Муха в дези

Теперь давайте вставим эти операции и посмотрим, что получится. Сначала я определяю а MessageOps введите, а затем добавьте a Вложенный мутация:

const typeDefs = `
type Query {
  noop: String!
}

type MessageOps {
  message(id: ID!, wait: Int!): String!
}

type Mutation {
  message(id: ID!, wait: Int!): String!
  Nested: MessageOps
}`

Мои мутации теперь проходят через вложенный резольвер, возвращая MessageOps, который затем использую для выполнения мутации сообщения:

mutation nested {
  Nested {
    message1: message(id: 1, wait: 3000)
    message2: message(id: 2, wait: 1000)
    message3: message(id: 3, wait: 500)
    message4: message(id: 4, wait: 100)
  }
}

Достаточно похож на то, что у нас было раньше, и ответ на запрос на мутацию выглядит почти так же:

{
  "data": {
    "Nested": {
      "message1": "response to message 1, wait is 3000 seconds",
      "message2": "response to message 2, wait is 1000 seconds",
      "message3": "response to message 3, wait is 500 seconds",
      "message4": "response to message 4, wait is 100 seconds"
    }
  }
}

Единственное отличие состоит в том, что ответы упаковываются во вложенный объект JSON. К сожалению, консоль раскрывает историю горя.

{ id: '4', wait: 100 }
{ id: '3', wait: 500 }
{ id: '2', wait: 1000 }
{ id: '1', wait: 3000 }

Это показывает, что сообщения обрабатываются не в последовательности: сообщения с быстрой обработкой публикуются первыми.

хорошо. В коде из моей начальной публикации я сделал нечто большее похожее на следующее:

mutation nested2 {
  Nested {
    message1: message(id: 1, wait: 3000)
  }
  Nested {
    message2: message(id: 2, wait: 1000)
  }
  Nested {
    message3: message(id: 3, wait: 500)
  }
  Nested {
    message4: message(id: 4, wait: 100)
  }
}

Может, это работает? Каждая операция мутации выполняется в собственной вложенной корневой мутации, поэтому мы ожидаем, что вложенные мутации будут выполняться последовательно. Ответ идентичен предыдущему:

{
  "data": {
    "Nested": {
      "message1": "response to message 1, wait is 3000 seconds",
      "message2": "response to message 2, wait is 1000 seconds",
      "message3": "response to message 3, wait is 500 seconds",
      "message4": "response to message 4, wait is 100 seconds"
    }
  }
}

Но журнал консоли также:

{ id: '4', wait: 100 }
{ id: '3', wait: 500 }
{ id: '2', wait: 1000 }
{ id: '1', wait: 3000 }

Итак, что здесь происходит?

«Проблема» состоит в том, что GraphQL выполняет вложенную мутацию, возвращая объект с последующими методами мутации. К сожалению, по возвращении этого объекта GraphQL переходит к следующему запросу на мутацию, не зная, что в запросе необходимо выполнить дополнительные операции мутации.

GraphQL элегантно прост, но прост стоит. Вполне возможно, что вложенные мутации могут поддерживаться, скажем, путем добавления a мутатор типа (его следствием будет введение type), который GraphQL будет рассматривать как расширение операции мутации. На данный момент в запросе на мутацию просто недостаточно информации, чтобы знать, что вложенные операции также являются мутаторами.

Организация мутаций GraphQL, часть 2

Вы все еще можете использовать эту технику для операций, не зависящих от последовательности, но это предположение, вероятно, будет хрупким и трудно отладить, если его нарушить. Возможно, вышивание по схеме или плетение дает ответ. Я надеюсь изучить эти подходы в следующей публикации.

Полную программу NodeJS, использованную для этой публикации, можно найти здесь.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *