Створення API за допомогою Ruby on Rails та GraphQL

11 хв. читання

Проектування 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 для того, щоб більш детально зануритись в дрібниці.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.2K
Приєднався: 9 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація