Найти в Дзене

Как преобразовать прокси-объект Hibernate в реальный объект сущности

Оглавление

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