Рубріки: Теория

Введение в GraphQL: только самое важное

Сергей Почекутов

GraphQL — язык запросов для взаимодействия клиента и сервера, а также среда исполнения этих запросов. Разработан как внутренний проект Facebook в 2012 году, с 2015 года в открытом доступе, с 2018 года развивается GraphQL Foundation в статусе Open Source.

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

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

Содержание:
1. Особенности GraphQL и сравнение с REST API
2. Система типов
3. Схема GraphQL
4. Запросы GraphQL
5. Взаимодействие клиента с сервером
Заключение

1. Особенности GraphQL и сравнение с REST API

Без сравнений с REST API не обойтись, потому что GraphQL изначально разрабатывался как более эффективная альтернатива этой архитектуре.

Но сначала — немного про особенности самого GraphQL:

  • Строгая типизация — оценить правильность запроса можно до его выполнения.
  • Универсальность — можно использовать с любым фреймворком, языком программирования. Источник данных также не имеет значения.
  • Простое масштабирование — позволяет добавлять новые типы и поля, не трогая существующие запросы и не создавая несколько версий одного и того же API.

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

При использовании REST API контролировать набор получаемых данных гораздо сложнее. Например, если нужно найти всех пользователей с именем Highload, то REST предлагает два подхода:

  • Определить эндпойнт, который будет отдавать всех пользователей с именем Highload.
  • Определить общий эндпойнт, который будет отдавать всех пользователей, а фильтрацию по имени выполнять самостоятельно уже на стороне клиента.

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

GraphQL легко решает эти проблемы. Эндпойнт один, лишних данных в ответе нет.

2. Система типов

В основе схемы GraphQL лежит описание типов. Чтобы сделать инструмент универсальным, разработчики предложили Schema Language. Ниже — основные поддерживаемые типы данных.

Объектные

Объектные типы — базовые типы, которые представляют собой объект и набор описывающих его полей.

Пример:

type Planet {
    id: ID!
    diameter: Int
    name: String!
    population: Float
    residents: [Person!]
}

Скалярные

Поля у объектов могут быть разных типов, но в итоге они должны быть приведены к одному из поддерживаемых скалярных типов:

  1. Int — 32-битное целое число со знаком.
  2. Float — число двойной точки с плавающей точкой со знаком.
  3. String — строка в кодировке UTF-8.
  4. Booleantrue или false.

Есть также специальный скалярный тип ID. Это уникальный идентификатор, который обычно используют, чтобы получить объект. Он также может быть ключом в кеше.

Значения ID сериализуется так же, как String. Но он используется не для отображения клиенту, а только в программах.

Кроме того, во многих имплементациях сервисов GraphQL можно создавать свои скалярные типы.

Аргументы

Аргументы — это пара «ключ — значение», привязанные к полю. Они могут быть литералами и переменными, их можно применять на любых полях. Аргументы должны быть именованными, могут быть опциональными и обязательными.

Например, здесь к полю привязан аргумент id, который указывает на конкретную книгу:

query {
    Book(id:"13678hhh") {
        id
        title
        characters {
            name
        }
    }
}

Перечисления

Специальный тип, ограниченный набором значений. Например:

enum HAIR_COLOR {
    BLACK
    BLONDE
    BROWN
    GREY
}

Интерфейсы

Интерфейсы — абстрактный тип, который включает набор обязательных полей. В этих полях указываются типы, наследующие интерфейс. Например:

interface Character {
    id: ID!
    name: String!
}
type Animal implements Character {
    id: ID!
    name: String!
    function: String
}
type Person implements Character {
    id: ID!
    name: String!
    starships: [Starship]
}

Есть также другой абстрактный тип, который не включает обязательные поля — union.

Например:

union SearchResult = Person | City | Planet
... on Person {
    name
    height
}
... on City {
    name
    population
    manufacturer
}
... on Planet {
    galaxy
    system
}

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

Query

Query — тип, который реализует запросы в GraphQL. По сути, это аналог GET в REST API. Каждый запрос можно представить в качестве строки, которая отправляется в теле HTTP POST-запрос. Это важное уточнение — все типы запросов в GraphQL отправляются через POST.

Query описывает, какие данные нужно получить с сервера. Например, у нас есть список пользователей, мы хотим узнать их имена и возраст:

