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

easycat

Olga Lugacheva

Posted on November 20, 2024

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

💖 💪 🙅 🚩
easycat
Olga Lugacheva

Posted on November 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related