Проектування API-інтерфейсів для мобільних додатків та веб-додатків – масштабна проблема. Високий попит на смартфони, який почався декаду тому (як наслідок – різке збільшення кількості мобільних додатків) призвів до того що, REST API став основним стандартом обміну даних між сервером та клієнтом.
Основна проблема з якою стикаються при розробці API – структура і ступінь деталізації даних, що повертає ваш бекенд. Припустимо, ви розробляєте соціальну мережу схожу на Twitter, де користувачі можуть фоловити інших користувачів. На етапі проектування вашого API, ви можете додати ендпоінт (GET /users/123
) для отримання даних про конкретного користувача. Чи повинен сервер відправляти дані про фоловерів цього користувача у відповідь на запит? Чи відповідь повинна бути дуже простою, оскільки вам потрібні тільки основні дані про цього користувача? Що робити, якщо потрібні не всі дані користувача, а лише його ім'я і фотографії його фоловерів.
Ви подумаєте про реалізацію «трюків» з використанням параметрів запиту під час виклику кінцевої точки, щось на зразок GET /users/123?full=true&with_followers=true&light_followers=true
. Проте, я впевнений, ви розумієте, що такі «трюки» в майбутньому викличуть головний біль у вас, та розробників, які будуть користуватись цим API.
Тепер уявіть, що ваш додаток щось дуже складне, GitHub або Facebook. З даними, які пов'язані між користувачами, повідомленнями, відносинами і т.д. Головний біль перетворюється в кошмар.
GraphQL - мова запитів, створена Facebook кілька років тому під час міграції їх основного додатка в нативний додаток. Facebook – хороший приклад складної архітектури даних. Ось чому вони розробили ефективніший спосіб обробки даних:
Нехай клієнт сам запитує, які дані йому потрібні з сервера.
Чувак з фейсбуку.
Повертаючись до нашого попереднього прикладу, уявіть собі, що наш мобільний додаток хоче запросити деякі дані про одного конкретного користувача, але потрібна тільки основна інформація про першу двадцятку його фоловерів. Запит GraphQL, який відправляється на сервер буде виглядати наступним чином:
{
user(id: 123) {
id
username,
email,
bio,
profile_picture,
followers(size: 20) {
id
username,
profile_picture
}
}
}
API, який використовує протокол GraphQL, відповість наступним JSON:
{
"user": {
"id": 123,
"username": "foo",
"email": "foo@myapp.com",
"bio": "I'm just a sample user, nothing much to say."
"profile_picture": "https://mycdn.com/somepicture.jpg",
"followers": [
{
"id": 134,
"username": "bar",
"profile_picture": "https://mycdn.com/someotherpicture.jpg"
},
{
"id": 153,
"username": "baz",
"profile_picture": "https://mycdn.com/anotherpicture.jpg"
},
// та 18 інших підписників
]
}
}
В цьому уроці ми розглянемо, як можна реалізувати простий API для бази даних фільмів з використанням GraphQL та Ruby on Rails.
Створення проекту
Створимо проект Rails застосовуючи опцію --api
для використання лайтової версії повноцінного фреймворку, яка містить лише те, що нам потрібно для створення REST API.
$ gem install rails
$ rails new graphql-tutorial --api
$ cd graphql-tutorial/
Додайте graphql
gem до вашого Gemfile:
gem 'graphql'
Виконайте $ bundle install
, щоб інсталювати gem. Тепер запустимо додаток сервера разом з $ rails server
.
Потрібно згенерувати дві моделі: одну для представлення фільмів, а іншу – для кіноакторів.
$ rails g model Movie title:string summary:string year:integer
$ rails g model Actor name:string bio:string
Необхідно оновити ці моделі.
# app/models/movie.rb
class Movie < ActiveRecord::Base
has_and_belongs_to_many :actors
end
# app/models/actor.rb
class Actor < ActiveRecord::Base
has_and_belongs_to_many :movies
end
У нас є простий Rails додаток з двома моделями. Побудуємо API, який реалізує схему GraphQL.
Побудова схеми GraphQL
Перед зануренням в код, давайте поговоримо трохи про специфікацію GraphQL. Ми почнемо з аналізу наступного запиту, який ми збираємося реалізувати в нашому додатку:
{
movie(id: 12) {
title
year
actors(size: 6) {
name
}
}
}
Давайте розіб'ємо його на наступні частини:
- Всередині першого відкриття і до закриття останніх дужок знаходиться тіло нашого GraphQL запиту. Воно називається кореневим об'єктом (root object) або об'єктом запиту ( query object). Цей об'єкт має одне поле
movie
і приймає один аргументid
. API відповідає за повернення об'єктуmovie
із зазначенимid
. - Всередині поля
movie
ми запитуємо скалярні поляtitle
таyear
, а також колекціюactors
. Ми надаємо аргументsize
, вказавши, що ми хочемо бачити тільки перших 6 акторів, пов'язаних з цим фільмом. - Нарешті, ми запитуємо одне поле
name
для кожного актора з колекції, яке зберігається в поліactors
.
Тепер, коли ми коротко оглянули можливості GraphQL, ми можемо почати реалізацію, визначивши кореневий об'єкт запиту:
# app/types/query_type.rb
QueryType = GraphQL::ObjectType.define do
name "Query"
description "The query root for this schema"
field :movie do
type MovieType
argument :id, !types.ID
resolve -> (obj, args, ctx) {
Movie.find(args[:id])
}
end
field :actor do
type ActorType
argument :id, !types.ID
resolve -> (obj, args, ctx) {
Actor.find(args[:id])
}
end
end
Кореневий об'єкт може мати безпосередньо два типи дітей, якими є дві моделі, які ми визначили в нашому додатку: movie
та actor
. Для кожного поля вкажіть його тип, який потім буде визначений в його власному класі. Нам також потрібно вказати аргументи, які може прийняти поле – тільки id
, який має спеціальний тип ID!
(!
– індикатор, який вказує, що цей аргумент необхідний).
В кінці, ми реалізуємо функцію resolve
, в якій отримуємо назад значення ID
заданого в запиті і повертаємо відповідну модель з БД. Слід зазначити, що в нашому випадку, єдине, що ми робимо, це виклик методу Active Record Actor::find
, але ми можемо реалізувати все, що ми хочемо, поки ми повертаємо дані, які запитували.
Не забудьте додати каталог, що містить наші визначення типів в список автозавантажувальних шляхів:
# config/application.rb
config.autoload_paths < < Rails.root.join("app", "types")
Нам все ще потрібно визначити типи для обробки полів movie
та actor
. Ось код для визначення MovieType
:
# app/types/movie_type.rb
MovieType = GraphQL::ObjectType.define do
name "Movie"
description "A Movie"
field :id, types.ID
field :title, types.String
field :summary, types.String
field :year, types.Int
field :actors do
type types[ActorType]
argument :size, types.Int, default_value: 10
resolve -> (movie, args, ctx) {
movie.actors.limit(args[:size])
}
end
end
Це визначення схоже на попереднє, за винятком скалярних полів title
, summary
, та year
. Нам не потрібно надавати метод resolve
, оскільки бібліотека буде викликати поля моделі від їх імені. Поле actors
типу [ActorType]
, яке являє собою колекцію об'єктів типу ActorType
.
Ми також зазначаємо, що поле actors
може отримувати додатковий аргумент size
. Визначення методу resolve
дозволяє обробляти його значення за допомогою Active Record API, з метою обмеження розміру колекції.
Визначення ActorType
нижче, реалізується подібним чином:
# app/types/actor_type.rb
ActorType = GraphQL::ObjectType.define do
name "Actor"
description "An Actor"
field :id, types.ID
field :name, types.String
field :bio, types.String
field :movies do
type types[MovieType]
argument :size, types.Int, default_value: 10
resolve -> (actor, args, ctx) {
actor.movies.limit(args[:size])
}
end
end
Нарешті, ми можемо створити схему, яка містить root object запиту в якості елемента запиту:
# app/types/schema.rb
Schema = GraphQL::Schema.define do
query QueryType
end
Тепер ми можемо використовувати це в якості вхідної точки для наших запитів всередині контролерів API:
# app/controllers/movies_controller.rb
class MoviesController < ApplicationController
# GET /movies
def query
result = Schema.execute params[:query]
render json: result
end
end
Ми готові використовувати запити GraphQL всередині нашого API! Давайте додамо деякі дані ...
$ bundle exec rails c
> movie = Movie.create!(title: "Indiana Jones", year: 1981, summary: "Raiders of the Lost Ark")
> actor = Actor.create!(name: "Harrison Ford", bio: "Some long biography about this actor")
> movie.actors < < actor
…і спробуйте звернутись до кінцевої точки використовуючи будь-який клієнт HTTP:
curl -XGET http://localhost:3000/movies -d "query={
movie(id: 1) {
title,
year,
actors {
name
}
}
}"
Рухаємось далі
Для тих з вас, хто хотів би дізнатися більше про використання GraphQL разом з Rails, ось деякі цікаві речі, які необхідно спробувати:
- Наразі ми можемо запитати тільки конкретний фільм по його ID, оскільки ми визначили єдине поле
movie
. Що робити, якщо нам потрібен список фільмів, які були випущені в 1993 році? Ми могли б додати ще одне полеmovie
в корінь нашого запиту, яке буде застосовувати параметри фільтрації, такі якyear
і повертати список записів:
# app/types/query_type.rb
field :movies do
type types[MovieType]
argument :year, types.Int
resolve -> (obj, args, ctx) {
if args[:year].present?
Movie.where(year: args[:year])
else
Movie.all
end
}
end
- Ми могли б дозволити клієнту визначити порядок повернення
actors
абоmovies
. Це на той випадок, якщо в нас буде багато записів. Це можна зробити, використовуючи необов'язкові аргументи і обробляючи їх в середині функціїresolve
. - Може виникнути потреба в обмеженні доступу клієнта до деяких об'єктів чи полів. Наприклад, ми б могли мати користувачів, які пройшли аутентифікацію за допомогою деякого токена відправленого в клієнтському запиті. Ми будемо перевіряти доступ до записів або полів в залежності від прав цього користувача. GraphQL надає можливості рішення цієї проблеми.
Є багато речей, які залишаються відкритими для вивчення. Ви можете переглянути документацію по gem, а також офіційну специфікацію по GraphQL для того, щоб більш детально зануритись в дрібниці.
Ще немає коментарів