REST vs. GraphQL: відмінності, переваги та конвертація. Частина 2


Станіслав Сілін, Senior .NET Developer

27 Липня, 2023

Повернемося до справи. Сьогодні ми розглянемо деякі особливості переходу на GraphQL.

Давайте додамо ролі до нашої схеми GraphQL. Початкова REST кінцевий пункт виглядає так:   

 o.MapGet("/users/{id}/roles",
async (int id, RoleService service) => await service.GetRolesByUserId(new[] { id }));   

Ми можемо зробити це двома способами. Перший — більш REST-подібний спосіб. Нам просто потрібно створити ще одне поле GraphQL, яке буде повертати список ролей для конкретного користувача. Схема буде виглядати так:

type Query {
utc: DateTime!
users(page: Int!, size: Int!): [User!]!
user(id: Int!): User
+ userRoles(userId: Int!): [Role!]!
}

type User {
id: Int!
name: String!
email: String!
}

+ type Role {
+ id: Int!
+ name: String!
+ }

Але є також GraphQL-шлях. Ми можемо розширити сам тип User. Так, коли ми робимо запит на користувача, ми можемо отримати додаткові ролі користувача. У цьому випадку ми отримаємо наступну схему GraphQL:    

type Query {
utc: DateTime!
users(page: Int!, size: Int!): [User!]!
user(id: Int!): User
}

type User {
id: Int!
name: String!
email: String!
+ roles: [Role!]!
}

+ type Role {
+ id: Int!
+ name: String!
+ }

Давайте зробимо це другим способом. Щоб це спрацювало, нам потрібно створити ще один клас розширення. Однак ми будемо розширювати тип User, а не Query:

[ExtendObjectType(typeof(User))]
public class UserRolesQueryExtensions
{
public async Task<Role[]> GetRoles([Parent] User user, [Service] RoleService roleService)
{
var rolesByUserId = await roleService.GetRolesByUserId(ids);
return rolesByUserId[user.Id];
}
}

Таке налаштування дозволяє нам побудувати наступний запит на основі прикладу з “недостатньою та надмірною вибіркою”:    

query GetUserProfile($userId: Int!) {
user(id: $userId) {
id
name
email
roles {
id
name
}
}
}

Але як це працює? Сервер GraphQL отримує запит, зіставлений з відповідними розширеннями GraphQL. Тут їх буде два — GetUser і GetRoles. Коли виконується перше розширення, його результат передається другому. 

 

У підсумку ми маємо один запит від клієнта і GraphQL-сервер, який виконав два GraphQL-поля. В результаті клієнт отримає кінцевий результат швидше, оскільки все відбувається локально на сервері, який знаходиться набагато ближче до бази даних.  

 

Однак є одна проблема з таким підходом. Давайте подивимося на наступний запит: 

query GetUsers($page: Int!, $size: Int!) {

users(page: $page, size: $size) {
id
email
roles {
id
}
}
}

Тут у нас є поле GraphQL, яке повертає список елементів. Потім для кожного елемента у нас є розширення. Якщо ми залишимо все так, як є зараз, це може призвести до катастрофи. 

 

Давайте проаналізуємо, що відбувається:    

  1. Сервер GraphQL виконує поле GetUsers GraphQL і отримує 5 елементів.    
  2. Сервер GraphQL виконує поле GetUserRoles GraphQL для 1-го елемента.    
  3. Сервер GraphQL виконує поле GetUserRoles GraphQL для 2-го елемента.    
  4. Сервер GraphQL виконує поле GetUserRoles GraphQL для 3-го елемента.    
  5. Сервер GraphQL виконує поле GetUserRoles GraphQL для 4-го елемента.    
  6. Сервер GraphQL виконує поле GetUserRoles GraphQL для 5-го елемента.    
  7. Сервер GraphQL повертає кінцевий результат.    

Як бачите, коли GetUserRoles звертається до бази даних для кожного елемента, це може бути проблемою. Це може дуже погіршити продуктивність. Для вирішення цієї проблеми в сервері GraphQL є функція, яка називається завантажувачі даних. Це дозволяє вам пакетно виконувати поля GraphQL і отримувати дані за один раз. Ось як виглядатиме покращений GetUserRoles: 