query {
  users {
    fname
    age
  }
}

Ответ приходит в формате JSON. Структура соответствует тому, что клиент запросил в запросе:

data : {
  users [
    {
      "fname": "High",
      "age": 39
    },
    {
      "fname": "Load",
      "age": 27
    }
  ]
}

При успешном выполнении операции возвращается JSON с ключами data или error. Неуспешные операции возвращают JSON с ключом и уведомлением об ошибке. Такой подход упрощает обработку ошибок на стороне клиента.

Mutation

Тип mutation добавляет данные. Это аналог POST и PUT в REST. Например, этот запрос добавит в БД пользователя с именем Highload и возрастом 10.

mutation createUser{
  addUser(fname: "Highload", age: 10) {
    id
  }
}

После выполнения запроса должен прийти ответ с идентификатором записи:

data : {
  addUser : "f374uw"
}

Subscription

Тип subscription позволяет слушать изменения в базе данных в реалтайме. Подписки используют для этого вебсокеты.

Пример:

subscription listenLikes {
  listenLikes {
    fname
    likes
  }
}

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

3. Схема GraphQL

В основе GraphQL API используется схема. Она описывает, какие типы данных поддерживаются и какие типы данных возвращаются в ответ на запрос.

Работа с сервером GraphQL всегда начинается с создания схемы. Она включает два взаимосвязанных объекта: TypeDefs и Resolvers.

TypeDefs

В схеме необходимо определить типы, с которым может работать клиент. Это делается с помощью объекта TypeDefs.

const typeDefs = gql`
  type User {
    id: Int
    fname: String
    age: Int
    likes: Int
    posts: [Post]
  }

  type Post {
    id: Int
    user: User
    body: String
  }

  type Query {
    users(id: Int!): User!
    posts(id: Int!): Post!
  }
  type Mutation {
    incrementLike(fname: String!) : [User!]
  }

  type Subscription {
    listenLikes : [User]
  }
`;

Сначала мы определяем тип User. Для него доступны данные id, fname, age, likes, posts. Это значит, что клиент может получить, например, возраст пользователя или увидеть все опубликованные этим пользователем посты.

Есть также тип Post, у которого указаны id, user и body.

Для каждого поля определяется тип данных. Всего GraphQL поддерживает четыре типа данных: String, Int, Float, Boolean. Если добавить в поле восклицательный знак, то оно становится обязательным.

В схеме также определены типы Query, Mutation и Subscription.

  • Внутри Query два типа — users и posts. Это обязательные поля. Работают они одинаково — принимают id и возвращают пользователя с соответствующим идентификатором.
  • В Mutation располагается тип incrementLike. Он принимает параметр fname и возвращает перечень пользователей.
  • В Subscription располагается listenLikes. В этом примере он также возвращает список пользователей.

После определения типов добавляется логика, которая определяет, как сервер будет отвечать на запросы клиентов. За эту часть работы отвечают распознаватели (Resolvers).

Resolvers

Resolver — функция, возвращающая данные для конкретного поля. Вернуть можно только те данные, которые определены в схеме объектом TypeDefs. Распознаватели могут быть асинхронными.

Пример функций-распознавателей:

const resolvers = {
  Query: {
    // Вернуть объект пользователя, который соответствует переданному id
    users(root, args) { return users.filter(user => user.id === args.id)[0] }, 
                // Вернуть объект поста, который соответствует переданному id
    posts(root, args) { return posts.filter(post => post.id === args.id)[0] }
  },

  User: {
    // Принять данные пользователя и вернуть его посты
    posts: (user) => {
      return posts.filter(post => post.userId === user.id)
    }
  },

  Post: {
    // Принять данные поста и вернуть пользователя, который его опубликовал
    user: (post) => {
      return users.filter(user => user.id === post.userId)[0]
    }
  },
  Mutation: {
    // Изменить объект users: увеличить количество лайков 
    //для пользователя с выбранным fname
    incrementLike(parent, args) {
      users.map((user) => {
        if(user.fname === args.fname) user.likes++
        return user
      })
      pubsub.publish('LIKES', {listenLikes: users});
      return users
    }
  },
  Subscription: {
    // Слушать LIKES и отвечать при обновлении pubsub
    listenLikes: {
      subscribe: () => pubsub.asyncIterator(['LIKES'])
    }
  }
};

