Pull to refresh
РСХБ.цифра (Россельхозбанк)
Меняем банк и сельское хозяйство

GraphQL: как сделать бэкенд приложения экономнее и быстрее

Reading time 10 min
Views 16K

Самый распространённый стандарт для обмена информацией внутри приложений — это REST API. Его все любят, но знают, что он не идеален. В этой статье обсудим его альтернативу — GraphQL. Мы расскажем, в чём преимущество GraphQL, как выглядят запросы и с чего начать.

Зачем был создан GraphQL, если уже есть REST

Есть две основные причины, по которым Facebook, Netflix и Coursera начали разрабатывать альтернативу REST:

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

  2. По мере роста использования мобильных устройств росло количество различных фронтенд-фреймворков и платформ, на которых работают клиентские приложения. Учитывая негибкость REST, было сложнее разработать единый API, который мог бы соответствовать требованиям каждого клиента.

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

Вследствие этого Facebook начал разработку GraphQL. В то же время Netflix и Coursera сами работали над альтернативными вариантами. После того, как Facebook выложил GraphQL в открытый доступ, Coursera прекратила свои усилия и перешла на новую технологию. Netflix, однако, продолжил разработку собственной альтернативы REST, а позже открыл исходный код Falcor.

Что такое GraphQL

GraphQL — это язык, используемый API для запроса информации из БД. Считается, что это промежуточный слой, поскольку данные затем могут быть отображены на внешней веб-странице. Ключевой момент здесь то, что GraphQL — это язык запросов для API. Обратите внимание на слово «API». Важно знать, что GraphQL — это не язык запросов к базам данных. Это распространённое заблуждение, вызывающее много споров. Цель GraphQL — быть языком запросов, позволяющим разработчикам собирать только те данные, которые им нужны в конкретный момент, и ничего кроме. А также стремиться сократить и оптимизировать время выполнения запросов.

GraphQL реализует систему, определяющую схему API, которая использует синтаксис языка определения схем (SDL). Это означает, что в рамках его синтаксиса можно создавать обязательные поля, строить таблицы в графовом виде или даже определять отношения между двумя таблицами.

