Это руководство по GraphQL. Из него вы узнаете базовую теорию, а также научитесь писать простые запросы с помощью GraphQL.
Прежде чем рассматривать GraphQL, давайте уделим внимание исторической базе. Что такое SQL — structured query language или язык структурированных запросов?
SQL — декларативный язык программирования, который применяется для создания, изменения и управления данными в базах данных. Этот язык поддерживает четыре базовых оператора запросов: SELECT, INSERT, UPDATE и DELETE. С помощью SQL мы можем запросить из базы данных (БД) именно то, что нам необходимо.
Например, когда необходимо «достать» из БД всех пользователей с именем Maria, это можно сделать с помощью запроса:
SELECT * FROM USERS WHERE FirstName = "Maria"
Решить эту задачу с помощью REST можно несколькими способами:
fname
Maria. users.filter(user => user.fname === "Maria");
При этом в каждом из вариантов есть свои минусы. Первый подход нельзя масштабировать, так как нет возможности создать endpoint для каждого пользователя. Если мы используем второй подход, повышается нагрузка на сеть, а также появляется необходимость в постобработке на стороне клиента.
А теперь представьте инструмент, который объединяет возможности SQL и REST на стороне клиента. Вы уже догадались, что он называется GraphQL. Этот инструмент берёт идеи, разработанные для манипуляции данными в БД, и использует их в вебе. Поэтому с помощью одного запроса GraphQL можно получить сразу все необходимые данные.
Ниже представлена информация, необходимая для начала работы с GraphQL.
С помощью запросов GraphQL получает необходимые данные с сервера. Тип запроса Query в GraphQL — аналог GET в REST. Запросы — строки, которые отправляются в теле HTTP POST-запроса.
Примечание — Обратите внимание, все типы запросов в GraphQL отправляются через POST.
Query описывает данные, которые необходимо получить с сервера. Например, с помощью кода ниже можно получить fname
и age
всех пользователей в базе данных.
query {
users {
fname
age
}
}
В ответ на этот запрос сервер присылает данные в формате JSON. Структура ответа соответствует структуре запроса.
data : {
users [
{
"fname": "Joe",
"age": 23
},
{
"fname": "Betty",
"age": 29
}
]
}
Успешные операции возвращают JSON с ключом "data" и с ключом "error", а неуспешные возвращают JSON с ключом и сообщением об ошибке. Благодаря этому удобно обрабатывать ошибки на стороне клиента.
Mutation — ещё один root types. С его помощью можно добавлять данные в БД. Mutation — аналог POST и PUT в REST. Ниже пример кода.
mutation createUser{
addUser(fname: "Richie", age: 22) {
id
}
}
Здесь создаётся мутация createUser, которая добавляет в БД пользователя с fname
Richie и age
22. В ответ на этот запрос сервер присылает JSON с id записи. Ответ выглядит так:
data : {
addUser : "a36e4h"
}
Subscription — третий тип операций в GraphQL. С его помощью клиент слушает изменения в БД в режиме реального времени. Под капотом подписки используют вебсокеты. Пример кода:
subscription listenLikes {
listenLikes {
fname
likes
}
}
С помощью этого запроса можно получать список пользователей с именами и количеством лайков каждый раз, когда оно меняется.
Например, когда пользователь с fname
Richie получает лайк, ответ будет таким:
data: {
listenLikes: {
"fname": "Richie",
"likes": 245
}
}
Подобный запрос можно использовать для обновления количества лайков в режиме реального времени в соответствующем интерфейсе, например, в форме с результатами голосования на сайте.
Выше представлены три основных типа запросов в GraphQL. Несмотря на поверхностное знакомство, вы получили достаточно знаний, чтобы начать работать со схемой GraphQL.
Рассмотрим ответ на этот вопрос на конкретном примере.
Цель: настроить сервер GraphQL, который отвечает на три типа запросов к БД NoSQL, в которой есть список пользователей с такой структурой:
[{
"id": 1,
"fname": "Richie",
"age": 27,
"likes": 8
},
{
"id": 2,
"fname": "Betty",
"age": 20,
"likes": 205
},
{
"id" : 3,
"fname": "Joe",
"age": 28,
"likes": 10
}]
Также в базе данных есть списки постов, которые опубликовали пользователи.
[{
id: 1,
userId: 2,
body: "Hello how are you?"
},
{
id: 1,
userId: 3,
body: "What's up?"
},
{
id: 1,
userId: 1,
body: "Let's learn GraphQL"
}]
Для дальнейшей работы понадобится сервер Apollo. Это сервер GraphQL с открытым исходным кодом.
Настройте проект и установите зависимости:
> mkdir graphql-server-example
> cd graphql-server-example
> npm init -y
Вы создали пустой проект. Теперь установите GraphQL, Apollo и nodemon для отслеживания изменений в файлах.
> npm install apollo-server graphql nodemon
Чтобы nodemon работал, добавьте в package.json
запись:
"scripts": "start": "nodemon -e js, json, graphql"
Данные в этом примере поместим в переменную JSON. В реальных проектах данные обычно хранятся в БД. В тестовом проекте данные хранятся в index.js. Вот они:
const users = [
{
id: 1,
fname: 'Richie',
age: 27,
likes: 0,
},
{
id: 2,
fname: 'Betty',
age: 20,
likes: 205,
},
{
id: 3,
fname: 'Joe',
age: 28,
likes: 10,
},
];
const posts = [
{
id: 1,
userId: 2,
body: "Hello how are you?"
},
{
id: 1,
userId: 3,
body: "What's up?"
},
{
id: 1,
userId: 1,
body: "Let's learn GraphQL"
},
]
Теперь можно перейти к работе с сервером GraphQL.
Работа с сервером GraphQL всегда начинается с разработки схемы (Schema). Она состоит из двух взаимосвязанных объектов: TypeDefs и Resolvers.
Выше были описаны основные типы GraphQL. Чтобы сервер мог с ними работать, эти типы необходимо определить. Объект typeDef определяет список типов, которые доступны в проекте. Код выглядит так:
const typeDefs = gql`
type User {
id: Int
fname: String
age: Int
likes: Int
posts: [Post]
}
type Post {
id: Int
user: User
body: String
}
type Query {
users(id: Int!): User!
posts(id: Int!): Post!
}
type Mutation {
incrementLike(fname: String!) : [User!]
}
type Subscription {
listenLikes : [User]
}
`;
В примере выше определяется тип User, в котором указываются fname
, age
, likes
и другие данные. Для каждого поля определяется тип данных: String
или Int
. GraphQL поддерживает четыре типа данных: String
, Int
, Float
, Boolean
. Если в поле указан восклицательный знак, оно становится обязательным.
Также в примере выше определяются типы Query
,Mutation
и Subscription
.
Первый тип, который содержит внутри себя тип Query
, называется users
. Он принимает id и возвращает объект с данными соответствующего пользователя. Это обязательное поле. Ещё один тип Query
называется posts
. Он устроен так же, как users
.
type Query {
users(id: Int!): User!
posts(id: Int!): Post!
}
Тип Mutation
называется incrementLike
. Он принимает параметр fname
и возвращает список пользователей.
type Mutation {
incrementLike(fname: String!) : [User!]
}
Тип Subscription
называется listenLikes
. Он возвращает список пользователей.
type Subscription {
listenLikes : [User]
}
После определения типов необходимо добавить их логику. Это нужно, чтобы сервер знал, как отвечать на запросы клиента. Эта задача решается с помощью Resolvers.
Также полезно Когда Gatsby заменит WordPress: интервью с GraphQL-гуру Михаилом Новиковым.
Resolver или распознаватель — функция, которая возвращает данные для определённого поля. Resolver’ы возвращают данные того типа, который определён в схеме. Распознаватели могут быть асинхронными. С их помощью можно получать данные из REST API, базы данных или другого источника.
Определим Resolver’ы:
const resolvers = {
Query: {
users(root, args) { return users.filter(user => user.id === args.id)[0] },
posts(root, args) { return posts.filter(post => post.id === args.id)[0] }
},
User: {
posts: (user) => {
return posts.filter(post => post.userId === user.id)
}
},
Post: {
user: (post) => {
return users.filter(user => user.id === post.userId)[0]
}
},
Mutation: {
incrementLike(parent, args) {
users.map((user) => {
if(user.fname === args.fname) user.likes++
return user
})
pubsub.publish('LIKES', {listenLikes: users});
return users
}
},
Subscription: {
listenLikes: {
subscribe: () => pubsub.asyncIterator(['LIKES'])
}
}
};
В примере выше есть шесть функций:
users
возвращает объект пользователя, соответствующий переданному id;posts
возвращает объект поста, соответствующий переданному id;posts
User
распознаватель принимает данные пользователя и возвращает список его постов;user
Posts
функция принимает данные поста и возвращает пользователя, который опубликовал пост;incrementLike
изменяет объект users
: увеличивает количество likes
для пользователя с соответствующим fname
. После этого users
публикуются в pubsub
с названием LIKES
;listenLikes
слушает LIKES
и отвечает при обновлении pubsub
. Два слова о pubsub
. Этот инструмент представляет собой систему передачи информации в режиме реального времени с использованием вебсокетов. pubsub
удобно использовать, так как всё, что касается вебсокетов, вынесено в отдельные абстракции.
Итак, работа с типами и распознавателями завершена. Теперь можно запустить сервер.
Запускаем сервер
Откройте http://localhost:4000/ в браузере. Благодаря Apollo вы можете полноценно протестировать сервер GraphQL.
Потратили 15 минут на настройку сервера и, вуаля, написали первый запрос. Полная версия кода опубликована здесь.
Что делать, если нам нужно найти пользователя, который опубликовал пост с id x? Рассмотрим, как эта задача решается с помощью REST. Вот данные:
Users:
{
"id": 1,
"fname": "Richie",
"age": 27,
"likes": 8
},
{
"id": 2,
"fname": "Betty",
"age": 20,
"likes": 205
},
{
"id" : 3,
"fname": "Joe",
"age": 28,
"likes": 10
}
Posts:
{
id: 1,
userId: 2,
body: "Hello how are you?"
},
{
id: 1,
userId: 3,
body: "What's up?"
},
{
id: 1,
userId: 1,
body: "Let's learn GraphQL"
}
Сначала необходимо определить endpoint’ы GET:
> https://localhost:4000/users/:id
> https://localhost:4000/posts/:id
Когда необходимо получить данные о пользователе, который опубликовал пост с id 1, запрос выглядит так:
GET https://localhost:4000/posts/1
Response:
{
id: 1,
userId: 2,
body: "Hello how are you?"
}
GET https://localhost:4000/users/2
Response:
{
"id": 2,
"fname": "Betty",
"age": 20,
"likes": 205
}
Чтобы получить нужные данные, приходится дважды обращаться к серверу.
Что делать, если нужно получить только имя пользователя? Конечно, можно использовать данные поля fname
. Но мы уже получили дополнительные данные: id
, likes
, age
. Операция становится слишком дорогой.
Можно попробовать использовать endpoint, который получает только данные из поля fname
:
https://localhost:4000/userByfname/:id
Это решает проблему. Но сложность в том, что вам придётся создавать endpoint для каждого типа информации. Это неудобно.
Давайте посмотрим, как можно решить задачу с помощью GraphQL. Всё, что вам требуется — простой запрос:
{
posts(id: 1) {
body
user {
fname
}
}
}
Если нужно получить age
пользователя, который опубликовал пост с id 1, запрос будет таким:
{
posts(id: 1) {
body
user {
age
}
}
}
Поле user
в запросе определено в схеме. Поэтому вы можете получать нужную информацию без использования endpoint’ов и повторных обращений к серверу.
Изучайте Node.js на Хекслете Первые курсы в профессии «Node.js-программист» доступны бесплатно. Регистрируйтесь и начинайте учиться.
В руководстве рассмотрена базовая информация о GraphQL: краткая теория, типы, отличия от REST. В комментариях можно поделиться впечатлениями о GraphQL и опытом применения этого инструмента.
Адаптированный перевод статьи So, What the Heck is GraphQL by Karthik Kalyanaraman. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».