Это пример простой схемы, которая определяет, как и какие данные будет получать клиент от GraphQL-сервера.

4. Запросы GraphQL

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

Поля

Пример простого запроса:

{
  user {
    name
  }
}

Ответ на запрос выглядит так:

{
  "data": {
    "user": {
      "name": "highload"
    }
  }
}

В запросе два поля. Поле user вернуло объект с типом String.

Аргументы

В запросе можно указать на конкретного пользователя, используя аргумент. Например:

{
  user(id: "2") {
    age
  }
}

Здесь мы хотим получить возраст пользователя с id = 2. Вместо id в качестве аргумента можно использовать name, если мы знаем, что в схеме описана функция для обработки такого значения.

Можно использовать несколько аргументов. Например, установить лимит на количество постов:

{
  user(id: "2") {
    name
    posts(limit: 10)
  }
}

Псевдонимы

Для удобства можно назначать полям алиасы — псевдонимы. Пример запроса:

{
  accholder: user(id: "22") {
    firstname: name
  }
}

В ответе будет указан псевдоним:

{
  "data": {
    "accholder": {
      "firstname": "high"
    }
  }
}

Алиасы помогают избежать конфликтов при совпадении названий полей.

Фрагменты

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

Например:

fragment comparisonFields on tweet {
  userName
  userHandle
  date
  body
  repliesCount
  likes
}

Ответ:

"data": {
    "sample": {
      userName: "highload",
      userHandle: "@highload",
      date: "2022-05-01",
      body: "Good",
      repliesCount: 11,
      tweetsCount: 300,
      likes: 568,
    },

Переменные

Значения внутри запроса можно сделать динамическими, используя переменные. Пример запроса:

query GetAccHolder($id: String) {
  accholder: user(id: $id) {
    fullname: name
  }
}
{
  "id": "1"
}

Здесь мы создаем именованную функцию. Это может быть полезным в приложениях, в которых выполняется много запросов.

Можно задать значение переменной по умолчанию:

query GetAccHolder($id: String = "1") {
  accholder: user(id: $id) {
    fullname: name
  }
}

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

query GetAccHolder($id: String!) {
  accholder: user(id: $id) {
    fullname: name
  }

Директивы

Концепция переменных позволяет использовать директивы, которые динамически меняют структуру и форму запросов в зависимости от условий. В GraphQL используются две директивы: @include и @skip.

Пример использования @include:

query GetFollowers($id: String) {
  user(id: $id) {
    fullname: name,
    followers: @include(if: $getFollowers) {
      name
      userHandle
      tweets
    }
  }
}
{
  "id": "1",
  "$getFollowers": false
}

Здесь мы говорим, что нужно использовать поле, если это true. Видим, что у $getFollowers значение false, поэтому поле не включается в ответ.

Пример использования @skip:

query GetFollowers($id: String) {
  user(id: $id) {
    fullname: name,
    followers: @skip(if: $getFollowers) {
      name
      userHandle
      tweets
    }
  }
}
{
  "id": "1",
  "$getFollowers": true
}

Здесь мы говорим, что нужно пропустить поле, если это true. Видим, что у $getFollowers значение true, поэтому поле не включается в ответ.

5. Взаимодействие клиента с сервером

Кратко порядок взаимодействия можно описать так:

  1. Клиент отправляет на GraphQL-сервер запрос на чтение или изменение данных. Этот запрос составлен в соответствии с утвержденной заранее схемой.
  2. GraphQL-сервер распознает запрос по специальным функциям — резолверам — и получает данные по запрашиваемым полям.
  3. Клиент получает ответ с запрашиваемой структурой данных, обычно в формате JSON.

Клиенту не важно, откуда поступают запрошенные им данные. Он делает запрос в нужном ему объеме к серверу GraphQL. Сервер может работать с любыми источниками: базами данных, результатами поиска, Docker-контейнерами.

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

Заключение

Мы разобрались с GraphQL, что это язык запросов, посмотрели систему типов, пример схемы и порядок общения между клиентами и сервером. Также мы сравнили GraphQL API и REST API.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023