Рубрики
Без рубрики

НЕТЕРПЕЛИВАЯ выборка-это запах кода при использовании JPA и гибернации

Узнайте, почему использование ассоциаций с быстрой выборкой плохо сказывается на производительности при использовании JPA и Hibernate. По умолчанию для ассоциаций должна быть установлена функция ОТЛОЖЕННОЙ выборки.

Автор оригинала: Vlad Mihalcea.

Вступление

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

Была ли эта проблема сегодня утром исправлена с помощью stackoverflow, чем нашла это. Спасибо Владу Михальче за краткое объяснение !!! НЕТЕРПЕЛИВАЯ выборка-это запах кода при использовании #JPA и @Hibernate | @vlad_mihalcea https://t.co/7NqKH0S9oG

Получение 101

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

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

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

  • ассоциация всегда инициализируется вместе с ее владельцем (например, НЕТЕРПЕЛИВЫЙ Тип выборки).
  • неинициализированная ассоциация (например, LAZY FetchType) перемещается, поэтому связь должна быть получена с помощью дополнительного ВЫБОРА

Отображение в режиме гибернации, извлекающее информацию, формирует глобальный план извлечения . Во время запроса мы можем переопределить глобальный план выборки, но только для ЛЕНИВЫХ ассоциаций . Для этого мы можем использовать директиву fetch HQL/JPQL/Критерии. НЕТЕРПЕЛИВЫЕ ассоциации не могут быть переопределены, поэтому привязка вашего приложения к глобальному плану выборки невозможна.

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

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

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

@OneToMany ЛЕНИВЫЙ
@ManyToMany ЛЕНИВЫЙ
@manytoon ЖАЖДУЩИЙ
@OneToOne ЖАЖДУЩИЙ

НЕТЕРПЕЛИВОЕ извлечение несоответствий

Хотя может быть удобно просто пометить ассоциации как НЕТЕРПЕЛИВЫЕ, делегируя ответственность за извлечение в режим гибернации, рекомендуется прибегнуть к планам извлечения на основе запросов.

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

Далее я собираюсь продемонстрировать, как НЕТЕРПЕЛИВАЯ выборка ведет себя для всех вариантов запросов в режиме гибернации. Я буду повторно использовать ту же модель сущностей, которую я ранее представил в своей статье о стратегиях извлечения:

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

@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 images = new LinkedHashSet<>();

Ассоциация компаний помечена как НЕТЕРПЕЛИВАЯ, и Hibernate всегда будет использовать стратегию извлечения, чтобы инициализировать ее вместе с ее владельцем.

Загрузка контекста сохранения

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

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

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

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]

Ассоциация НЕТЕРПЕЛИВОЙ компании была получена с помощью внутреннего соединения. Для M таких ассоциаций таблица сущностей-владельцев будет присоединена M раз.

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

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

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

или с

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

Написание запросов API критериев JPA не очень просто. Плагин Codota IDE может помочь вам в написании таких запросов, что повысит вашу производительность.

Для получения более подробной информации о том, как вы можете использовать Codota для ускорения процесса написания запросов API критериев, ознакомьтесь с этой статьей .

Создает следующие инструкции SQL SELECT:

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, так и запросы критериев по умолчанию используют select выборку, поэтому выдают дополнительный выбор для каждой отдельной ассоциации. Чем больше число ассоциаций, тем больше дополнительных индивидуальных вариантов, тем больше это повлияет на производительность нашего приложения.

API критериев гибернации

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

Если метод делегатов реализации EntityManager вызывает устаревший API сеанса, реализация критериев JPA была написана с нуля. Вот причина, по которой Hibernate и API критериев JPA ведут себя по-разному для аналогичных сценариев запросов.

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

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 выборки, в отличие от select выборки, используемой JPQL/HQL и API критериев.

Критерии гибернации и НЕТЕРПЕЛИВЫЕ коллекции

Давайте посмотрим, что произойдет, когда стратегия извлечения изображений коллекции будет настроена на НЕТЕРПЕЛИВЫЙ:

@OneToMany(
    mappedBy = "product", 
    fetch = FetchType.EAGER, 
    cascade = CascadeType.ALL, 
    orphanRemoval = true
)
@OrderBy("index")
private Set 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]}

Критерии гибернации не группируют автоматически список родительских сущностей. Из-за ОБЪЕДИНЕНИЯ дочерних таблиц “один ко многим” для каждой дочерней сущности мы получим новую ссылку на объект родительской сущности (все они указывают на один и тот же объект в нашем текущем контексте сохранения).:

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));

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

Чтобы исправить это, нам нужно указать критериям гибернации использовать отдельные корневые сущности:

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

assertEquals(1, products.size());

Вывод

Стратегия НЕТЕРПЕЛИВОЙ выборки-это запах кода. Чаще всего он используется для простоты, не принимая во внимание долгосрочные штрафы за производительность. Стратегия извлечения никогда не должна быть ответственностью за сопоставление сущностей. Каждый бизнес-вариант использования имеет различные требования к загрузке объектов, и поэтому стратегия извлечения должна быть делегирована каждому отдельному запросу.

Глобальный план выборки должен определять только ЛЕНИВЫЕ ассоциации, которые извлекаются на основе каждого запроса. В сочетании со стратегией всегда проверять сгенерированные запросы планы выборки на основе запросов могут повысить производительность приложений и снизить затраты на обслуживание.