Как GraphQL превращает запрос в ответ

В этом посте я собираюсь ответить на один простой вопрос: Как сервер GraphQL превращает запрос в ответ?

Если вы новичок в GraphQL, прочитайте трехминутное введение в Как мне использовать GraphQL? прежде чем читать дальше. Так вы получите больше удовольствия от чтения этой публикации.

Вот что мы расскажем в этом посте:

  • Запросы GraphQL
  • Схема и функции разрешения
  • Выполнение GraphQL - шаг за шагом

Готовый? Давайте прямо сейчас!

Запросы GraphQL

Запросы GraphQL имеют очень простую структуру и просты для понимания. Возьми вот это:

{
  subscribers(publication: "apollo-stack"){
    name
    email
  }
}

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

{
  subscribers: [
    { name: "Jane Doe", email: "[email protected]" },
    { name: "John Doe", email: "[email protected]" },
    ...
  ]
}

Обратите внимание, что форма ответа почти такая же, как у запроса. Клиентская часть GraphQL настолько проста, что практически не требует пояснений!

А как насчет сервера? Это сложнее?

Оказывается, серверы GraphQL тоже довольно простые. Прочитав этот пост, вы будете точно знать, что происходит на сервере GraphQL, и будете готовы создать свой собственный.

Функции схемы и разрешения

Каждый сервер GraphQL состоит из двух основных частей, которые определяют, как он работает: схема и функции разрешения.

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

В нотации схемы GraphQL это выглядит так:

type Author {
  id: Int
  name: String
  posts: [Post]
}
type Post {
  id: Int
  title: String
  text: String
  author: Author
}
type Query {
  getAuthor(id: Int): Author
  getPostsByTitle(titleContains: String): [Post]
}
schema {
  query: Query
}

Эта схема довольно проста: в ней указано, что приложение имеет три типа - Автор, Публикация и Запрос. Третий тип - Запрос - - предназначен только для того, чтобы отметить точку входа в схему. Каждый запрос должен начинаться с одного из своих полей: getAuthor или getPostsByTitle. Вы можете думать о них как о конечных точках REST, за исключением более мощных.

Автор и Сообщение ссылаются друг на друга. Вы можете перейти от Автора к Опубликовать с помощью поля Автор «сообщений» , и вы можете получить из Опубликовать для Автора с помощью поля Записи «Автор» .

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

Вот для чего нужны функции разрешения.

Разрешить функции

Функции Resolve похожи на маленькие маршрутизаторы. Они указывают, как типы и поля в схеме связаны с различными серверными модулями, отвечая на вопросы «Как мне получить данные для авторов?» и «Какой бэкэнд мне нужно вызвать с какими аргументами, чтобы получить данные для сообщений?».

Функции разрешения GraphQL могут содержать произвольный код, что означает, что сервер GraphQL может взаимодействовать с любым сервером, даже с другими серверами GraphQL. Например, тип Автор может храниться в базе данных SQL, а Записи - в MongoDB или даже обрабатываться микросервисом.

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

Вот пример двух функций разрешения:

getAuthor(_, args){
  return sql.raw('SELECT * FROM authors WHERE id = %s', args.id);
}
posts(author){
  return request(`https://api.blog.io/by_author/${author.id}`);
}

Конечно, вы не стали бы писать запрос или URL-адрес непосредственно в функции разрешения, вы бы поместили их в отдельный модуль. Но вы понимаете.

Выполнение запроса - шаг за шагом

Хорошо, теперь, когда вы знаете о схемах и функциях разрешения, давайте посмотрим на выполнение реального запроса.

Дополнительное примечание: приведенный ниже код предназначен для GraphQL-JS, эталонной реализации GraphQL на JavaScript, но модель выполнения одинакова на всех известных мне серверах GraphQL.

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

Вот запрос, который работает со схемой, представленной ранее. Он извлекает имя автора, все сообщения этого автора и имя автора каждого сообщения.

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above
      }
    }
  }
}

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

Вот три основных шага, которые сервер предпринимает для ответа на запрос:

  1. Разобрать
  2. Подтвердить
  3. Выполнять

Шаг 1. Анализ запроса

Сначала сервер анализирует строку и превращает ее в AST - абстрактное синтаксическое дерево. Если есть какие-либо синтаксические ошибки, сервер остановит выполнение и вернет синтаксическую ошибку клиенту.

Шаг 2. Проверка

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

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

  • getAuthor - это поле типа Query?
  • принимает ли getAuthor аргумент с именем id?
  • Возвращаются ли поля name и posts для типа с помощью getAuthor?
  • … И многое другое…

Как разработчику приложения, вам не нужно беспокоиться об этой части, потому что сервер GraphQL делает это автоматически. Поместите это в отличие от большинства RESTful API, где вы - разработчик - должны убедиться, что все параметры действительны.

Шаг 3. Выполнение

Если проверка пройдена, сервер GraphQL выполнит запрос.

