Для реализации Batch Loading средствами Spring GraphQL мы будем использовать DataLoader. DataLoader помогает группировать загрузки данных (batch) и кэшировать их для последующего использования. Это особенно полезно, когда один запрос запрашивает много однотипных данных, которые можно извлечь за одну операцию, например, из базы данных.
Пример задачи: представьте, что у нас есть RESTful приложение для магазина книг. Каждый заказ связан с несколькими книгами, и нам нужно отображать заказы вместе с их книгами в GraphQL-запросе. Классическая реализация без Batch Loading может привести к проблеме N+1 запросов, где для каждой записи выполняется отдельный запрос к базе данных. Batch Loading поможет заменить это на единичный "оптовый" запрос.
Создание проекта с GraphQL и DataLoader
1. Настройка проекта
Убедитесь, что у вас уже есть настроенный Spring Boot проект с GraphQL. Если нет, добавьте следующие зависимости в pom.xml:
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>12.0.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>12.0.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>java-dataloader</artifactId>
<version>3.1.1</version>
</dependency>
Также добавьте необходимые зависимости для работы с базой данных (например, H2 или PostgreSQL) и JPA.
2. Создание сущностей
Создадим две сущности: Order (заказ) и Book (книга). Заказ будет содержать список идентификаторов книг, связанных с ним.
// Order.java
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customerName;
@ElementCollection
private List<Long> bookIds; // Идентификаторы книг, связанные с заказом
// Геттеры и сеттеры
}
// Book.java
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
// Геттеры и сеттеры
}
3. Репозитории
Создадим JPA-репозитории для взаимодействия с базой данных:
// OrderRepository.java
public interface OrderRepository extends JpaRepository<Order, Long> {
}
// BookRepository.java
public interface BookRepository extends JpaRepository<Book, Long> {
}
4. GraphQL Schema
Добавим схему GraphQL в файл schema.graphqls:
type Order {
id: ID!
customerName: String!
books: [Book!]! # Ассоциированные книги
}
type Book {
id: ID!
title: String!
author: String!
}
type Query {
orders: [Order!]!
}
5. Resolver для запросов
Настраиваем резолвер для загрузки заказов. Однако, для загрузки книг мы будем использовать DataLoader.
@Component
public class OrderResolver implements GraphQLQueryResolver {
private final OrderRepository orderRepository;
public OrderResolver(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getOrders() {
return orderRepository.findAll();
}
}
6. Настройка DataLoader
Создадим DataLoader для загрузки книг по их идентификаторам.
@Component
public class BookDataLoader {
private final BookRepository bookRepository;
public BookDataLoader(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Bean
public DataLoader<Long, Book> bookLoader() {
return DataLoader.newMappedDataLoader(bookIds -> {
List<Book> books = bookRepository.findAllById(bookIds);
return CompletableFuture.supplyAsync(() -> books.stream()
.collect(Collectors.toMap(Book::getId, book -> book)));
});
}
}
7. Интеграция DataLoader в GraphQL
Настроим обработчик, который подключит DataLoader к контексту GraphQL.
@Component
public class DataLoaderRegistryFactory {
private final DataLoader<Long, Book> bookLoader;
public DataLoaderRegistryFactory(DataLoader<Long, Book> bookLoader) {
this.bookLoader = bookLoader;
}
@Bean
public DataLoaderRegistry dataLoaderRegistry() {
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("bookLoader", bookLoader); // Регистрируем DataLoader
return registry;
}
}
8. Использование DataLoader в Fetcher'е
Пора связать заказы с их книгами, используя DataLoader в Data Fetcher:
@Component
public class OrderDataFetcher implements GraphQLResolver<Order> {
public CompletableFuture<List<Book>> books(Order order, DataLoader<Long, Book> bookLoader) {
// Загружаем книги для текущего заказа
return bookLoader.loadMany(order.getBookIds());
}
}
Тестирование Batch Loading
Создайте в GraphQL Playground или Postman запрос на получение заказов и связанных с ними книг:
query {
orders {
id
customerName
books {
id
title
author
}
}
}
Если всё настроено верно, запрос к базе данных произойдёт единожды для всех книг, а не для каждой отдельно.
Логирование SQL-запросов
Включите логирование SQL-запросов в application.properties:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Проанализируйте выполненные запросы: вы должны видеть один запрос для группы идентификаторов книг, а не множество отдельных.
Что дальше?
Мы реализовали Batch Loading с использованием DataLoader и интегрировали его в Spring GraphQL приложение. Теперь вы можете избежать проблемы N+1 запросов и повысить производительность ваших API. В следующих лекциях мы изучим асинхронные вызовы и научимся использовать DataLoader совместно с CompletableFuture, чтобы ускорить обработку данных ещё больше. До встречи на следующей теме!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