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 — мощный инструмент, но он требует умелого обращения. Следите за его поведением, и ваш зоопарк приложений будет работать как часы! 🦛✨

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay