GraphQL Resolvers: кращі практики

З graphql.org

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

Можливо, ви бачили наше попереднє повідомлення "GraphQL: історія успіху для оплати через PayPal" про подорож PayPal від REST до GraphQL. У цій публікації детально описуються деякі найкращі практики створення розв'язувальних пристроїв, які є швидкими, перевіреними та стійкими з часом.

Що вирішувач?

Почнемо з тієї ж базової лінії. Що вирішувач?

Визначення роздільної здатності
Кожне поле кожного типу підтримується функцією, що називається роздільною здатністю.

Розв’язувач - це функція, яка розв'язує значення для типу або поля в схемі. Resolvers можуть повертати об'єкти або скаляри, такі як Strings, Numbers, Booleans тощо. Якщо Об'єкт повернуто, виконання продовжується до наступного дочірнього поля. Якщо скаляр повертається (як правило, у вузол листків), виконання завершується. Якщо null повернуто, виконання зупиняється і не продовжується.

Роздільники теж можуть бути асинхронними! Вони можуть вирішувати значення з іншого API REST, бази даних, кеш, константа тощо.

Пізніше ми ознайомимось із низкою прикладів, що ілюструють, як створити швидкі, перевірені та стійкі розв’язувачі.

Виконання запитів

Щоб краще зрозуміти розв’язувачі, потрібно знати, як виконуються запити.

Кожен запит GraphQL проходить три фази. Запити аналізуються, перевіряються та виконуються.

  1. Аналіз - Запит аналізується на абстрактне синтаксичне дерево (або AST). AST неймовірно потужні та за такими інструментами, як ESLint, babel тощо. Якщо ви хочете побачити, як виглядає GraphQL AST, ознайомтеся з astexplorer.net та змініть JavaScript на GraphQL. Ви побачите запит зліва та AST праворуч.
  2. Перевірка - AST перевіряється щодо схеми. Перевіряє правильність синтаксису запиту та чи існують поля.
  3. Виконати - Час виконання проходить через AST, починаючи з кореня дерева, викликає роздільники, збирає результати та випромінює JSON.

У цьому прикладі ми звернемося до цього запиту:

Запит для подальшого посилання

Коли цей запит аналізується, він перетворюється на AST або дерево.

Запит представлений у вигляді дерева

Тип кореневого запиту є вхідною точкою до дерева і містить два наші кореневі поля, користувача та альбом. Користувачі та роздільники альбомів виконуються паралельно (що характерно для всіх періодів виконання). Дерево виконується в першу чергу, тобто користувач повинен бути вирішений, перш ніж його ім'я та адреса дітей будуть виконані. Якщо роздільна здатність користувача асинхронна, гілка користувача затримується до її вирішення. Після того, як всі вузли аркушів, ім'я, електронна адреса, заголовок вирішені, виконання завершено.

Поля Root Query, як-от користувач та альбом, виконуються паралельно, але не в конкретному порядку. Зазвичай поля виконуються в тому порядку, який вони відображаються в запиті, але це не безпечно припускати. Оскільки поля виконуються паралельно, вони вважаються атомарними, ідентифікаційними та без побічних ефектів.

Придивившись уважніше до резолюцій

У наступних кількох розділах ми будемо використовувати JavaScript, але сервери GraphQL можна писати майже будь-якою мовою.

Розв’язувачі з чотирма аргументами - корінь, аргументи, контекст, інформація

У тій чи іншій формі кожен резолютор на кожній мові отримує ці чотири аргументи:

  • root - Результат від попереднього / батьківського типу
  • args - Аргументи, що надаються в поле
  • контекст - об'єкт, що змінюється, який надається всім розв'язникам
  • info - інформація, що стосується поля, що стосується запиту (використовується рідко)

Ці чотири аргументи є основними для розуміння того, як обтікаються дані між розв'язувачами.

Роздільники за замовчуванням

Перш ніж продовжити, варто зауважити, що на сервері GraphQL є вбудовані роздільники за замовчуванням, тому вам не потрібно вказувати функцію роздільної здатності для кожного поля. Розділювач за замовчуванням буде шукати у корінні властивість з тим самим іменем, що і поле. Можливо, реалізація виглядає приблизно так:

Реалізація за замовчуванням

Отримання даних у роздільній здатності

Де нам брати дані? Які компроміси з нашими варіантами?

У наступних кількох прикладах ми повернемося до цієї схеми:

Поле події містить необхідний аргумент id, повертає подію

Передача даних між роздільниками

контекст - це мінливий об'єкт, який надається всім розв'язникам. Він створений і знищений між кожним запитом. Це прекрасне місце для зберігання загальних даних Auth, загальних моделей / виборців для API та баз даних і т.д.

Коли ви вперше дізнаєтесь про контекст, початковою думкою може бути використання контексту як кеш загального призначення. Це не рекомендується, але ось як може виглядати реалізація.

Передача даних між роздільниками за допомогою контексту. Це не рекомендується!

Коли викликається заголовок, ми зберігаємо результат події в контексті. Коли викликається photoUrl, ми витягуємо подію з контексту та використовуємо її. Цей код не є надійним. Немає гарантії, що заголовок буде виконано перед photoUrl.

Ми можемо виправити обидва рішення, щоб перевірити, чи існує подія в контексті. Якщо так, використовуйте його. В іншому випадку ми дістаємо його і зберігаємо на потім, але все ж є велика площа поверхні для помилок.

Натомість нам слід уникати мутуючого контексту всередині резолюцій. Ми повинні не допускати змішування знань і проблем між собою, щоб наші розв’язувачі були легкими для розуміння, налагодження та перевірки.

