[Перевод] Руководство для начинающих по стратегиям Hibernate fetching (извлечение данных)
Введение
Когда дело доходит до работы с ORM, все признают важность проектирования базы данных и маппинга сущностей на таблицы. Этим аспектам уделяется много внимания, при этом такие вещи, как стратегии извлечения данных, могут быть просто отложены на потом.
По нашему мнению, стратегия извлечения данных никогда не должна прорабатываться отдельно от непосредственного маппинга сущностей, поскольку отсутствие проработки в этой части, может оказать сильное влияние на скорость работы приложения.
До того, как Hibernate и JPA стали популярны, на разработку каждого запроса тратилось много усилий, потому что вам приходилось явно джойнить (join), таблицы и столбцы, которые вас интересовали. И когда этого было недостаточно, администратор базы данных оптимизировал медленно выполняющиеся запросы.
Во времена JPA запросы JPQL или HQL извлекают сущности вместе с некоторыми связанными с ними отношениями. Это упрощает разработку, поскольку освобождает нас от необходимости вручную выбирать все интересующие нас поля таблицы, кроме того, джойны или другие дополнительные запросы могут генерироваться автоматически. Однако это палка о двух концах. С одной стороны, вы можете реализовывать функционал быстрее, с другой, если ваши автоматически генерируемые SQL-запросы будут неэффективными, общая производительность вашего приложения может значительно снизиться.
Что такое стратегия извлечения (fetching strategy)?
Persistence context (контекст персистентности/сохраняемости) — это кеш первого уровня, в котором все сущности извлекаются из базы данных или сохраняются в ней. Он находится между нашим приложением и постоянным хранилищем. Контекст сохраняемости отслеживает любые изменения, внесенные в управляемый объект. Если во время транзакции что-то меняется, сущность помечается как грязная. Когда транзакция завершается, эти изменения сбрасываются в постоянное хранилище.
Если коротко, контекст персистентности — это набор экземпляров сущностей, который обеспечивает безопасную синхронизацию данных между базой данных и приложением.
Когда JPA загружает объект, он также загружает все EAGER (ЖАДНЫЕ) ассоциации или ассоциации join fetch
. Пока открыт контекст персистентности, переход по LAZY (ЛЕНИВОЙ) ассоциации также приводит к их извлечению с помощью дополнительных запросов.
По умолчанию, для аннотаций JPA @ManyToOne
и @OneToOne
установлена стратегия ЖАДНОГО извлечения отношений, в то время как @OneToMany
и @ManyToMany
являются ЛЕНИВЫМИ. Это стратегии по умолчанию, и Hibernate, никаким магическим способом не оптимизирует поиск объектов, он выполняет только то, что указано в инструкции.
Небольшие приложения не требуют тщательного планирования стратегии извлечения сущностей, однако средние и крупные проекты, никогда не должны забывать про это.
Планирование стратегии извлечения, с самого начала и ее последующая корректировка на протяжении всего цикла разработки — это не какая-то «специальная оптимизация», это естественная часть любой ORM реализации.
По умолчанию стратегия извлечения определяется JPA маппингом, ручное извлечение, предполагает использование JPQL запросов. Лучший совет, который можно дать, — это отдавать предпочтение стратегии ручного извлечения данных. Хотя некоторые ассоциации @ManyToOne
или @OneToOne
имеет смысл всегда извлекать ЖАДНО, для операций извлечения, в большинстве случаев, они не нужны.
Для дочерних ассоциаций всегда безопаснее пометить их как ЛЕНИВЫЕ и джойнить (join fetch
), только тогда, когда это необходимо, потому что они могут легко генерировать большие SQL запросы с ненужными джойнами.
При использовании ЛЕНИВЫХ запросов, необходимо использовать оператор JPQL join fetch
и извлекать только те ассоциации, которые нужны для выполнения конкретного запроса. Если вы забудете правильно »объединить выборку
», контекст персистентности будет выполнять запросы от вашего имени, пока вы будете перемещаться по ЛЕНИВЫМ ассоциациям, и это может привести к проблемам с N+1 или дополнительным SQL-запросам, которые могли быть получены с помощью простого джойна.
Время тестирования
Для примера рассмотрим диаграмму:
Связи сущностей Product
ассоциированы следующим образом:
@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 images = new LinkedHashSet();
Большинство ассоциаций помечены как ЛЕНИВЫЕ, потому что нет необходимости извлекать их все при каждой загрузке ProductWarehouseProductInfo
, Importer
, Image
извлекаются ЛЕНИВО, поскольку не все запросы требуют отображения этих объектов.
Жадно извлекается только Company
, поскольку этот объект будет отображаться всегда при запросе продукта.
Хорошей практикой является явное указание стратегии извлечения данных при объявлении, даже если она является стратегией по умолчанию. Несмотря на то, что у аннотации @ManyToOne
значение по умолчанию EAGER и для нее не обязательно это указывать, явное указание делает код более понятным. @ManyToOne
и @ManyToOne(fetch = FetchType.EAGER)
равнозначны, но второй вариант понятней.
Пример 1: Загрузка продукта по id.
При запросе продукта по id, будет сгенерирован следующий SQL-запрос:
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 = ?
Каждый раз, когда мы загружаем данные через EntityManager
, вместе с выбранным Product
будет загруженаCompany
.
Пример 2: Выбор Product
с помощью запроса JPQL
Запросы JPQL и Criteria могут переопределять стратегии извлечения по умолчанию.
entityManager.createQuery(
"select p " +
"from Product p " +
"where p.id = :productId", Product.class)
.setParameter("productId", productId)
.getSingleResult();
Будет выполнен следующий SQL-запрос:
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 = ?
SELECT company0_.id as id1_6_0_,
company0_.name as name2_6_0_
FROM Company company0_
WHERE company0_.id=?
Запросы JPQL могут переопределять стратегию выборки по умолчанию. Если мы явно не объявляем, что мы хотим получить, используя директивы inner
или left join fetch
, применяется политика извлечения по умолчанию. В случае ЛЕНИВЫХ ассоциаций все неинициализированные сущности могут вызвать исключение LazyInitializationException
если доступ к ним осуществляется из закрытого контекста персистентности. Если контекст персистентности все еще открыт, он будет генерировать дополнительные запросы, что может привести к проблемам с N+1 запросом.
Пример 3: Выбор списка Product
с джойном
На этот раз загрузим список товаров вместе со связанными с ним отношениями между складом и импортером.
entityManager.createQuery(
"select p " +
"from Product p " +
"inner join fetch p.warehouseProductInfo " +
"inner join fetch p.importer", Product.class)
.getResultList();
Будет выполнен следующий SQL-запрос:
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
SELECT company0_.id AS id1_6_0_ ,
company0_.name AS name2_6_0_
FROM Company company0_
WHERE company0_.id = 1
Здесь можно видеть, что стратегия извлечения JPQL переопределяет стратегию ЛЕНИВОЙ выборки по умолчанию.
ЖАДНАЯ ассоциация также переопределена, это является причиной второго запроса.
Пример 4: Выбор списка изображений при явном джойне Product
Стратегия извлечения по умолчанию переопределяется запросом JPQL. Чтобы получить родительский объект, мы должны явно запросить его:
entityManager.createQuery(
"select i " +
"from Image i " +
"inner join fetch i.product p " +
"where p.id = :productId", Image.class)
.setParameter("productId", productId)
.getResultList();
Будет выполнен следующий SQL-запрос:
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
игнорируется, поскольку Hibernate необходимо знать, должен ли он заполнять сущность значением null или прокси. В нашем примере имеет смысл сделать его обязательным, поскольку каждый продукт в любом случае находится на складе. В других случаях вы можете просто сделать ассоциацию однонаправленной и сохранить только ту часть, которая управляет ссылкой (ту, где находится внешний ключ).