Пример запроса (# - это комментарий) :

# В запросе мы указываем необходимые поля, которые хотим получить. 
# Исходная модель данных позволяет выбрать большой объём информации, 
# который не нужен для текущей работы
query{
  # Имя вызываемой функции. 
  # В данном случае — получение — мы можем получить авторов и всё, что с ними связано
  authors{
    # Поле уникального идентификатора
    id
    # Имя автора
    name
    # Книги автора
    books{
      # Поле уникального идентификатора книги
      id
      # Заголовок книги
      title
      # Идентификатор ISBN 10
      iSBN_10
      # Подробная информация о книге
      details{
        # Язык
        language
        # Разрешённый возраст
        readingAge
        # Отзывы
        reviews
      }
    }
  }
}

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

Сравнение GraphQL с REST

Если говорить о сходстве, то и REST, и GraphQL используются для построения API. Кроме того, оба они могут управляться через HTTP.

Примечательно, что ситуация с GraphQL и REST полностью идентична ситуации с реляционными и NoSQL базами данных пару лет назад.

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

Что касается различий, то REST в первую очередь является структурной концептуализацией для сетецентрического программного обеспечения, не имеет спецификации и определённого набора инструментов. Он больше сосредоточен на долговечности API, чем на оптимизации производительности.

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

1.Различие в получении данных и их структуре

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

В нашем примере это эндпоинт /author/{idAuthor} для получения исходных данных о авторе. 

Вероятно, так же будет конечная точка /author/{idAuthor}/book/{idBook}, которая возвращает конкретную книгу автора.

С другой стороны, в GraphQL вы просто отправляете один запрос на сервер GraphQL, который включает конкретные требования к данным. Затем сервер отвечает объектом JSON, в котором эти требования выполнены.

2.Использование шаблонов проектирования

GraphQL разделяет свои типы API-запросов на запросы и мутации. Запрос не изменяет состояние данных и просто возвращает результат. Мутация, с другой стороны, изменяет данные на стороне сервера. Таким образом, для CRUD-операций мы будем использовать запрос для чтения и мутацию для создания, обновления или удаления.

Подход CRUD в своей основе повторяет простейшие операции с базой данных:

create, read, update, delete. В GraphQL мы не ограничены только этим подходом, хотя легко можем его реализовать:

/// <summary>
/// Запрос чтения
/// </summary>
/// <param name="ctx">Контекст базы данных Entity</param>        	
/// <returns>Авторы</returns>    	
[UseDbContext(typeof(DemoContext))]
[UseProjection]
[UseFiltering()]
[UseSorting()]
public IQueryable<Author> Authors([ScopedService] DemoContext ctx)
{
	return ctx.Authors;
}


/// <summary>
/// Создание автора
/// </summary>
/// <param name="author">Создаваемый автор</param>
/// <param name="ctx">Контекст базы данных Entity</param>
/// <returns>Автор</returns>    	
public Author Create(Author author, [Service] DemoContext ctx)
{
  ctx.Add(author);
  ctx.SaveChanges();
  return author;
}
#endregion

/// <summary>
/// Обновление автора
/// </summary>
/// <param name="author">Обновляемый автор</param>
/// <param name="ctx">Контекст базы данных Entity</param>
/// <returns>Автор</returns>    	
public Author Update(Author author, [Service] DemoContext ctx)
{
  ctx.Update(author);
  ctx.SaveChanges();
  return author;
}

/// <summary>
/// Удаление автора
/// </summary>
/// <param name="id">Уникальный идентификатор автора</param>    	
/// <param name="ctx">Контекст базы данных Entity</param>
/// <returns>Автор</returns>    	
public Author Delete(int id, [Service] DemoContext ctx)
{
  var author = ctx.Authors.FirstOrDefault(a => a.Id == id);
  
  if (author == null)
    throw new ArgumentException("Автор не найден");
  
  ctx.Remove(author);
  ctx.SaveChanges();
  return author;
}

3.Авторизация и аутентификация

Рассматривайте GraphQL как специфичный язык. Это всего лишь один слой, который мы можем поместить между сервисом данных и нашими клиентами. Авторизация — это совершенно отдельный слой, и сам язык не поможет в применении или использовании верификации или аутентификации. Однако вы можете использовать GraphQL для связи токенов входа между клиентами и действующим планом. Это совершенно идентично подходу, которому мы следуем в REST.

Получение token-авторизации может выглядеть так:

На примере .NET Core backend (C#) достаточно в раздел ConfigureServices добавить:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, config =>
        {
            config.TokenValidationParameters = new TokenValidationParameters
            {
                ClockSkew = TimeSpan.FromSeconds(5),
                ValidateAudience = false
            };                        
            config.RequireHttpsMetadata = false;
            // Сервер OAUTH 2.0 
            config.Authority = "http://localhost:8080/auth/realms/SAT/";
            // Проверка для проекта
            config.Audience = "DEMO";
        });

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

 /// <summary>
 /// Тестовая функция с авторизацией
 /// </summary>
 /// <param name="claimsPrincipal"></param>
 /// <returns></returns>
 [Authorize(Roles = new[] { "admin" })]    	
 public bool AuthorizeQuery([Service] IHttpContextAccessor context, ClaimsPrincipal claimsPrincipal)
 {
 var user = context.HttpContext.User;
 var username = user.FindFirstValue("preferred_username");
 return true; 
 }

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

Для авторизации может быть использована любая система HTTP-авторизации. Например, с помощью JWT Token мы передаём токен авторизации в заголовке:

В этом примере токен авторизации был получен из сервиса OAuth 2.0. Backend при получении запроса обращается в OAuth 2.0 для проверки авторизации.

4.Кэширование

Поскольку REST использует HTTP, который сам использует кэширование, вы можете использовать его для предотвращения потери ресурсов. GraphQL, с другой стороны, не имеет системы кэширования, оставляя пользователям бремя самостоятельной работы с кэшированием.Для примера разработчики могут использовать кэширование HTTP, чтобы легко избежать повторной выборки ресурсов в API на основе эндпоинтов. В таком случае URL-адрес является глобальным уникальным идентификатором (UUID).

Кэширование данных осуществляется несколькими способами:

  • Кэширование на уровне клиента. Этот подход активно используется в библиотеке Apollo.

  • Кэширование запросов — так называемые сохранённые запросы. Это когда мы передаём в запросе extensions: { "persistedQuery": {} }, в результате чего в ответе возвращается "expectedHashValue", который мы можем в дальнейшем

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

5.Контроль версий

Когда есть ограниченный контроль данных, возвращаемых из API, любой сдвиг можно рассматривать как критическое изменение, а такие изменения требуют изменение и указание версий. Это, пожалуй, основная причина, по которой большинство API используют управление версиями. Если для включения обновлённых функций в API требуется последняя версия, то возникает необходимость в корректировке между частой публикацией, интерпретацией и сохранением API.

Так же, как и на примере с REST, мы можем осуществлять обращением к соответствующей версии. Например, .NET Core C#:

public class Query
{ 
     	/// <summary>
    	/// Конструктор
    	/// </summary>
    	public Query() { }
 
    	/// <summary>
    	/// Возвращает первую версию api
    	/// </summary>
    	public Api1 V1() =>
        	new Api1();
}

А дальше выполняем запрос GraphQL для первой версии:

query{
  v1{
    authors{
      id
      name
      books
      {
        id
        title
      }
    }
  }
}

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

6.WebSockets и подписки

GraphQL — это спецификация. Обычно мы наблюдаем GraphQL через HTTP для запросов и мутаций, однако при подписке на GraphQL нам необходимо получать непрерывные обновления от API. Именно здесь на помощь приходят WebSockets.

WebSockets часто используются в качестве транспортного протокола для GraphQL Subscriptions. Итак, подписки GraphQL не привязаны к какому-либо протоколу. На самом деле запросы и мутации GraphQL также не ограничены HTTP. Следовательно, библиотеки GraphQL Subscriptions на базе WebSockets реализуют небольшой протокол, по которому они отправляют операции и результаты подписки GraphQL.Две реализации WebSockets для GraphQL:

  1. subscriptions-transport-ws, который был создан командой Apollo (и поэтому пользуется большой поддержкой в Apollo Server), но больше не поддерживается активно;

  2. graphql-ws, который является проектом-преёмником (с небольшими несовместимостями). В его readme объясняется, как добавить его в Apollo Server.

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

GraphQL поддерживает подписную модель. Это когда по инициативе сервера данные отправляются на frontend. Для этого мы создаём класс:

/// <summary>
/// Подписки
/// </summary>
public class Subscription
{
  /// <summary>
  /// Добавлен новый автор
  /// </summary>
  /// <param name="author"></param>
  /// <returns>Автор</returns>
  [Subscribe]
  public Author OnAuthorChanged([EventMessage] Author author)
    => author;
}

Регистрируем его в разделе ConfigureServices:

services
.AddGraphQLServer()
.AddSubscriptionType<Subscription>();

После чего уже по инициативе сервера вызываем:

/// <summary>
/// Создать или обновить автора, если Id равен 0, то это однозначно новый автор
/// </summary>
/// <param name="ctx">Контекст базы данных Entity</param>
/// <param name="author">Создать или обновить автора</param>
/// <param name="sender"></param>
/// <returns>Автор</returns>    	
public async Task<Author> CreateOrUpdate(Author author, [Service] DemoContext ctx, 
                                         [Service] ITopicEventSender sender)
{
  if (author.Id == 0 || !ctx.Authors.Any(a => a.Id == author.Id))
    ctx.Add(author);
  else
    ctx.Update(author);

  ctx.SaveChanges();

  // по инициативе сервера отправляем клиентам данные
  await sender.SendAsync(nameof(Subscription.OnAuthorChanged), author);
  return author;
}

Отличия от аналогичных систем в том, что клиент также получает только те поля, которые он настроил.

В библиотеке Hot Chocolate GraphQL реализовано много всего интересного: автоматическая пагинация данных и интеграция с Redis.

Заключение

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

REST был представлен в ответ на потребность в более практичном и адаптируемом подходе к публикации и использованию веб-служб. Эта концепция была разумно проста и полностью лишена статичности, что исключало любые ненужные осложнения. Кроме того, этот подход также легко сочетается с JSON и XML. Но опять же, унификация данных была самым большим препятствием. Кроме того, ещё одной проблемой были разногласия по поводу того, как следует управлять версиями. Чтобы справиться с этой ситуацией, Facebook выступил вперёд и предоставил разработчикам решение, которое предлагает лучшее из обоих миров — GraphQL.

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

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

Tags:
Hubs:
+9
Comments 22
Comments Comments 22

Articles

Information

Website
www.rshb.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия
Representative
Юлия Князева