1. Обзор
В этом руководстве мы узнаем, как преобразовать прокси-объект Hibernate в реальный объект сущности. Сначала разберёмся, когда Hibernate создаёт прокси-объект, затем — зачем он нужен, и наконец смоделируем ситуацию, в которой возникает необходимость «распроксить» объект.
2. Когда Hibernate создаёт прокси-объект?
Hibernate использует прокси-объекты для реализации ленивой загрузки (lazy loading). Чтобы лучше понять это, рассмотрим сущности PaymentReceipt и Payment:
@Entity
public class PaymentReceipt {
...
@OneToOne(fetch = FetchType.LAZY)
private Payment payment;
...
}
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
...
@ManyToOne(fetch = FetchType.LAZY)
protected WebUser webUser;
...
}
Например, загрузка любой из этих сущностей приведёт к тому, что Hibernate создаст прокси-объект для связанного поля с FetchType.LAZY.
Для демонстрации создадим и запустим интеграционный тест:
javaКопироватьРедактировать@Test
public void givenPaymentReceipt_whenAccessingPayment_thenVerifyType() {
PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
Assert.assertTrue(paymentReceipt.getPayment() instanceof HibernateProxy);
}
В этом тесте мы загружаем PaymentReceipt и проверяем, что поле payment не является экземпляром CreditCardPayment — это объект HibernateProxy.
Для сравнения: без ленивой загрузки этот тест завершился бы с ошибкой, так как возвращаемый объект payment был бы экземпляром CreditCardPayment.
Также стоит упомянуть, что Hibernate использует инструментирование байткода для создания прокси-объектов.
Чтобы убедиться в этом, можно установить точку останова на строке с утверждением (assert) и запустить тест в режиме отладки. Посмотрим, что покажет отладчик:
perlКопироватьРедактироватьpaymentReceipt = {PaymentReceipt@5042}
payment = {Payment$HibernateProxy$CZIczfae@5047} "com.baeldung.jpa.hibernateunproxy.CreditCardPayment@2"
$$_hibernate_interceptor = {ByteBuddyInterceptor@5053}
Из вывода отладчика видно, что Hibernate использует Byte Buddy — библиотеку для динамической генерации Java-классов во время выполнения.
3. Зачем нужен прокси в Hibernate?
3.1. Прокси Hibernate для ленивой загрузки
Мы уже немного затрагивали это. Чтобы подчеркнуть значимость, попробуем отключить ленивую загрузку в сущностях PaymentReceipt и Payment:
public class PaymentReceipt {
...
@OneToOne
private Payment payment;
...
}
public abstract class Payment {
...
@ManyToOne
protected WebUser webUser;
...
}
Теперь попробуем быстро получить PaymentReceipt и посмотрим на сгенерированный SQL в логах:
select
paymentrec0_.id as id1_2_0_,
paymentrec0_.payment_id as payment_3_2_0_,
paymentrec0_.transactionNumber as transact2_2_0_,
payment1_.id as id1_1_1_,
payment1_.amount as amount2_1_1_,
payment1_.webUser_id as webuser_3_1_1_,
payment1_.cardNumber as cardnumb1_0_1_,
payment1_.clazz_ as clazz_1_,
webuser2_.id as id1_3_2_,
webuser2_.name as name2_3_2_
from
PaymentReceipt paymentrec0_
left outer join
(
select
id,
amount,
webUser_id,
cardNumber,
1 as clazz_
from
CreditCardPayment
) payment1_
on paymentrec0_.payment_id=payment1_.id
left outer join
WebUser webuser2_
on payment1_.webUser_id=webuser2_.id
where
paymentrec0_.id=?
Как видно, запрос содержит много объединений (JOIN).
Теперь запустим тот же код с включённой ленивой загрузкой:
select
paymentrec0_.id as id1_2_0_,
paymentrec0_.payment_id as payment_3_2_0_,
paymentrec0_.transactionNumber as transact2_2_0_
from
PaymentReceipt paymentrec0_
where
paymentrec0_.id=?
Очевидно, что сгенерированный SQL становится проще, поскольку Hibernate исключает ненужные JOIN-запросы, отложив загрузку связанных сущностей до фактического обращения к ним.
3.2. Прокси Hibernate для записи данных
Для примера давайте создадим объект Payment и назначим ему WebUser. Без использования прокси это приведёт к выполнению двух SQL-запросов: одного SELECT для получения WebUser и одного INSERT для создания Payment.
Создадим тест с использованием прокси:
@Test
public void givenWebUserProxy_whenCreatingPayment_thenExecuteSingleStatement() {
entityManager.getTransaction().begin();
WebUser webUser = entityManager.getReference(WebUser.class, 1L);
Payment payment = new CreditCardPayment(new BigDecimal(100), webUser, "CN-1234");
entityManager.persist(payment);
entityManager.getTransaction().commit();
Assert.assertTrue(webUser instanceof HibernateProxy);
}
Стоит отметить, что мы используем entityManager.getReference(...) для получения прокси-объекта.
Теперь запустим тест и посмотрим логи:
insert
into
CreditCardPayment
(amount, webUser_id, cardNumber, id)
values
(?, ?, ?, ?)
Как видно, при использовании прокси Hibernate выполняет только один запрос: INSERT для создания Payment.
4. Сценарий: необходимость в распроксировании
Допустим, мы получаем PaymentReceipt. Как мы уже знаем, он связан с сущностью Payment, которая использует стратегию наследования Table-per-Class и тип загрузки LAZY.
В нашем случае, исходя из данных, связанный с PaymentReceipt объект Payment — это CreditCardPayment. Однако из-за ленивой загрузки мы получим прокси-объект.
Вот как выглядит сущность CreditCardPayment:
@Entity
public class CreditCardPayment extends Payment {
private String cardNumber;
...
}
Без распроксирования невозможно обратиться к полю cardNumber класса CreditCardPayment. Тем не менее, попробуем привести объект к CreditCardPayment и посмотрим, что произойдёт:
@Test
public void givenPaymentReceipt_whenCastingPaymentToConcreteClass_thenThrowClassCastException() {
PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
assertThrows(ClassCastException.class, () -> {
CreditCardPayment creditCardPayment = (CreditCardPayment) paymentReceipt.getPayment();
});
}
Как видно из теста, попытка привести объект payment к CreditCardPayment приводит к ClassCastException, потому что это всё ещё Hibernate-прокси.
5. Преобразование прокси Hibernate в реальный объект сущности
Начиная с Hibernate 5.2.10, можно использовать встроенный статический метод для распроксирования:
Hibernate.unproxy(paymentReceipt.getPayment());
Финальный интеграционный тест с использованием этого подхода:
@Test
public void givenPaymentReceipt_whenPaymentIsUnproxied_thenReturnRealEntityObject() {
PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
Assert.assertTrue(Hibernate.unproxy(paymentReceipt.getPayment()) instanceof CreditCardPayment);
}
Из теста видно, что мы успешно преобразовали прокси-объект Hibernate в реальный объект сущности.
До Hibernate 5.2.10 использовалось такое решение:
HibernateProxy hibernateProxy = (HibernateProxy) paymentReceipt.getPayment();
LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
CreditCardPayment unproxiedEntity = (CreditCardPayment) initializer.getImplementation();
6. Заключение
В этом руководстве мы узнали, как преобразовать прокси-объект Hibernate в реальный объект сущности. Кроме того, мы рассмотрели, как работает прокси Hibernate и зачем он нужен. Также мы смоделировали ситуацию, в которой возникает необходимость в распроксировании объекта.
Наконец, мы выполнили несколько интеграционных тестов, чтобы продемонстрировать примеры и подтвердить работоспособность решения.
Оригинал статьи: https://www.baeldung.com/hibernate-proxy-to-real-entity-object