[ExtendObjectType(typeof(User))]
public class UserRolesQueryExtensions
{
public async Task<Role[]> GetRoles(
IResolverContext context,
[Parent] User user, [Service] RoleService roleService)
{
return await context.BatchDataLoader<int, Role[]>(async (ids, ct)
=> await roleService.GetRolesByUserId(ids))
.LoadAsync(user.Id);
}
}

При першому запиту він створює екземпляр пакетного завантажувача даних і додає поточний ідентифікатор до списку. Потім, при кожному наступному запиту, ми просто запам’ятовуємо ідентифікатори користувачів. Коли у нас є повний список ідентифікаторів користувачів, він викликає лямбду з цими ідентифікаторами, і ми можемо просто отримати ролі користувачів для всіх користувачів одразу. В цьому випадку лог буде виглядати так:    

  1. Сервер GraphQL виконує поле GetUsers GraphQL і отримує 5 елементів.    
  2. Сервер GraphQL виконує поле GetUserRoles GraphQL, яке створює пакетний завантажувач даних і передає ідентифікатор 1-го елемента.    
  3. Сервер GraphQL виконує поле GetUserRoles GraphQL, яке передає ідентифікатор 2-го елемента.    
  4. Сервер GraphQL виконує поле GetUserRoles GraphQL, яке передає ідентифікатор 3-го елемента.    
  5. Сервер GraphQL виконує поле GetUserRoles GraphQL, яке передає ідентифікатор 4-го елемента.    
  6. Сервер GraphQL виконує поле GetUserRoles GraphQL, яке передає ідентифікатор 5-го елемента.    
  7. Сервер GraphQL виконує завантажувач даних.    
  8. Сервер GraphQL повертає кінцевий результат.    

Повну документацію можна знайти тут.    

СТВОРЕННЯ МУТАЦІЇ

Ще одна маленька деталь про GraphQL. Можливо, ви забули, але є ще один кінцевий пункт:  

 o.MapPost("/users", async (UserCreationRequest request, UserService service) 
=> await service.CreateUser(request.Name, request.Name));

Семантично він має інше значення в порівнянні з попередніми кінцевими пунктами. Це операція мутації, яка створює нову сутність. GraphQL проводить межу між операціями читання та редагування. Всі операції читання повинні бути визначені в типі Query, а всі операції редагування повинні бути визначені в типі Mutation. Отже, визначення цього кінцевого пункту як мутації буде гарною ідеєю. 

   

Ось як ми можемо це зробити:    

services
.AddGraphQLServer()
.AddQueryType<Query>()
+ .AddMutationType<Mutation>()
+ .AddTypeExtension<UserMutationExtensions>();

// ...

public class Mutation
{

}

// ...

[ExtendObjectType(typeof(Mutation))]
public class UserMutationExtensions
{
public Task<User> CreateUser(string name, string email, [Service] UserService userService)
=> userService.CreateUser(name, email);
}

Це той самий підхід, що і у випадку з типом Query, але тепер у нас є тип Mutation як кореневий тип.  

ВИСНОВОК

Спочатку ми коротко розглянули причини, чому було розроблено GraphQL. Недостатня та надмірна вибірка є поширеними проблемами традиційних RESTful API, що потенційно призводить до неефективності та поганого користувацького досвіду.   

 

GraphQL пропонує спосіб уникнути цього, дозволяючи клієнтам точно вказувати, які дані їм потрібні, і зменшуючи непотрібні запити і передачу даних. Така гнучкість оптимізує продуктивність і використання пропускної здатності, що робить GraphQL привабливим вибором для сучасної веб-розробки. 

 

Потім ми розглянули, як налаштувати сервер GraphQL. Ми з’ясували, як створювати поля GraphQL і що потрібно враховувати при перетворенні існуючих кінцевих пунктів REST в поля GraphQL.    

 

Сподіваюся, ці статті були для вас корисними. Наступного разу ми розглянемо, як проводити інтеграційне тестування наших GraphQL API. 

Дивіться також

Дізнайтеся більше про наш досвід, перемоги, виклики та секрети роботи