Отправная точка
Строить мы будем на базовом сервере GraphQL. Код можно скачать здесь или с помощью:
git clone --branch setup https://github.com/bjdixon/graphql-server.git
При клонировании/загрузке нужно создать экземпляр MongoDB и заменить строку подключения в файле ./index.js
.
Убедитесь, что зависимости установлены:
npm install
Затем запустите сервер:
npm start
Что мы строим
Сейчас у нас есть две модели, с помощью которых можно создавать и перечислять авторов и книги. Они соединены естественной связью, которую нам требуется определить. Также мы добавим возможность просматривать отдельные их экземпляры и связанные с ними объекты.
Расширение схемы
В файле ./schema/index.js
мы определили два типа объектов: Author
и Book
, используя язык определения схемы (SDL).
type Author {
id: ID!
name: String!
}
type Book {
id: ID!
name: String!
pages: Int
}
У каждого типа объекта есть список свойств и связанный тип для каждого из них. В нашем примере у типа Book
есть три свойства:
id
— специальный тип ID;name
— строка;pages
— целое число.
Список всех поддерживаемых типов можно найти здесь.
В конце некоторых определений типов можно заметить !
. Этот элемент указывает на то, что свойство является обязательным. Его можно увидеть в типе Query
:
type Query {
message: String!
authors: [Author!]!
books: [Book!]!
}
Свойство message
требует строку, authors
— список (обозначается квадратными скобками), а значения в этом списке должны иметь тип Author
, хотя возврат пустого списка также допустим. На данный момент свойство authors
должно быть либо null
, либо списком. Нам также нужно убедиться, что если оно возвращает непустой список, то он содержит данные типа Author
. Избавимся от запроса статического сообщения (message):
type Query {
authors: [Author!]
books: [Book!]!
}
Удаляем строку с распознавателем message
:
message: () => 'hello world',
Помимо удаления message
, разница в запросах мала. Строка author
больше не заканчивается восклицательным знаком. В приведенном примере свойство authors
может возвращать значение null
или список, но если оно возвращает непустой список, то он может содержать только тип authors
. Свойство books
должно возвращать список (который может быть пустым), и он может содержать только books
.
Создадим пару отношений в схеме. К типу Book
добавим обязательное свойство author
(предположим, что только один человек может быть автором книги), и это значение должно иметь тип Author
. К типу Author
добавим свойство books
, которое может быть null
или списком, чьи значения должны иметь тип Book
. Отредактируйте ./schema/index.js
:
type Author {
id: ID!
name: String!
books: [Book!]
}
type Book {
id: ID!
name: String!
pages: Int
author: Author!
}
Проверка обновлений схемы
Мы также можем перечислить авторов и вернуть их имена, но при этом получим null
для их книг. Если попытаемся перечислить книги и вернуть их авторов, получим ошибку — автор книги должен быть типа Author
, а у экземпляров книг его еще нет.
Нужно обновить базовые модели и мутации, которые создают книги и авторов.
Расширение моделей
Сначала расширим модель Author
, добавив массив книг. Мы собираемся сохранить список объектов Mongo ObjectId
(вы сможете увидеть это свойстве type
для книг) и использовать свойство ref
, чтобы сообщить Mongo, с какой моделью мы хотим его связать. Отредактируйте файл ./models/Author.js
:
import mongoose from 'mongoose'
const Schema = mongoose.Schema
export const Author = mongoose.model('Author', {
name: String,
books: [{
type: Schema.Types.ObjectId,
ref: 'Book'
}]
})
Точно так же нужно обновить модель Book
, но вместо массива авторов добавим отношение к одному Author
. В файле ./models/Book.js
:
import mongoose from 'mongoose'
const Schema = mongoose.Schema
export const Book = mongoose.model('Book', {
name: String,
pages: Number,
author: {
type: Schema.Types.ObjectId,
ref: 'Author'
}
})
Теперь можно изменить три строки в схеме и создать первые отношения.
Во-первых, в типе Mutation
мы добавляем автора при создании новой книги. Это будет строка, поскольку так Mongo хранит и принимает ID автора. В файле ./schema/index.js
отредактируйте createBook
в типе Mutation
, добавив новый параметр Author
:
type Mutation {
createAuthor(name: String!): Author!
createBook(name: String!, pages: Int, author: String!): Book!
}
Затем в том же файле внесем небольшое изменение в распознаватель createBook
, добавив параметр author
в функцию и в качестве аргумента в конструктор Book
:
const resolvers = {
Query: {
authors: () => Author.find(),
books: () => Book.find()
},
Mutation: {
createAuthor: async (_, { name }) => {
const author = new Author({ name });
await author.save();
return author;
},
createBook: async (_, { name, pages, author }) => {
const book = new Book({ name, pages, author });
await book.save();
return book;
}
}
}
Проверка обновленных моделей и мутаций
Удалите старые экземпляры Book
и Author
в MongoDB — они не отражают обновленную схему. Создайте новый объект Author
. Выполните запрос к Author
, чтобы получить его ID — он понадобится при создании Book
.
Теперь создайте новый Book
, используя только что созданный ID автора в качестве аргумента author
.
Мы создали новую книгу без ошибок. В БД Mongo мы видим, что экземпляр книги имеет свойство Author
с ObjectId
нашего автора.
Но если мы попытаемся получить сведения об авторе для новой книги с помощью запроса books
, то столкнемся с проблемами.
Отношения нужно не просто создавать, но и видеть, поэтому переходим к следующему пункту.
Связующие отношения в распознавателях
Мы будем использовать рекурсию.
В MongoDB мы храним свойство author
для каждой книги в виде строки ObjectId
, а массив книг каждого автора — это список ObjectId
объектов Book
. Сделаем эти ObjectId
более полезными.
Создадим две вспомогательные функции, которые будут использоваться для расширения ObjectId
автора книги в объект Author
и массива Author
, включающего ObjectId
книг, в массив объектов Book
. Определим эти функции в файле ./schema/index.js
перед распознавателями, в которых они будут использоваться:
const books = async bookIds => {
try {
const books = await Book.find({_id: { $in: bookIds }})
return books.map(book => ({
...book._doc,
author: author.bind(this, book._doc.author)
}))
} catch {
throw err
}
}
const author = async authorId => {
try {
const author = await Author.findById(authorId)
return {
...author._doc,
books: books.bind(this, author._doc.books)
}
} catch (err) {
throw err
}
}
Функция books
принимает массив ObjectId
объекта Book
в качестве аргумента, а затем находит все документы Book
с этими ID. Затем она возвращает отображение Book
поверх них, и каждая Book
возвращает все свои свойства, кроме Author
. Перезапишем его, когда оно запрашивается вызовом функции Author
, которая была привязана к ObjectId
автора Book
.
Функция author
, принимающая ObjectId
объекта Author
, находит документ Author
, связанный с этим ObjectId
, и возвращает все его свойства, за исключением массива книг. Он перезаписывается, когда это свойство запрашивается вызовом функции Books
, которая возвращает все свойства для каждой книги, имеющей связанный ObjectId
в массиве книг объекта Author
.
Свяжем эти функции для последующего использования. Если бы мы вызвали их немедленно, это привело бы к бесконечному циклу с функцией Books
, вызывающей Author
, которая вызвала бы Books
, и так далее.
Мы можем переписать наши распознаватели Query
, используя удобные функции для заполнения связанного с Book
объекта Author
и связанных с Author
объектов Book
:
const resolvers = {
Query: {
authors: async () => {
try {
const authors = await Author.find()
return authors.map(author => ({
...author._doc,
books: books.bind(this, author._doc.books)
}))
} catch (err) {
throw err
}
},
books: async () => {
try {
const books = await Book.find()
return books.map(book => ({
...book._doc,
author: author.bind(this, book._doc.author)
}))
} catch (err) {
throw err
}
}
},
Эти запросы имеют множество сходств как друг с другом, так и со вспомогательными функциями. Однако отличаются от них тем, что мы перечисляем все книги и авторов и используем вспомогательные функции для заполнения связанных свойств по требованию.
Закончим изменения распознавателя, переписав Mutation
:
Const resolvers = {
Query: {
...
},
Mutation: {
createAuthor: async (_, { name }) => {
try {
const author = new Author({ name })
await author.save()
return author;
} catch (err) {
throw err
}
},
createBook: async (_, { name, pages, author: authorId }) => {
const book = new Book({ name, pages, author: authorId })
try {
const savedBook = await book.save()
const authorRecord = await Author.findById(authorId)
authorRecord.books.push(book)
await authorRecord.save()
return {
...savedBook._doc,
author: author.bind(this, authorId)
}
} catch (err) {
throw err
}
}
}
}
Мутация createAuthor
остается неизменной, за исключением добавления блока try/catch
.
В мутации createBook
нужно переименовать параметр author
, который мы предоставляем в качестве аргумента от author
к authorId
, поскольку мы собираемся использовать функцию author
и конфликт имен. После сохранения нового Book
обновляем связанного Author
, находя его документ, помещая новые ObjectId
, принадлежащие Books
, в массив книг объектов Author
, а затем сохраняя запись. Используем нашу вспомогательную функцию для заполнения свойства Author
по требованию при возврате нового объекта Book
.
Проверка финальной версии
Мы внесли много изменений с момента последнего тестирования, поэтому удаляем всех ранее созданных Author
и Book
в MongoDB, а затем создаем нового Author
, используя мутацию createAuthor
. Не забудьте сохранить ID автора. Это понадобится при создании экземпляров Book
.
Выполнив это, мы можем увидеть изменения, используя мутацию createBook
и запросив возврат свойств Author
:
Создадим еще один Book
, используя того же автора, но на этот раз при возврате автора для нового Book
вернем имена всех его книг:
Воспользуемся запросом Author
, чтобы перечислить всех Author
и названия их книг:
Для демонстрации рекурсии можно сделать:
Не рекомендую это делать — медленно и бессмысленно.
Чтобы доказать, что магия, расширяющая связанные свойства, выполняется в коде, а не в фоновом режиме, можете заглянуть в MongoDB. Там должен быть документ Author
с массивом объектов Book
, содержащим ObjectId
, и документы Book
со свойствами Author
, которые являются ObjectId
.
Полный рабочий код для этой статьи можно найти здесь.
Читайте также:
- Почему нельзя разрешать поля GraphQL как конечные точки REST
- Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL)
- Введение в GraphQL: сложные операции и переменные
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Brian Dixon: How to Add Relations to Your GraphQL Schema