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

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

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

Комментарии (0)