JavaRush/Курсы/Модуль 5. Spring/Лекция 293: Практика: Реализация Batch Loading в Spring G...

Лекция 293: Практика: Реализация Batch Loading в Spring GraphQL

Открыта

Для реализации 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, чтобы ускорить обработку данных ещё больше. До встречи на следующей теме!

Комментарии (3)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Anonymous #3613348
Уровень 115
5 сентября, 14:30
Сейчас уже есть возможность не писать эти три этапа, а использовать аннотацию @BatchMapping. Для примера с постами юзера выглядело бы так:
// Spring САМ поймет, что это BatchLoader + DataLoader + Data Fetcher в одном флаконе!
@BatchMapping(field = "posts", typeName = "User")
public Map<User, List<Post>> getPostsForUsers(List<User> users) {
    // 1. Извлекаем все ID пользователей из пришедшего списка
    Set<Long> userIds = users.stream()
                              .map(User::getId)
                              .collect(Collectors.toSet());

    // 2. Делаем ОДИН пакетный запрос к базе (или к микросервису)
    List<Post> allPosts = postService.findAllByUserIdIn(userIds);

    // 3. Группируем посты по пользователям и возвращаем Map
    return allPosts.stream()
                           .collect(Collectors.groupingBy(Post::getUser));
}
p.s. Spring Boot 3+
Виктор
Уровень 115
5 мая, 08:14
Ну это танцы с бубном конечно, потому что в примерах старый подход с кикстарт для старой версии спринг бута, а в спринг иншалайзере минимум версию можно сделать 3.5 на данный момент, по этому там совсем по другому сейчас делается. Но балалайку завел!
Артём
Уровень 112
30 октября 2025, 13:34
Напишите, у кого получилось завести эту балалайку, и что вы для этого сделали!