Гайд по использованию Spring GraphQL. Часть 2

от автора

Привет, Хабр!

Меня зовут Дмитрий, я бэкенд-разработчик в 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:

Создайте соответствующие 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-сервису.

А пока, давайте обсудим в комментариях, какие из этих тем вам особенно интересны или уже использовались в ваших проектах?


ссылка на оригинал статьи https://habr.com/ru/articles/937128/