Введение

Разные fetching strategies (стратегии извлечения), могут оказывать сильное влияние на скорость работы приложения, делать его быстрым или наоборот замедлять.

Hibernate определяет четыре стратегии выборки ассоциаций:

стратегия

описание

Join

Ассоциация является OUTER JOINED, соединением исходного SELECT

Select

Дополнительный SELECT используется для извлечения связанных сущностей

Subselect

Дополнительный SELECT используется для извлечения связанной коллекции. Этот режим предназначен для ассоциаций ко-многим

Batch

Для извлечения связанной коллекции используется некоторое количество дополнительных SELECT. При каждом дополнительном SELECT, будет извлекаться фиксированное количество связанных сущностей. Этот режим предназначен для ассоциаций ко-многим

Эти стратегии выборки могут быть применены в следующих сценариях:

  • ассоциация или связанная сущность, всегда инициализируется вместе со своим владельцем (например, EAGER FetchType).

  • выполняется навигация по неинициализированной связи (например, LAZY FetchType), поэтому связь должна быть получена с помощью дополнительного SELECT.

Hibernate маппинг, формирует глобальный план выборки. Во время запроса мы можем переопределить план выборки, но только для LAZY (ленивых) ассоциаций. Для этого мы можем использовать директиву fetch HQL/JPQL/Criteria. EAGER (жадные) связи не могут быть переопределены, поэтому привязка вашего приложения к глобальному плану выборки невозможна.

В Hibernate 3 было подтверждено, что LAZY стратегия извлечения, должна быть использована по умолчанию:

По умолчанию Hibernate 3 использует ленивую выборку для коллекций и ленивую выборку прокси. Такие значения по умолчанию имеют смысл для большинства ассоциаций в большинстве приложений.

Такое решение было принято после того, как было замечено множество проблем с производительностью, связанных с дефолтной ЖАДНОЙ выборкой данных в Hibernate 2. К сожалению, JPA использует другой подход, при котором связи "ко-многим" являются ЛЕНИВЫМИ, в то время как связи "к одному" являются ЖАДНЫМИ.

Тип ассоциации

Политика по умолчанию

@OneToMany

LAZY

@ManyToMany

LAZY

@ManyToOne

EAGER

@OneToOne

EAGER

Проблеммы жадного извлечения

Хотя может быть удобно просто помечать ассоциации как EAGER, делегируя ответственность за выборку Hibernate, тем не менее, рекомендуется создавать выборки на основе запросов.

EAGER ассоциация будет запрашиваться всегда, даже если она не нужна в каких-то запросах.

Далее, продемонстрируем, как работает EAGER выборка для разных Hibernate запросов. Используем модель данных ранее описанную в статье про стратегии извлечения:

У сущности Product есть следующие ассоциации:

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(
    name = "company_id",
    nullable = false
)
private Company company;
 
@OneToOne(
    mappedBy = "product",
    fetch = FetchType.LAZY,
    cascade = CascadeType.ALL,
    optional = false
)
private WarehouseProductInfo warehouseProductInfo;
 
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "importer_id")
private Importer importer;
 
