Статьи

Факты гибернации: важность стратегии извлечения

Когда дело доходит до работы с инструментом ORM, все признают важность проектирования базы данных и сопоставления сущностей с таблицами. Этим аспектам уделяется много внимания, в то время как такие вещи, как выбор стратегии, могут быть просто отложены.

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

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

Во времена JPA запросы JPA-QL или HQL выбирают сущности вместе с некоторыми из их взаимосвязей. Это облегчает разработку, поскольку освобождает нас от ручного выбора всех интересующих нас полей таблицы, а иногда автоматически создаются соединения или дополнительные запросы для удовлетворения наших потребностей.

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

Так что же такое стратегия извлечения сущностей?

Когда JPA загружает объект, он также загружает все ассоциации EAGER или «join fetch». Пока контекст постоянства открыт, навигация по ассоциациям LAZY приводит к их извлечению через дополнительные выполняемые запросы.

По умолчанию аннотации JPA @ManyToOne и @OneToOne выбираются EAGERly, а отношения @OneToMany и @ManyToMany считаются LAZY. Это стратегия по умолчанию, и Hibernate волшебным образом не оптимизирует поиск вашего объекта, он делает только то, что ему поручено.

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

Планирование стратегии извлечения с самого начала и ее корректировка на протяжении всего цикла разработки — это не «преждевременная оптимизация», это просто естественная часть любого проекта ORM.

Стратегия выборки по умолчанию — та, которую вы определяете для отображения JPA, тогда как выборка вручную выполняется при использовании запросов JPA-QL.

Лучший совет, который я могу вам дать, — это выбрать стратегию ручной выборки (определенную в запросах JPA-QL с использованием оператора выборки). Хотя некоторые ассоциации @ManyToOne или @OneToOne имеют смысл всегда получать с нетерпением, в большинстве случаев они не нужны для каждой операции извлечения.

Для дочерних ассоциаций всегда безопаснее помечать их как LAZY и только «извлекать их» только при необходимости, поскольку они могут легко генерировать большие наборы результатов SQL с ненужными объединениями.

Для определения большинства ассоциаций, определенных как LAZY, нам необходимо использовать оператор JPA-QL «join fetch» ​​и извлекать только те ассоциации, которые нам нужны для выполнения данного запроса. Если вы забудете «соединить выборку» правильно, контекст постоянства будет запускать запросы от вашего имени, пока вы перемещаетесь по ленивым ассоциациям, и это может вызвать проблемы «N + 1» или дополнительные запросы SQL, которые могли бы быть получены с помощью простого объединения. в первую очередь.

Для конкретного примера давайте начнем со следующей диаграммы:

product2

Ассоциации сущностей Продукта отображаются как:

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

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

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

Рекомендуется явно задавать стратегию извлечения по умолчанию (это делает код более информативным), даже если @ManyToOne по умолчанию использует опцию извлечения EAGER.

Вариант использования 1: загрузка продукта по идентификатору генерирует следующий SQL

01
02
03
04
05
06
07
08
09
10
SELECT product0_.id          AS id1_7_1_,
       product0_.code        AS code2_7_1_,
       product0_.company_id  AS company_4_7_1_,
       product0_.importer_id AS importer5_7_1_,
       product0_.name        AS name3_7_1_,
       company1_.id          AS id1_1_0_,
       company1_.name        AS name2_1_0_
FROM   product product0_
INNER JOIN company company1_ ON product0_.company_id = company1_.id
WHERE  product0_.id = ?

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

Вариант использования 2: выбор продукта с помощью запроса JPA-QL (в обход кэша первого уровня сохраняемого контекста)

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

Это выполняет следующий запрос SQL:

1
2
3
4
5
6
7
SELECT product0_.id          AS id1_7_,
       product0_.code        AS code2_7_,
       product0_.company_id  AS company_4_7_,
       product0_.importer_id AS importer5_7_,
       product0_.name        AS name3_7_
FROM   product product0_
WHERE  product0_.id = ?

Таким образом, использование JPA-QL переопределяет стратегию выборки по умолчанию, но все равно оставляет нас уязвимыми, если мы хотим перемещаться по ленивым ассоциациям. Если контекст постоянства закрывается, мы получаем LazyInitializationException при доступе к отложенным отношениям, но если он не закрыт, он будет генерировать дополнительные запросы выбора, которые могут повлиять на производительность приложения.

Вариант использования 3: выбор списка продуктов с соответствующими ассоциациями склада и импортера:

1
2
3
4
5
6
entityManager.createQuery(
   "select p " +
   "from Product p " +
   "inner join fetch p.warehouseProductInfo " +
   "inner join fetch p.importer", Product.class)
.getResultList();

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

01
02
03
04
05
06
07
08
09
10
11
12
SELECT product0_.id          AS id1_7_0_,
       warehousep1_.id       AS id1_11_1_,
       importer2_.id         AS id1_3_2_,
       product0_.code        AS code2_7_0_,
       product0_.company_id  AS company_4_7_0_,
       product0_.importer_id AS importer5_7_0_,
       product0_.name        AS name3_7_0_,
       warehousep1_.quantity AS quantity2_11_1_,
       importer2_.name       AS name2_3_2_
FROM   product product0_
INNER JOIN warehouseproductinfo warehousep1_ ON product0_.id = warehousep1_.id
INNER JOIN importer importer2_ ON product0_.importer_id = importer2_.id

Здесь вы можете видеть, что стратегия явного извлечения JPA-QL переопределяет стратегию по умолчанию. Поскольку мы не указали «соединение» с Компанией, ассоциация EAGER игнорируется.

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

1
2
3
4
5
6
7
entityManager.createQuery(
   "select i " +
   "from Image i " +
   "inner join fetch i.product p " +
   "where p.id = :productId", Image.class)
.setParameter("productId", productId)
.getResultList();

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

01
02
03
04
05
06
07
08
09
10
11
12
SELECT image0_.id            AS id1_2_0_,
       product1_.id          AS id1_7_1_,
       image0_.index         AS index2_2_0_,
       image0_.name          AS name3_2_0_,
       image0_.product_id    AS product_4_2_0_,
       product1_.code        AS code2_7_1_,
       product1_.company_id  AS company_4_7_1_,
       product1_.importer_id AS importer5_7_1_,
       product1_.name        AS name3_7_1_
FROM   image image0_
INNER JOIN product product1_ ON image0_.product_id = product1_.id
WHERE  product1_.id = ?

Я должен добавить еще одну вещь, касающуюся отношения @oneToOne для warehouseProductInfo. Для необязательных ассоциаций @OnetoOne атрибут LAZY игнорируется, поскольку Hiberante должен знать, должен ли он заполнять вашу сущность нулем или прокси. В нашем примере имеет смысл сделать его обязательным, поскольку каждый товар в любом случае находится на складе. В других случаях вы можете просто сделать ассоциацию однонаправленной и оставить только часть, управляющую ссылкой (ту, где находится внешний ключ).

  • Код доступен на GitHub .