DEV Community

Cover image for Зоопарк Hibernate: N+1 запросов или как накормить жадного бегемота
Olga Lugacheva
Olga Lugacheva

Posted on

1

Зоопарк Hibernate: N+1 запросов или как накормить жадного бегемота

Добро пожаловать в продолжение нашего приключения в зоопарке Hibernate! Сегодня мы сосредоточимся на одной из самых распространённых и коварных проблем — N+1 запросов. Если вы когда-либо замечали внезапные замедления в работе вашего приложения, скорее всего, это была проделка Жадного Бегемота. Но не волнуйтесь — мы расскажем, как приручить его и избежать неприятностей.

Что такое проблема N+1 запросов?

Представьте, что вы пришли в зоопарк посмотреть на группу бегемотов (сущностей). Вы хотите узнать, что каждый из них ел на обед (связанные сущности). Вместо того чтобы получить список всех бегемотов с их обедами за один раз, вы сначала спрашиваете список бегемотов (один запрос), а затем делаете отдельный запрос за обедом для каждого бегемота.

nplus

Почему возникает проблема?

Ленивая загрузка (LAZY):
Hibernate откладывает получение связанных данных до момента, когда вы действительно к ним обратитесь. Например, вы получили объект Hippo, а связанные Meal загрузятся только тогда, когда вы вызовете hippo.getMeals(). Это и порождает множество запросов — по одному на каждый элемент коллекции.

Жадная загрузка (EAGER):
Hibernate сразу загружает все связанные данные. Однако если связь описана как @OneToMany и запрос не настроен оптимально, Hibernate может загрузить данные отдельными запросами, а не объединённо, что также вызывает проблему N+1.
Таким образом, даже если вы не планировали проблемы, Hibernate может устроить вам приключения.

Пример проблемы N+1 в Hibernate

Сущности

@Entity
public class Hippo {
    @Id
    private Long id;
    private String name;

    @OneToMany(mappedBy = "hippo", fetch = FetchType.LAZY)
    private List<Meal> meals;

    // getters and setters
}

@Entity
public class Meal {
    @Id
    private Long id;
    private String type;

    @ManyToOne(fetch = FetchType.LAZY)
    private Hippo hippo;

    // getters and setters
}
Enter fullscreen mode Exit fullscreen mode

Репозиторий

@Query("SELECT h FROM Hippo h")
List<Hippo> findAllHippos();

Enter fullscreen mode Exit fullscreen mode

Сценарий

List<Hippo> hippos = hippoRepository.findAllHippos();

for (Hippo hippo : hippos) {
    System.out.println(hippo.getMeals().size());
}

Enter fullscreen mode Exit fullscreen mode

Что происходит?

Hibernate выполняет один запрос для загрузки всех гиппопотамов:

SELECT * FROM hippos; -- Получаем список бегемотов

Enter fullscreen mode Exit fullscreen mode

Дополнительные запросы (+1):

SELECT * FROM meals WHERE hippo_id = 1;
SELECT * FROM meals WHERE hippo_id = 2;
...
SELECT * FROM meals WHERE hippo_id = N;
Enter fullscreen mode Exit fullscreen mode

В результате вы выполняете 1 запрос для основной сущности и N дополнительных запросов для связанных сущностей. Если у вас 100 гиппопотамов, это приведёт к 1 (на гиппопотамов) + 100 (на еду) = 101 запросу. Это и есть проблема N+1 запросов. На небольших наборах данных это может быть незаметно, но при сотнях или тысячах записей производительность падает катастрофически.

Как решить проблему N+1 запросов?

1. Используйте JOIN FETCH

Самый прямолинейный способ избежать N+1 запросов — явно указать Hibernate объединить данные в одном запросе. Для этого используйте JPQL с JOIN FETCH.

Пример:
Допустим, у нас есть сущности Hippo (бегемот) и Meal (обед), связанные через @OneToMany. Вместо ленивого выполнения запросов мы пишем так:

List<Hippo> hippos = entityManager.createQuery(
    "SELECT h FROM Hippo h JOIN FETCH h.meals", Hippo.class)
    .getResultList();

Enter fullscreen mode Exit fullscreen mode

Это создаст один запрос с объединением:

SELECT h.*, m.* 
FROM hippos h
LEFT JOIN meals m ON h.id = m.hippo_id;

Enter fullscreen mode Exit fullscreen mode

Теперь Hibernate загрузит всех бегемотов вместе с их обедами за один раз.

2. Используйте @BatchSize

batch

Если невозможно или неудобно использовать JOIN FETCH, аннотация @BatchSize может помочь. Она позволяет Hibernate загружать данные пакетами, а не по одному.

Пример:

@OneToMany(mappedBy = "hippo", fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Meal> meals;
Enter fullscreen mode Exit fullscreen mode

Теперь, если вы запросите 50 бегемотов, Hibernate выполнит не 50 отдельных запросов, а 5 запросов по 10 обедов каждый.

3. Используйте графы сущностей (Entity Graphs)

Графы сущностей позволяют гибко указывать, какие связи нужно загрузить, прямо на уровне запросов или аннотаций.

Пример

@Entity
@NamedEntityGraph(
    name = "hippo-with-meals",
    attributeNodes = @NamedAttributeNode("meals")
)
public class Hippo { /* ... */ }

Enter fullscreen mode Exit fullscreen mode

Запрос с графом

@EntityGraph(value = "hippo-with-meals")
@Query("SELECT h FROM Hippo h")
List<Hippo> findAllHipposWithMeals();
Enter fullscreen mode Exit fullscreen mode

4. Используйте проекции (Projections)

Проекции позволяют извлекать только нужные данные и избегать загрузки лишних сущностей.

Пример DTO

public class HippoMealDTO {
    private final Long hippoId;
    private final String hippoName;
    private final String mealType;

    public HippoMealDTO(Long hippoId, String hippoName, String mealType) {
        this.hippoId = hippoId;
        this.hippoName = hippoName;
        this.mealType = mealType;
    }
}

Enter fullscreen mode Exit fullscreen mode
@Query("SELECT new com.example.dto.HippoMealDTO(h.id, h.name, m.type) " +
       "FROM Hippo h JOIN h.meals m")
List<HippoMealDTO> findHipposWithMeals();

Enter fullscreen mode Exit fullscreen mode

Результат:
Вы получаете только нужные данные, а Hibernate не загружает полностью сущности Hippoи Meal.

Заключение

Проблема N+1 запросов — это как попытка кормить Жадного Бегемота по одному кусочку вместо того, чтобы дать ему сразу целую корзину. Знание стратегий, таких как JOIN FETCH, @BatchSize, кеширование и использование DTO, позволяет избежать этой проблемы и сделать ваш код более эффективным.

Hibernate — мощный инструмент, но он требует умелого обращения. Следите за его поведением, и ваш зоопарк приложений будет работать как часы! 🦛✨

Billboard image

Use Playwright to test. Use Playwright to monitor.

Join Vercel, CrowdStrike, and thousands of other teams that run end-to-end monitors on Checkly's programmable monitoring platform.

Get started now!

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay