Введение
Разные 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 использует другой подход, при котором связи "ко-многим" являются ЛЕНИВЫМИ, в то время как связи "к одному" являются ЖАДНЫМИ.
Тип ассоциации |
Политика по умолчанию |
LAZY |
|
LAZY |
|
EAGER |
|
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 связи, которые настраиваются для каждого отдельного запроса. В сочетании со стратегией обязательной проверки сгенерированных запросов, планы выборки на основе запросов могут повысить производительность приложения и снизить затраты на обслуживание.
Комментарии (14)
Iqorek
17.01.2025 15:08Пришел к аналогичным выводам. Этот фреймворк, вероятно, создавался с целью упростить процессы, но в итоге всё вышло сложным, неочевидным да еще и медленным. Я этими фичами вообще не пользуюсь и все работает без проблем, а вот в параллельном проекте да и там постоянно, что то происходит
GarfieldX
17.01.2025 15:08Как и все фреймворки. Ради мнимого ускорения разработки, которое нивелируется кривыми ручками способными испортить что угодно, получаем тормоза и прожорливость в работе. Достаточно глянуть сколько жрут браузеры на якобы современных и прогрессивных сайтах.
Kassiy_Pontiy_Pilat
17.01.2025 15:08На самом деле каждому инструменту свое применение. Если бездумно вытаскивать коллекции с данной аннотацией без ограничения по размеру вы скорее всего получите проблемы. Но если вы точно знаете что вам надо только один объект к которому вы будете вынуждены потом по всем его вложенным коллекциям пройтись чтобы их подтянуло из БД то почему бы и нет. Вопрос в поддерживаемости этого кода в дальнейшем но для этого есть например ассерты или еще какие то интерфейсные ограничения
ruomserg
17.01.2025 15:08Вот ничего себе рекомендация! Ленивая или немедленная выборка подчиненных/связанных объектов должна диктоваться логикой приложения и структурой таблиц! Иначе у вас на каждый чих будет случаться дополнительный запрос в БД - а это 10-60 мсек потерь чисто за то чтобы на другой сервер за данными сходить. Сто запросов - и вот уже на ровном месте полсекунды производительности потеряли...
Другое дело - что иногда eager начинают применять для того чтобы лечить веселые экспешены инициализации проксей/коллекций: "сессия уже закрыта, и транзакшн-менеджер не может начать новую в этом месте". Так делать, конечно же, не надо - это значит что приложение неправильно спроектировано, и объекты из БД утекают за пределы транзакции без ясного понимания зачем это делается! Если же бездумно ставить Eager на все такие случаи - то скоро по каждому чиху в память будет подниматься копия БД, и начнутся дикие тормоза и OOM...
NihilPersonalis
17.01.2025 15:08Дополнительные запросы всяко лучше, чем запрос всего и вся сразу. Вероятность что они понадобятся весьма мала. Потому короткие дозапросы весьма логичны. Ещё и нагрузку размазывают во времени. Вообще же счастье в балансе, конечно же. Который, в свою очередь опирается на архитектуру и логику работы системы. Проблема в том что многие разрабы об этом не задумываются.
kulity
Слова "FetchType EAGER" в заголовке статьи явно лишние
rexer
Проблема Hibernate, кмк, в том, что за внешней простотой скрывается достаточно большой пласт дополнительных знаний, которые вам необходимо иметь. При этом, если их нет вы столкнетесь с проблемами только по ходу даже не разработки скорее всего, а уже эксплуатации продукта в продакшене. И эти знания должны быть у всех членов команды разработки, а это не всегда достижимо. И вот эта сложность, которая очень хорошо скрыта и завуалирована - главная проблема в использовании. Hibernate, кмк, очень хорош в прототипировании и mvp, после уже становится сложно.
Видел много команд, кто как раз запускался на Hibernate и после с него уходили.
headliner1985
На самом деле это не большой пласт, всем этим проблемам, как и их решениям уже сто лет в обед, нужно просто сесть, прочитать пару статей и выучить их уже наконец. Меня удивляют люди которые зачитываются и учат наизусть тома по базам данных на сотни страниц и их оптимизации, зато потом кричат: ой орм зло, тут столько проблем и все неочевидно, я лучше руками сам все напишу. Выучи уже наконец эти подходы и работай себе в удовольствие)
gerashenko
Плюсую
y90a19
Скорее солидную такую книгу страниц в 600. И все это постоянно забывается.
Моё мнение по хибернейту:
Какую проблему решает хибернейт? Борьба со сложностью? Нет, оно добавляет сложность, но маскирует её.
Экономия времени разработки? Возможно, но экономит на самом бутылочном горлышке приложения - общении с БД. Эта экономия вылазит боком при начале эксплуатации.
Остается третий вариант: все используют и мы используем. Кто мы такие чтобы сомневаться в умных дядях.
Возможно изначально это создавалось чтобы избавить от необходимости знать sql. Но на деле выходит что знать sql с хибернейтом надо ещё лучше, плюс приходиться изучать кишочки хибернейта.
ruomserg
Я виду один плюс - но большо-о-ой. Если вы стратегически правильно расставили аннотации Transactional - вам не надо вручную отслеживать и сохранять изменения извлеченных объектов обратно в БД. То есть - невозможна ситуация, когда вы изменили объект, а в базу его не записали обратно. Или наоборот - записали, а потом перезатерли другой копией того же объекта, но по-другому измененной. При ручном управлении базой данных такие ошибки почему-то рано или поздно возникают, а ловить их - тот еще цирк... А попытки это автоматизировать - приводят обратно к написанию куска Hibernate, только хуже и с глюками. Этот-то хоть уже вылизали за столько лет...
y90a19
Вот это "мы изменили поле в объекте и оно должно автоматически сохраниться в бд" я считаю порочной практикой.
1) Это магия, сайд-эффект, ниразу не очевидное поведение
2) Похоже у вас в коде бардак, что вы не можете уследить за изменениями объекта и что несколько мест могут внести изменения и перезатереть, а вы об этом не знаете. Я предпочитаю идти по пути бизнес-действие = транзакция. Входные данные, их обработка, сохранение в бд. При чём в бд должны изменяться не все данные, как в случае с хибернейтом, а только изменяемые по этому бизнес-сценарию. Как такое реализовать в хибернейте не представляю. То что у вас объект с данными куда-то уходит, кто-то что-то там делает и вы не знаете что и зачем, и при этом ещё и хотите автоматом сохранить всё что там кто-то наделал - это караул.
Хибернейт получается поощряет такое
ruomserg
Я соглашусь с тем, что это сложный вопрос - и все существующие решения плохие: приходится выбирать чем жертвовать, а что важнее получить.
Смотрите - когда у вас простой CRUD - действительно, магия Hibernate выглядит избыточной. Чего проще - вытащили объект, модифицировали, сохранили... Но когда строится сложная система, которая должна работать у разных заказчиков без переписывания кода - приходится принудительно встраивать в систему возможность устраивания бардака (например, через хуки и события которые можно подключать позже через конфигурацию). И тут получается, что изначальный разработчик больше не контролирует жизненный цикл объекта так, как это было в простом сервисе. Потому что неизвестно, что напишут другие люди в этих хуках и кастомизациях.
Hibernate в этой части удобен тем, что он дает единые (и, в целом, понятные - хотя и непростые) правила жизненного цикла объектов и связи их с БД. Соответственно, что разработчик core functions, что разработчик хуков/плагинов могут рассчитывать на то, что эти правила будут соблюдаться (и баги в этой части уже вычищны).
Поэтому да - использование JPA/Hibernate не является строго обязательным. Можно посмотреть в сторону более легковесных решений типа MyBatis или чего-то еще. Но есть и ситуации, когда некоторое количество магии (эхм, давайте вспомним сколько ее в спринге?) делает жизнь легче...
breninsul
а зачем обкладываться дополнительными, протекающими абстракциями, нюансы которых надо знать в дополнение к знаниям о работе БД и SQL (а иначе вы не сможете анализировать проблемы производительности и строить адекватную схему БД), когда уже есть старый добрый SQL?