1. Обзор
В этом руководстве мы узнаем, как преобразовать прокси-сервер Hibernate в реальный объект-сущность. Перед этим разберемся, когда Hibernate создает прокси-объект. Затем мы поговорим о том, чем полезен прокси-сервер Hibernate. И, наконец, мы смоделируем сценарий, в котором необходимо распаковать объект.
2. Когда Hibernate создает прокси-объект?
Hibernate использует прокси-объекты для отложенной загрузки . Чтобы лучше представить сценарий, давайте посмотрим на сущности 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
.
Для демонстрации создадим и запустим интеграционный тест:
@Test
public void givenPaymentReceipt_whenAccessingPayment_thenVerifyType() {
PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
Assert.assertTrue(paymentReceipt.getPayment() instanceof HibernateProxy);
}
Из теста мы загрузили PaymentReceipt
и убедились, что объект платежа
не является экземпляром CreditCardPayment
— это объект HibernateProxy
.
Напротив, без ленивой загрузки предыдущий тест завершился бы неудачей, поскольку возвращаемый объект платежа
был бы экземпляром CreditCardPayment
.
Кроме того, стоит упомянуть, что Hibernate использует инструментарий байт-кода для создания прокси-объекта.
Чтобы убедиться в этом, мы можем добавить точку останова в строке оператора утверждения интеграционного теста и запустить его в режиме отладки. Теперь посмотрим, что показывает отладчик:
paymentReceipt = {PaymentReceipt@5042}
payment = {Payment$HibernateProxy$CZIczfae@5047} "com.foreach.jpa.hibernateunproxy.CreditCardPayment@2"
$$_hibernate_interceptor = {ByteBuddyInterceptor@5053}
Из отладчика видно, что Hibernate использует Byte Buddy , библиотеку для динамического создания классов Java во время выполнения.
3. Чем полезен Hibernate Proxy?
3.1. Hibernate Proxy для ленивой загрузки
Мы немного узнали об этом ранее. Чтобы придать этому больше значения, давайте попробуем удалить механизм ленивой загрузки как из объектов 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=?
Как видно из журналов, запрос PaymentReceipt
содержит несколько операторов соединения.
Теперь давайте запустим его с ленивой загрузкой:
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 упрощается за счет исключения всех ненужных операторов соединения.
3.2. Hibernate Proxy для записи данных
Чтобы проиллюстрировать, давайте используем его для создания платежа
и назначения ему веб
-пользователя. Без использования прокси это привело бы к двум операторам SQL: оператору SELECT
для получения WebUser
и оператору INSERT
для создания платежа .
Создадим тест с использованием прокси:
@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
для `` создания платежа .
`` ****
4. Сценарий: необходимость распроксирования
Учитывая нашу модель предметной области, давайте предположим, что мы получаем PaymentReceipt.
Как мы уже знаем, он связан с сущностью Payment
, имеющей стратегию наследования Table-per-Class
и тип отложенной выборки .
В нашем случае, на основе заполненных данных, связанный платеж
PaymentReceipt имеет тип 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();
});
}
Из теста мы увидели необходимость преобразовать объект платежа в
CreditCardPayment
. Однако, поскольку объект платежа
по- прежнему является прокси-объектом Hibernate, мы столкнулись с ClassCastException
. **** **
``**
5. Hibernate Proxy to Entity Object
Начиная с Hibernate 5.2.10, мы можем использовать встроенный статический метод для депроксирования сущностей Hibernate:
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 и чем он полезен. Затем мы смоделировали ситуацию, когда необходимо распаковать объект.
Наконец, мы провели несколько интеграционных тестов, чтобы продемонстрировать наши примеры и проверить наше решение.
Как всегда, полный исходный код статьи доступен на GitHub .