Каждый запрос GraphQL имеет форму дерева, то есть никогда не бывает круглым. Выполнение начинается с корня запроса. Сначала исполнитель вызывает функцию разрешения полей на верхнем уровне - в данном случае просто getAuthor - с предоставленными параметрами. Он ожидает, пока все эти функции разрешения не вернут значение, а затем продолжает каскадно вниз по дереву. Если функция разрешения возвращает обещание, исполнитель будет ждать, пока это обещание не будет разрешено.

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

Процесс выполнения в виде диаграммы:

Последовательность выполнения в табличной форме:

3.1: run Query.getAuthor
3.2: run Author.name and Author.posts (for Author returned in 3.1)
3.3: run Post.title and Post.author (for each Post returned in 3.2)
3.4: run Author.name (for each Author returned in 3.3)

Последовательность выполнения в текстовой форме (со всеми подробностями):

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

Еще раз для удобства:

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above
      }
    }
  }
}

В этом запросе есть только одно корневое поле - getAuthor - и один параметр - id - со значением 5. Функция разрешения getAuthor побежит и вернет обещание.

getAuthor(_, { id }){
  return DB.Authors.findOne(id);
}
// let's assume this returns a promise that then resolves to the
// following object from the database: 
{ id: 5, name: "John Doe" }

Обещание выполняется, когда возвращается вызов базы данных. Как только это произойдет, сервер GraphQL примет возвращаемое значение этой функции разрешения - в данном случае объект - и передаст его функциям разрешения name и posts полей автора, потому что это те поля, которые были запрошены в запросе. Функции разрешения name и posts выполняются параллельно:

name(author){
  return author.name;
}
posts(author){
  return DB.Posts.getByAuthorId(author.id);
}

Функция разрешения name довольно проста: она просто возвращает свойство name объекта author, которое только что было передано из функции разрешения getAuthor.

Функция разрешения posts вызывает базу данных и возвращает список объектов post:

// list returned by DB.Posts.getByAuthorId(5)
[{
  id: 1,
  title: "Hello World",
  text: "I am here",
  author_id: 5
},{
  id: 2,
  title: "Why am I still up at midnight writing this post?",
  text: "GraphQL's query language is incredibly easy to ...",
  author_id: 5
}]

Примечание. GraphQL-JS ожидает разрешения / отклонения всех обещаний в списке, прежде чем вызывать функции разрешения следующего уровня.

Поскольку запрос запрашивает поля title и author каждого сообщения, GraphQL затем запускает четыре функции разрешения параллельно: title и автор для каждого сообщения.

Функция разрешения title снова тривиальна, а функция разрешения author такая же, как и для getAuthor, за исключением того, что она использует author_id в публикации, тогда как функция getAuthor использовала аргумент id:

author(post){
  return DB.Authors.findOne(post.author_id);
}

Наконец, исполнитель GraphQL снова вызывает функцию разрешения name для Author, на этот раз с объектами автора, возвращенными функцией разрешения автора Posts. Он запускается дважды - по одному разу для каждой публикации.

Готово! Все, что осталось сделать, это передать результаты в корень запроса и вернуть результат:

{
  data: {
    getAuthor: {
      name: "John Doe",
      posts: [
        {
          title: "Hello World",
          author: {
            name: "John Doe"
          }
        },{
          title: "Why am I still up at midnight writing this post?",
          author: {
            name: "John Doe"
          }
        }
      ]
    }
  }
}

Примечание. Этот пример был немного упрощен. Настоящий производственный сервер GraphQL будет использовать пакетирование и кеширование, чтобы уменьшить количество запросов к бэкэнду и избежать избыточных запросов, таких как выборка одного и того же автора дважды. Но это тема для другого поста!

Заключение

Как видите, как только вы погрузитесь в него, GraphQL довольно легко понять! Я думаю, что это замечательно, если учесть, насколько легко он упрощает такие вещи, как объединения, фильтрация, проверка аргументов, документация, которые сложно решить с помощью традиционных RESTful API.

Конечно, GraphQL - это гораздо больше, чем то, что я написал здесь, но это тема для будущих публикаций!

Если это заинтересовало вас попробовать GraphQL для себя, вам следует ознакомиться с нашим Руководством по серверу GraphQL или прочитать о использовании GraphQL на клиенте вместе с React + Redux.

Обновление 2018: понимание выполнения GraphQL с помощью Apollo Engine

За время, прошедшее с тех пор, как Джонас написал этот пост, мы также создали сервис под названием Apollo Engine, который помогает разработчикам понимать и отслеживать, что происходит на их сервере GraphQL, предоставляя:

Если вы хотите увидеть выполнение ваших запросов GraphQL в реальной жизни, вы можете войти в систему и настроить свой сервер здесь. Если вы заинтересованы в поддержке высокопроизводительного современного приложения с GraphQL, мы можем помочь! "Дайте нам знать".