При работе с базами данных существует проблема которую принято называть «SELECT N + 1» — это когда приложение вместо одного запроса к базе данных, который выбирает все необходимые данные из нескольких связанных таблиц, коллекций, — делает дополнительный подзапрос для каждой строки результата первого запроса, чтобы получить связанные данные. Например, сначала мы получаем список студентов университета, в котором его специальность обозначена идентификатором, а потом для каждого из студентов делаем дополнительный подзапрос в таблицу или коллекцию специальностей, чтобы по идентификатору специальности получить наименование специальности. Поскольку каждый из подзапросов может потребовать еще один подзапрос, и еще один подзапрос — колчество запросов к базе данных начинает расти в геометрической прогрессии.
При работе с graphql очень просто породить проблему «SELECT N + 1», если в resolver-функции сделать подзапрос к связанной таблице. Первое что приходит в голову — сделать запрос сразу с учетом всех связанных данных, но это, согласитесь, нерационально, если связанные данные не запрашиваются клиентом.
Один из вариантов решения проблемы «SELECT N + 1» для graphql будет рассмотрен в этом сообщении.
Для примера возьмем две коллекции: «Авторы» (Author) и «Книги» (Book). Связь, как и следует полагать, «многие-ко-многим». У одного Автора может быть несколько Книг, и одна Книга может быть написана несколькими Авторами. Для хранения информации будем использовать базу данных mongodb и библиотеку mongoose.js
Связь между коллекциями «многие-ко-многим» реализуем при помощи вспомогательной коллекции «BookAuthor» и «виртуальных» полей.
// Author.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const schema = new Schema({
name: String
});
schema.virtual('books', {
ref: 'BookAuthor',
localField: '_id',
foreignField: 'author'
});
module.exports = schema;
// Book.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const schema = new Schema({
title: String
});
schema.virtual('authors', {
ref: 'BookAuthor',
localField: '_id',
foreignField: 'book'
});
module.exports = schema;
// BookAuthor.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const schema = new Schema({
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
book: { type: mongoose.Schema.Types.ObjectId, ref: 'Book' }
});
module.exports = schema;
// mongoSchema.js
const mongoose = require('mongoose');
const Author = require('./Author');
const Book = require('./Book');
const BookAuthor = require('./BookAuthor');
mongoose.connect('mongodb://localhost:27017/books')
mongoose.set('debug', true);
exports.Author = mongoose.model('Author', Author);
exports.Book = mongoose.model('Book', Book);
exports.BookAuthor = mongoose.model('BookAuthor', BookAuthor);
// graphqlType.js
exports.Author = require('./Author');
exports.Book = require('./Book');
// Author.js
const graphql = require('graphql')
const graphqlType = require('./index')
module.exports = new graphql.GraphQLObjectType({
name: 'author',
description: 'Авторы',
fields: () => ({
_id: {type: graphql.GraphQLString},
name: {
type: graphql.GraphQLString,
},
books: {
type: new graphql.GraphQLList(graphqlType.Book),
resolve: obj => obj.books && obj.books.map(book => book.book)
}
})
});
// Book.js
const graphql = require('graphql')
const graphqlType = require('./index')
module.exports = new graphql.GraphQLObjectType({
name: 'book',
description: 'Книги',
fields: () => ({
_id: {type: graphql.GraphQLString},
title: {
type: graphql.GraphQLString,
},
authors: {
type: new graphql.GraphQLList(graphqlType.Author),
resolve: obj => obj.authors && obj.authors.map(author => author.author)
}
})
});
const graphql = require('graphql');
const getFieldNames = require('graphql-list-fields');
const graphqlType = require('../graphqlType');
const mongoSchema = require('../mongoSchema');
module.exports = {
type: new graphql.GraphQLList(graphqlType.Author),
args: {
_id: {
type: graphql.GraphQLString
}
},
resolve: (_, {_id}, context, info) => {
const fields = getFieldNames(info);
const where = _id ? {_id} : {};
const authors = mongoSchema.Author.find(where)
if (fields.indexOf('books.authors.name') > -1 ) {
authors.populate({
path: 'books',
populate: {
path: 'book',
populate: {path: 'authors', populate: {path: 'author'}}
}
})
} else if (fields.indexOf('books.title') > -1 ) {
authors.populate({path: 'books', populate: {path: 'book'}})
}
return authors.exec();
}
};
{
author {
_id
name
books {
_id
title
authors {
_id
name
}
}
}
}
authors.find({}, { fields: {} })
bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} })
books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} })
bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} })
authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} })
{
author {
_id
name
}
}
authors.find({}, { fields: {} })
// Autors.js
const graphql = require('graphql')
const DataLoader = require('dataloader')
const graphqlType = require('./index')
const mongoSchema = require('../mongoSchema');
const bookLoader = new DataLoader(async ids => {
const data = await mongoSchema.Book.find({ _id: { $in: ids }}).populate('authors').exec();
const books = data.reduce((obj, item) => (obj[item._id] = item) && obj, {})
const response = ids.map(id => books[id]);
return response;
});
module.exports = new graphql.GraphQLObjectType({
name: 'authors',
description: 'Авторы',
fields: () => ({
_id: {type: graphql.GraphQLString},
name: {
type: graphql.GraphQLString,
},
books: {
type: new graphql.GraphQLList(graphqlType.Books),
resolve: obj => obj.books && obj.books.map(book => bookLoader.load(book.book))
}
})
});
// authors.js
const graphql = require('graphql');
const getFieldNames = require('graphql-list-fields');
const graphqlType = require('../graphqlType');
const mongoSchema = require('../mongoSchema');
module.exports = {
type: new graphql.GraphQLList(graphqlType.Authors),
args: {
_id: {
type: graphql.GraphQLString
}
},
resolve: (_, {_id}, context, info) => {
const fields = getFieldNames(info);
const where = _id ? {_id} : {};
const authors = mongoSchema.Author.find(where).populate('books')
return authors.exec();
}
};
{
authors {
_id
name
books {
_id
title
authors {
_id
name
books {
_id
title
authors {
_id
name
}
}
}
}
}
}
К сожалению, не доступен сервер mySQL