Статьи

EAGER fetching — это кодовый запах

Вступление

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

Извлечение 101

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

Выбор стратегии Описание
Присоединиться Ассоциация ВНЕШНЕГО СОЕДИНЕНА в исходном операторе SELECT
Выбрать Дополнительный оператор SELECT используется для извлечения ассоциированной сущности (сущностей)
подвыбор Дополнительный оператор SELECT используется для извлечения всей связанной коллекции. Этот режим предназначен для многих ассоциаций
партия Дополнительное количество операторов SELECT используется для извлечения всей связанной коллекции. Каждый дополнительный SELECT будет извлекать фиксированное количество связанных объектов. Этот режим предназначен для многих ассоциаций

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

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

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

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

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

Это решение было принято после того, как было замечено множество проблем с производительностью, связанных с активной загрузкой по умолчанию в Hibernate 2. К сожалению, JPA придерживалась другого подхода и решила, что многие ассоциации будут ленивыми, в то время как отношения «один к одному» будут охотно доставаться.

Тип ассоциации Политика выборки по умолчанию
@OneTMany LAZY
@ManyToMany LAZY
@ManyToOne EAGER
@Один к одному EAGER

EAGER извлекает несоответствия

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

Ассоциация EAGER всегда извлекается, и стратегия извлечения не является единой для всех методов запросов.

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

product2

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "company_id", nullable = false)
private Company company;
 
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "product", optional = false)
private WarehouseProductInfo warehouseProductInfo;
 
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "importer_id")
private Importer importer;
 
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "product", orphanRemoval = true)
@OrderBy("index")
private Set<Image> images = new LinkedHashSet<Image>();

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

Постоянство Контекстная загрузка

Сначала мы загрузим объект, используя API-интерфейс Persistence Context:

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

Который генерирует следующий оператор SQL SELECT:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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]

Ассоциация компаний EAGER была найдена с использованием внутреннего соединения. Для M таких ассоциаций таблица сущностей-владельцев собирается M раз.

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

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

1
2
3
4
5
6
Product product = entityManager.createQuery(
    "select p " +
            "from Product p " +
            "where p.id = :productId", Product.class)
    .setParameter("productId", productId)
    .getSingleResult();

или с

1
2
3
4
5
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 SELECT:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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 по умолчанию выбирается выборка, поэтому выдается вторичный выбор для каждой отдельной ассоциации EAGER. Чем больше число ассоциаций, тем больше дополнительных отдельных SELECTS, тем больше это повлияет на производительность нашего приложения.

Hibernate Criteria API

Хотя в JPA 2.0 добавлена ​​поддержка запросов Criteria, Hibernate долгое время предлагал конкретную реализацию динамических запросов .

Если метод делегатов реализации EntityManager вызывает унаследованный Session API, реализация JPA Criteria была написана с нуля. По этой причине Hibernate и JPA Criteria API ведут себя по-разному для похожих сценариев запросов.

Предыдущий пример эквивалента Hibernate Criteria выглядит следующим образом:

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

И связанный SQL SELECT:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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]}

В этом запросе используется стратегия выборки соединения , а не выборочная выборка, используемая в JPQL / HQL и Criteria API.

Критерии гибернации и множество коллекций EAGER

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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]}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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));

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

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

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

Вывод

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

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