Передача даних від батьків до дитини

Основний аргумент призначений для передачі даних від батьківських дозволів до дочірніх дозволів.

Наприклад, якщо ви будуєте тип події, де всі поля події
залежать від одних і тих же даних, можливо, ви захочете отримати їх один раз у полі події,
а не на кожному події.

Здається, гарна ідея, правда? Це швидкий спосіб розпочати роботу зі створенням рішень, але у вас можуть виникнути проблеми. Давайте розберемося, чому.

Для наведених нижче прикладів ми будемо працювати з типом події, який має два поля.

Тип події з двома полями: заголовком та фотографією

Більшість полів для події можна отримати з API подій, тож ми зможемо отримати їх у вирішальній програмі верхнього рівня та надати результати нашим розв'язникам заголовка та photoUrl.

Резолютор події верхнього рівня отримує дані, надає результати для розділення розділів поля заголовків та photoUrl

Ще краще, нам не потрібно вказувати два найнижчих рішення.
Ми можемо використовувати роздільники за замовчуванням, тому що Об'єкт повертається getEvent ()
має властивість title та photoUrl.

id та заголовок вирішуються за допомогою стандартних розв’язувачів

Що з цим не так?

Є два сценарії, в яких ви можете зіткнутися із перевантаженням ...

Сценарій №1: отримання багатошарових даних

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

Тип події з додатковим полем для відвідувачів

Коли ви отримуєте дані про відвідувачів, у вас є два варіанти: отримати ці дані на розділювачі події або роздільну здатність відвідувачів.

Ми перевіримо перший варіант: додавши його до вирішувача подій.

Резолютор подій викликає два API, отримання даних про події та інформацію про відвідувачів

Якщо клієнт запитує лише заголовок та photoUrl, але не відвідувачі. Тепер ви неефективні та подаєте непотрібний запит до свого API Attendees.

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

У нас є ще один варіант тестування із залученням відвідувачів всередині резолерів відвідувачів.

резолюр учасників отримує реквізити відвідувачів у API Attendees

Якщо наш клієнт запитує лише учасників, а не титул та photoUrl. Ми все ще неефективні, подаючи непотрібний запит до нашого API API.

Сценарій №2: Проблема N + 1

Оскільки дані збираються на рівні поля, ми ризикуємо перезавантажити. Перезавантаження та проблема N + 1 - популярна тема у світі GraphQL. У Shopify є чудова стаття, яка добре пояснює N + 1.

Як це впливає на нас тут?

Щоб краще проілюструвати його, ми додамо нове поле подій, яке повертає всі події.

Поле подій повертає всі події.Запит на всі події з назвою та учасниками

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

Ця проблема посилюється у великій організації, де запити можуть розкриватися і викликати зайвий тиск на вашу систему.

Щоб вирішити цю проблему, нам потрібно складати і де-дупувати запити!

У JavaScript деякі популярні варіанти - це завантажувач даних та джерела даних Apollo.

Якщо ви використовуєте іншу мову, ви можете вибрати щось, що ви можете. Тому погляньте навколо, перш ніж вирішити це самостійно.

По суті, ці бібліотеки розташовуються поверх вашого шару доступу до даних і кешуватимуть та видалятимуть вихідні запити, використовуючи деблокування або запам'ятовування. Якщо вам цікаво, як виглядає нагадування про асинхронізацію, перегляньте чудовий пост Даніеля Брейна!

Отримання даних на рівні поля

Раніше ми бачили, що легко перегоріти, перевантажившись «найважчими» рішеннями батьків-до-дітей.

Чи є краща альтернатива?

Давайте ще раз дражнимо варіант батько-дитина. Що робити, якщо ми скасуємо це так, щоб наші дочірні поля відповідали за отримання власних даних?

Поля відповідають за власне отримання даних.
Чому це краща альтернатива?

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

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

Для деяких дублювання getEvent може виглядати як кодовий запах. Але, маючи простий код, простий в обґрунтуванні і більш перевірений, варто трохи дублювати.

Але тут все ще є потенційна проблема Якщо клієнт запитує заголовок та photoUrl, ми викликаємо один додатковий запит у нашому API API за допомогою getEvent. Як ми вже бачили в проблемі N + 1, нам слід розробити запити на рамковому рівні, використовуючи бібліотеки, такі як завантажувач даних та джерела даних Apollo.

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

Кращі практики

  • Отримання та передача даних від батька до дитини слід застосовувати ощадливо.
  • Використовуйте бібліотеки, як завантажувач даних, для усунення запитів нижче.
  • Будьте в курсі будь-якого тиску, який ви чините на свої джерела даних.
  • Не мутуйте "контекст". Забезпечує послідовний, менший баггі-код.
  • Напишіть роздільну здатність, яку можна прочитати, відремонтувати, перевірити. Не надто розумний.
  • Зробіть роздільну здатність якомога тоншою. Витягніть логіку отримання даних для повторно використовуваних функцій асинхронізації.

Будьте в курсі!

Думки? Ми хотіли б почути кращі практики та знання вашої команди зі створення дозволів. Це тема, яку не часто обговорюють, але важлива для створення довготривалих API GraphQL.

У наступних публікаціях ми поділимося своїми думками щодо: схеми дизайну, керування помилками, видимості виробництва, оптимізації інтеграції на стороні клієнта та інструментальної роботи для команд.

Ми наймаємо! Якщо ви хочете зайнятися роботою над передовою інфраструктурою, GraphQL або React на PayPal, надішліть мене в Twitter на @mark_stuart!