Привет, Хабр!
Меня зовут Дмитрий, я бэкенд-разработчик в SENSE и последние 10 лет пишу серверную часть на Java. Эта статья – продолжение первой части гайда по Spring GraphQL, где мы с нуля подняли проект и подключили GraphQL к Spring Boot.
Теперь углубимся в разработку полноценного API: создадим более сложную схему с вложенными типами и связями между ними, реализуем запросы с фильтрацией, добавим мутации для изменения данных и затронем важные аспекты производительности.
Поехали!
Реализация усложненного @QueryMapping
Предположим, что у вас есть база данных с каталогом авторов и их книг:

Необходимо создать метод getAuthors с фильтром получения конкретных авторов, который вернет список авторов и их книг.
Создайте в схеме новый метод с фильтром, а также типы и подтипы (фильтром может выступать также любой пользовательский тип).
В схеме добавьте описание полей и метода для удобства в использовании:
type Query {
helloWorld(text: String!): String!
"Получить всех авторов"
getAuthors(filter: Int): [Author!]!
}
"Автор"
type Author {
"id автора"
id: ID!
"Имя автора"
name: String!
"Фамилия автора"
surname: String!
"День рождения автора"
birthday: String!
"Книги автора"
books: [Book!]!
}
"Книга"
type Book {
"id книги"
id: ID!
"Название книги"
title: String!
"Год издания книги"
year: Int!
"Описание книги"
description: String!
}
Правила валидации массивов схемы graphql:
SDL | Значение |
[Int!] | null или массив чисел |
[Int]! | массив чисел (допускается null в массиве), пустой массив |
[Int!]! | массив чисел (null не допускается) или пустой массив |
Полное описание notNull:
В спеке от октября 2021 года;
В драфте спеки от 2025 года.
Создайте соответствующие record для Author и Book:
public record Author(
int id,
String name,
String surname,
String birthday,
List<Book> books
) {
}
public record Book(
int id,
String title,
int year,
String description
) {
}
Реализуйте контроллер:
@Controller
@RequiredArgsConstructor
public class GraphQLController {
private final DataBaseService dataBaseService;
@QueryMapping
public List<Author> getAuthors(@Argument Integer filter) {
return dataBaseService.getAuthors(filter);
}
}
В настоящее время, независимо от того, запрашиваете ли вы объект Book или нет, вам придется получать его из базы данных, поскольку данные для запроса получаются из одного метода getAuthors:

Чтобы оптимизировать эту ситуацию, необходимо использовать @SchemaMapping.

Реализация @SchemaMapping с фильтрами
Вы можете удалить из record Author поле books, т.к. graphql его смапит автоматически. Добавьте фильтр к books в схеме graphql:
"Автор"
type Author {
"id автора"
id: Int!
"Имя автора"
name: String!
"Фамилия автора"
surname: String!
"День рождения автора"
birthday: String!
"Книги автора"
books(id: Int): [Book!]!
}
Добавьте метод для получения книг в контроллере:
@SchemaMapping(field = "books", typeName = "Author")
public List<Book> getBookForAuthor(@Argument Integer id, Author author) {
return dataBaseService.getBooks(id, author.id());
}
Как видите, в метод мы можем передать аргумент фильтра, а также объект, в контексте которого вызвано поле.
То есть, если метод getAuthors возвращает 3 автора, метод getBookForAuthor будет вызван 3 раза - по одному для каждого автора:


Возникает вопрос: как можно избежать трехкратного обращения к источнику данных и получить книги для всех авторов за один раз?
Для этого нужно использовать несколько способов:
@BatchMapping (имеет ограничения);
DataLoader.
@BatchMapping
@BatchMapping - это аннотация, которая используется для определения метода, который будет загружать данные в пакетном режиме. Этот метод будет вызван в целях загрузки данных для нескольких объектов одновременно, что может улучшить производительность запросов GraphQL.
Данная аннотация не может работать с аргументами.
Поэтому удалите фильтр у поля books из схемы:
"Автор"
type Author {
"id автора"
id: UUID!
"Имя автора"
name: String!
"Фамилия автора"
surname: String!
"День рождения автора"
birthday: String!
"Книги автора"
books: [Book!]!
}
Закомментируйте ранее созданный SchemaMapping в контроллере и создайте BatchMapping:
@BatchMapping(field = "books", typeName = "Author")
public Map<Author, List<Book>> getBooks(List<Author> authorsIds) {
return dataBaseService.dataload(authorsIds);//получаем данные за один поход в БД
}

DataLoader
Верните фильтр в поле books:
"Автор"
type Author {
"id автора"
id: UUID!
"Имя автора"
name: String!
"Фамилия автора"
surname: String!
"День рождения автора"
birthday: String!
"Книги автора"
books(id: Int): [Book!]!
}
Удалите BatchMapping из предыдущего примера и добавьте в конструктор контроллера DataLoader и измените SchemaMapping:
public GraphQLController(BatchLoaderRegistry registry, GraphQLClient graphQLClient, DataBaseService dataBaseService) {
RegistrationSpec<Author, List<Book>> spec = registry.forName("loaderBook");
spec.registerMappedBatchLoader((authorIds, env) -> {
Integer id = null;
if (!CollectionUtils.isEmpty(env.getKeyContextsList())) {
id = (Integer) env.getKeyContextsList().getFirst();
}
Map<Author, List<Book>> thingsInSites = dataBaseService.testDataload(id);
return Mono.just(thingsInSites);
});
this.graphQLClient = graphQLClient;
this.dataBaseService = dataBaseService;
}
@SchemaMapping(field = "books", typeName = "Author")
public CompletableFuture<List<Book>> getBookForAuthor(@Argument Integer id, Author author,
DataLoader<Author, List<Book>> loaderBook) {
return loaderBook.load(author, id);
}
Если бы у вас не было @Argument, то в load можно было бы передать только Author.
Реализуйте Equals и HashCode для Author.
DataFetchingEnvironment
DataFetchingEnvironment - это объект, который предоставляет информацию о контексте выполнения запроса GraphQL. Он содержит данные о запросе, схеме, типах данных и других важных деталях, которые необходимы для обработки запроса.
Мы можем получить его в методах нашего запроса.
К примеру,
@QueryMapping
public List<Author> getAuthors (@Argument Integer filter, DataFetchingEnvironment environment) ...
Через DataFetchingEnvironment, например, можно передавать какую-либо информацию между QueryMapping и SchemaMapping:
(DataFetchingEnvironment) environment.getGraphQlContext().put(
"key", value);
if (environment.getGraphQlContext().hasKey("key")) {
Object info = environment.getGraphQlContext().get("key");
}
Через контекст также можно передавать различные данные, которые не доступны по умолчанию в @SchemaMapping, к примеру: данные фильтра узла Author.
Вместо вывода: что будет дальше
В следующей части разберёмся с валидацией данных, обработкой ошибок, работой с заголовками, пользовательскими скалярами и директивами. Также добавим поддержку интерфейсов и union-типов, напишем тесты и клиент для обращения к GraphQL-сервису.
А пока, давайте обсудим в комментариях, какие из этих тем вам особенно интересны или уже использовались в ваших проектах?