Разработка API для мобильных и веб-приложений сегодня стала очень распространенной проблемой. Со времени появления смартфонов десять лет назад (а вместе с ними и взрыва мобильных приложений) API REST стали основным стандартом обмена данными между сервером приложений и клиентами.
Одна из самых больших проблем, возникающих при разработке API, — это структура и детализация данных, которые ваш сервер возвращает вашему клиенту. Допустим, вы создаете социальную сеть в стиле Twitter, где пользователи могут подписываться на других пользователей. При разработке вашего API вы можете добавить конечную точку ( GET /users/123
) для получения данных о конкретном пользователе. Должен ли сервер отправлять данные о подписчиках этого пользователя в ответе API? Или он должен держать ответ очень легким, потому что вам нужны только основные данные об этом одном пользователе? Что если вашему клиенту нужны полные данные этого пользователя, а только имя пользователя и фотографии профиля его подписчиков?
Прямо сейчас вы можете подумать о некоторых хитростях, используя параметры запроса при вызове конечной точки, что-то вроде GET /users/123?full=true&with_followers=true&light_followers=true
. Тем не менее, держу пари, вы легко поймете, как этот подход может стать огромным источником головной боли для вас и всех других инженеров, которым необходимо использовать ваш API.
Теперь представьте, что ваше приложение является чем-то таким сложным, как GitHub или Facebook, с чередованными данными между пользователями, сообщениями, отношениями и т. Д., И головная боль теперь превращается в кошмар.
Введите GraphQL , язык запросов, созданный Facebook несколько лет назад при переносе их основного приложения в нативное приложение. Facebook является хорошим примером очень сложной архитектуры данных. Вот почему они разработали лучший способ работы с данными:
Пусть клиент спросит, какие данные ему нужны от сервера.
Возвращаясь к нашему предыдущему примеру, представьте, что наше мобильное приложение хочет запросить некоторые данные об одном конкретном пользователе, но ему нужна только базовая информация о его 20 первых подписчиках. Запрос 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": "[email protected]", "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" }, // and another 18 followers at maximum ] } }
В этом уроке мы увидим, как мы можем реализовать простой API базы данных фильмов, используя язык GraphQL и Ruby on Rails.
Создание проекта
Мы создадим проект Rails, используя опцию --api
чтобы использовать облегченную версию среды полного стека, содержащую только то, что нам нужно для построения REST API.
$ gem install rails $ rails new graphql-tutorial --api $ cd graphql-tutorial/
Теперь добавьте gem- graphql
в ваш 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
Мы собираемся реализовать отношение «многие ко многим» между нашими двумя моделями, поэтому нам нужно сгенерировать миграцию для таблицы соединений:
$ rails g migration CreateActorsMovies
Затем обновите модели:
# 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, также называемое корневым объектом или объектом запроса . Этот объект имеет одно поле
movie
и принимаетid
одного аргумента. API отвечает за возврат с объектом фильма с указанным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!
типа ID!
( !
указывает на то, что этот аргумент обязателен).
Наконец, мы предоставляем функцию resolve
, где мы возвращаем значение идентификатора, указанное в запросе, и возвращаем связанную модель из базы данных. Обратите внимание, что в нашем случае единственное, что мы делаем, — это вызываем метод 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
позволяет нам обрабатывать его значение с помощью API Active Record, чтобы ограничить размер коллекции.
Определение 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
Наконец, мы можем создать схему, содержащую корневой объект запроса в качестве члена запроса:
# 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, вот несколько интересных вещей:
-
На данный момент мы можем запросить только конкретный фильм по его идентификатору, так как мы определили поле особого
movie
. Что если бы мы хотели получить список фильмов, выпущенных в 1993 году? Мы могли бы добавить еще одно полеmovies
в наш корневойQuery
, используя для фильтрации аргументы фильтра, такие какyear
и возвращать список записей:```ruby # 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
. -
В какой-то момент мы, вероятно, захотим ограничить доступ к некоторым объектам или полям для клиентов. Например, мы могли бы аутентифицировать пользователей по некоторому токену, отправленному в клиентском запросе. Затем мы проверим доступ к записям или полям в зависимости от разрешений этого пользователя. Gem GraphQL также предоставляет несколько способов справиться с этим .
Есть много вещей, которые нужно исследовать. Вы можете проверить документацию к гему и официальную спецификацию GraphQL, чтобы глубже погрузиться в его детали.
Я надеюсь, что вам понравилось знакомство с возможностями, предлагаемыми GraphQL, и вы попробуете его в своих будущих проектах!
Дальнейшее чтение на SitePoint
- Книга: Rails Deep Dive
- Screencast: тестирование представлений, маршрутов и помощников для проблем в вашем приложении Rails
- Screencast: тестирование моделей приложений Rails с RSpec
- Screencast: Управление образцами данных с помощью приборов и фабрик
- Screencast: Настройка автоматического тестирования с RSpec