Разбор запросов GraphQL от простого к сложному — queries и mutations, аргументы, переменные, вложенные схемы, подписки. С примерами и кодом схемы.



Привет, Хабр!


Про суть GraphQL написано много, про сами запросы меньше. С них и начнем — от простого запроса к вложенным схемам и подпискам.


Среды тестирования


  • GraphiQL — встроенный пакет для тестирования на сервере.
  • Apollo Client Developer Tools — тот же GraphiQL в виде расширения Chrome.

Документация схемы


Схема сама себя документирует. После изменений на сервере справка по запросам обновляется автоматически и подсвечивает ошибки в самих запросах.



Два базовых типа операций — queries (чтение) и mutations (запись).


Queries


Запросом обращаемся к полю user и перечисляем, какие поля вернуть — firstName и lastName.


query myQuery {
  user {
    firstName
    lastName
  }
}

В GraphiQL ключевое слово query перед скобками можно опустить. На клиенте тип операции нужно указывать явно, иначе запрос не соберется.


Mutations


Мутация меняет данные. Указываем операцию и ее аргументы.


mutation myMutation {
  userCreate(firstName: "Jane", lastName: "Doe")
}

Queries выполняются параллельно, mutations — последовательно, сверху вниз.


Subscriptions


Третий тип операций — подписки. Синтаксис как у queries, но вместо разового ответа клиент держит поток обновлений.


subscription mySubscription {
  user {
    firstName
    lastName
  }
}

Query и mutation отвечают один раз и закрывают запрос. Subscription живет в постоянном канале, обычно поверх WebSocket, и шлет данные по мере событий на сервере.


Аргументы


Аргументы подставляют значения в запрос и работают одинаково для query, mutation и subscription. Сначала объявляем аргумент, потом применяем к полю.


query myQuery($id: ID) {
  user(id: $id) {
    firstName
    lastName
  }
}

Запрос найдет пользователя с нужным id и вернет firstName и lastName.


Переменные


Значение аргумента передается отдельным блоком переменных. Тип переменной объявляется в сигнатуре запроса и должен совпадать с типом поля в схеме.



query myQuery($id: ID!) {
  user(id: $id) {
    firstName
    lastName
  }
}

Блок переменных:


{
  "id": "595fdbe2cc86ed070ce1da52"
}

Восклицательный знак в ID! делает поле обязательным.


На клиенте запрос формировать руками не нужно. Под популярные фреймворки есть готовые клиенты Apollo — vue-apollo, apollo/client для React, apollo-angular.


Вложенные запросы


Плоские поля — простой случай, реальные схемы вложенные. Пример на mongoose и graphql-js из проекта того времени.


Без вложенности:


// MongoDB schema
const schema = new mongoose.Schema({
  firstName: { type: String },
  lastName: { type: String },
})

export const USER_MODEL = mongoose.model('users', schema)

// GraphQL type
const user = {
  firstName: { type: GraphQLString },
  lastName: { type: GraphQLString },
}

export const USER = new GraphQLObjectType({
  name: 'User',
  fields: user,
})

С вложенной схемой:


// MongoDB schema
const schema = new mongoose.Schema({
  firstName: { type: String },
  lastName: { type: String },
  secure: {
    public_key: { type: String },
    private_key: { type: String },
  },
})

// ...

const secure = new GraphQLObjectType({
  name: 'Secure',
  fields: {
    public_key: { type: GraphQLString },
  },
})

export const USER = new GraphQLObjectType({
  name: 'User',
  fields: {
    firstName: { type: GraphQLID },
    secure: { type: secure },
  },
})

Запрос по вложенному полю:


query myQuery($id: ID!) {
  user(id: $id) {
    firstName
    secure {
      public_key
    }
  }
}

Мутация возвращает объект


Мутация не обязана возвращать true/false. Если типом ответа поставить тип из схемы вместо GraphQLBoolean, мутация вернет объект, как обычный query.



mutation auth($email: String!, $password: String!) {
  auth(email: $email, password: $password) {
    id
    secure {
      public_key
    }
  }
}

Где подписки реально пригодились


Синтаксис подписок простой, интереснее то, где они окупаются. В realtime-приложении на Vue Apollo и Apollo Server клиент и сервер держали постоянный канал по WebSocket и синхронизировали состояние на лету. Каталог с конечными позициями — забрали последний экземпляр, и он сразу пропадает у всех, у кого открыт список. Сделано на GraphQL-подписках Apollo поверх Redis PubSub, чтобы подписки жили на нескольких инстансах, даже если событие отработало на одном.


Ключевое отличие подписки от query не в синтаксисе, а в транспорте. Query отвечает один раз по HTTP, subscription держит соединение и требует WebSocket-слоя на сервере и клиенте, плюс шину событий, если инстансов больше одного.


Чего статья не покрывала


Статья 2017 года про форму запросов. За ее рамками осталось то, обо что упираешься на нагрузке.


  • N+1 на резолверах. Вложенный запрос по списку из N элементов легко превращается в N+1 обращений к базе — резолвер дергается на каждый элемент. Лечится батчингом на уровне резолверов, DataLoader собирает запросы за один тик в один запрос к базе. Тогда я об этом не думал, и это первое, что добавил бы сегодня.
  • Защита схемы. Клиент сам задает форму и глубину запроса, поэтому глубокий вложенный запрос — это вектор нагрузки. В проде нужны лимит глубины и сложности запроса и отключенная интроспекция. В статье этого нет, тогда не задавался вопросом.

Заключение


Базовые запросы GraphQL — queries, mutations, subscriptions, аргументы, переменные, вложенность — закрывают почти все, что нужно на старте. Синтаксисом список не заканчивается, на нагрузке добавляются N+1, защита схемы и транспорт под подписки.