@OneToMany(
    mappedBy = "product",
    fetch = FetchType.LAZY,
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
@OrderBy("index")
private Set<Image> images = new LinkedHashSet<>();

Объект Company помечен как EAGER, поэтому Hibernate всегда при запросе Product будет также запрашивать связанную сущность Company.

Загрузка Контекста Персистентности

Сначала загрузим объект с помощью Persistence Context API:

Product product = entityManager.find(Product.class, productId);

Это сгенерирует следующий SQL-запрос:

Query:{[
select
    product0_.id as id1_18_1_,
    product0_.code as code2_18_1_,
    product0_.company_id as company_6_18_1_,
    product0_.importer_id as importer7_18_1_,
    product0_.name as name3_18_1_,
    product0_.quantity as quantity4_18_1_,
    product0_.version as version5_18_1_,
    company1_.id as id1_6_0_,
    company1_.name as name2_6_0_
from Product product0_
inner join Company company1_ on product0_.company_id=company1_.id
where product0_.id=?][1]

Ассоциация Company была получена с помощью inner join , который объединит только связанные сущности. При создании M ассоциаций таблица сущностей-владельцев будет объединена M раз.

Каждое дополнительное объединение увеличивает общую сложность запроса и время его выполнения. Если мы не используем все эти ассоциации для каждого возможного бизнес-сценария, то мы просто теряем производительность, не получая ничего взамен.

Выборка с использованием JPQL и Criteria

код 1:

Product product = entityManager.createQuery(
    "select p " +
    "from Product p " +
    "where p.id = :productId", Product.class)
.setParameter("productId", productId)
.getSingleResult();

код 2:

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> productRoot = cq.from(Product.class);
cq.where(cb.equal(productRoot.get("id"), productId));
Product product = entityManager.createQuery(cq).getSingleResult();

Оба блока кода, указанные сверху сенерируют следующие SQL-запросы:

Query:{[
select
    product0_.id as id1_18_,
    product0_.code as code2_18_,
    product0_.company_id as company_6_18_,
    product0_.importer_id as importer7_18_,
    product0_.name as name3_18_,
    product0_.quantity as quantity4_18_,
    product0_.version as version5_18_
from Product product0_
where product0_.id=?][1]}
 
Query:{[
select
    company0_.id as id1_6_0_,
    company0_.name as name2_6_0_
from Company company0_
where company0_.id=?][1]}

Как в случае JPQL, так и в случае Criteria запросов, будет создана выборка по умолчанию, в результате чего, будет выполнен дополнительный select для каждой EAGER ассоциации. Чем больше число ассоциаций, тем больше дополнительных select-ов, и тем больше это повлияет на производительность нашего приложения.

Hibernate Criteria API

В то время как в JPA только в версии 2.0, была добавлена поддержка Criteria запросов, Hibernate уже давно предлагает конкретную реализацию динамических запросов. Однако Hibernate и JPA Criteria API ведут себя по-разному для аналогичных сценариев запроса.

В Hibernate Criteria, предыдущий пример выглядел бы следующим образом:

Product product = (Product) session
    .createCriteria(Product.class)
    .add(Restrictions.eq("id", productId))
    .uniqueResult();

Связанный с ним SQL-запрос был бы таким:

Query:{[
select
    this_.id as id1_3_1_,
    this_.code as code2_3_1_,
    this_.company_id as company_6_3_1_,
    this_.importer_id as importer7_3_1_,
    this_.name as name3_3_1_,
    this_.quantity as quantity4_3_1_,
    this_.version as version5_3_1_,
    hibernatea2_.id as id1_0_0_,
    hibernatea2_.name as name2_0_0_
from Product this_
inner join Company hibernatea2_ on this_.company_id=hibernatea2_.id
where this_.id=?][1]}

Этот запрос использует join fetch стратегию объединения.

Hibernate Criteria и EAGER колекции

Давайте посмотрим, что происходит, когда стратегия выборки коллекции Image настроена на EAGER:

@OneToMany(
    mappedBy = "product",
    fetch = FetchType.EAGER,
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
@OrderBy("index")
private Set<Image> images = new LinkedHashSet<>();

Будет сгенерирован следующий SQL-запрос:

Query:{[
select
    this_.id as id1_3_2_,
    this_.code as code2_3_2_,
    this_.company_id as company_6_3_2_,
    this_.importer_id as importer7_3_2_,
    this_.name as name3_3_2_,
    this_.quantity as quantity4_3_2_,
    this_.version as version5_3_2_,
    hibernatea2_.id as id1_0_0_,
    hibernatea2_.name as name2_0_0_,
    images3_.product_id as product_4_3_4_,
    images3_.id as id1_1_4_,
    images3_.id as id1_1_1_,
    images3_.index as index2_1_1_,
    images3_.name as name3_1_1_,
    images3_.product_id as product_4_1_1_
from Product this_
inner join Company hibernatea2_ on this_.company_id=hibernatea2_.id
left outer join Image images3_ on this_.id=images3_.product_id
where this_.id=?
order by images3_.index][1]}

Hibernate Criteria автоматически не группируют список родительских сущностей. Из-за объединения дочерних объектов в таблицу один-ко-многим для каждого дочернего объекта мы получим новую ссылку на родительский объект (все они указывают на один и тот же объект в нашем текущем Контексте Персистентности):

product.setName("TV");
product.setCompany(company);
 
Image frontImage = new Image();
frontImage.setName("front image");
frontImage.setIndex(0);
 
Image sideImage = new Image();
sideImage.setName("side image");
sideImage.setIndex(1);
 
product.addImage(frontImage);
product.addImage(sideImage);
 
List products = session
    .createCriteria(Product.class)
    .add(Restrictions.eq("id", productId))
    .list();
     
assertEquals(2, products.size());
assertSame(products.get(0), products.get(1));

Поскольку у нас есть два объекта image, мы получим две ссылки на объекты Product, обе из которых указывают на одну и ту же запись в кэше первого уровня.

Чтобы исправить это, нам нужно указать Hibernate Criteria, использовать различные корневые объекты:

List products = session
    .createCriteria(Product.class)
    .add(Restrictions.eq("id", productId))
    .setResultTransformer(
        CriteriaSpecification.DISTINCT_ROOT_ENTITY
    )
    .list();
 
assertEquals(1, products.size());

Заключение

Стратегия выборки EAGER - это code smell. Эта стратегия упрощает реализацию, но совсем не учитывает долгосрочные потери производительности. Стратегия выборки никогда не должна выносится на уровень сущности. Различные бизнес-процессы чаще всего имеют свои требования к загрузке и отображению данных, и поэтому стратегия выборки должна быть делегирована на уровень отдельных запросов.

Глобальный план выборки должен определять только LAZY связи, которые настраиваются для каждого отдельного запроса. В сочетании со стратегией обязательной проверки сгенерированных запросов, планы выборки на основе запросов могут повысить производительность приложения и снизить затраты на обслуживание.

Комментарии (3)


  1. kulity
    17.01.2025 15:08

    Слова "FetchType EAGER" в заголовке статьи явно лишние


  1. Iqorek
    17.01.2025 15:08

    Пришел к аналогичным выводам. Этот фреймворк, вероятно, создавался с целью упростить процессы, но в итоге всё вышло сложным, неочевидным да еще и медленным. Я этими фичами вообще не пользуюсь и все работает без проблем, а вот в параллельном проекте да и там постоянно, что то происходит


  1. Kassiy_Pontiy_Pilat
    17.01.2025 15:08

    На самом деле каждому инструменту свое применение. Если бездумно вытаскивать коллекции с данной аннотацией без ограничения по размеру вы скорее всего получите проблемы. Но если вы точно знаете что вам надо только один объект к которому вы будете вынуждены потом по всем его вложенным коллекциям пройтись чтобы их подтянуло из БД то почему бы и нет. Вопрос в поддерживаемости этого кода в дальнейшем но для этого есть например ассерты или еще какие то интерфейсные ограничения