1、概览 {#1概览}
本文将带你了解 Hibernate 是在什么时候创建代理对象的,代理对象有啥用?以及如何把 Hibernate 的代理对象转换为真正的实体对象。
2、Hibernate 何时创建代理对象? {#2hibernate-何时创建代理对象}
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
,并验证了 payment
对象不是 CreditCardPayment
的实例,而是一个 HibernateProxy
对象。
相反,如果没有懒加载,前面的测试就会失败,因为返回的 payment
对象将是 CreditCardPayment
的实例。
Hibernate 使用字节码工具来创建代理对象。
为了验证这一点,可以在集成测试的断言语句行上添加一个断点,并在 Debug 模式下运行。现在,来看看 Debugger 会显示什么:
paymentReceipt = {PaymentReceipt@5042}
payment = {Payment$HibernateProxy$CZIczfae@5047} "com.baeldung.jpa.hibernateunproxy.CreditCardPayment@2"
$_hibernate_interceptor = {ByteBuddyInterceptor@5053}
从 Debugger 中,可以看到 Hibernate 使用的是 ByteBuddy,这是一个在运行时动态生成 Java 类的库。
3、Hibernate 代理对象有啥用? {#3hibernate-代理对象有啥用}
3.1、懒加载 {#31懒加载}
先尝试从 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
的查询包含多条 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=?
显然,由于省略了所有不必要的 join
语句,生成的 SQL 语句得到了简化。
3.2、写入数据 {#32写入数据}
为了说明这一点,创建一个 Payment
对象并为其设置一个 WebUser
对象。如果不使用代理,就会产生两条 SQL 语句:一条是检索 WebUser
的 SELECT
语句,另一条是创建 Payment
的 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 只执行了一条语句:创建 Payment
的 INSERT
语句。
4、需要取消 Hiernate 代理对象的场景 {#4需要取消-hiernate-代理对象的场景}
根据上述 Domain Model,假设要检索 PaymentReceipt
。它与一个 Payment
实体相关联,该实体具有 Table-per-Class 的继承策略和延迟加载(lazy fetch type)。
在本例中,PaymentReceipt
的关联 Payment
型为 CreditCardPayment
。不过,由于使用的是懒加载,因此它是一个代理对象。
现在,来看看 CreditCardPayment
实体:
@Entity
public class CreditCardPayment extends Payment {
private String cardNumber;
...
}
也就是说,如果不取消 Payment
对象的代理,就无法从 CreditCardPayment
类中获取 cardNumber
字段。不管怎样,试着将 Payment
对象转换为 CreditCardPayment
,看看会发生什么:
@Test
public void givenPaymentReceipt_whenCastingPaymentToConcreteClass_thenThrowClassCastException() {
PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
assertThrows(ClassCastException.class, () -> {
CreditCardPayment creditCardPayment = (CreditCardPayment) paymentReceipt.getPayment();
});
}
如上,强制把 payment
对象转换为 CreditCardPayment
。然而,由于 payment
对象仍然是 Hibernate 代理对象,所以抛出了 ClassCastException
异常。
5、将 Hibernate 代理对象转换为实体对象 {#5将-hibernate-代理对象转换为实体对象}
自 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 及其以前的版本,那么可以如下方式来将 Hibernate 代理对象转换成真正的实体对象。
HibernateProxy hibernateProxy = (HibernateProxy) paymentReceipt.getPayment();
LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
CreditCardPayment unproxiedEntity = (CreditCardPayment) initializer.getImplementation();
6、总结 {#6总结}
本文介绍了 Hibernate 代理对象的原理和它的用处,以及如何将 Hibernate 代理对象转换成真正的实体对象。
Ref:https://www.baeldung.com/hibernate-proxy-to-real-entity-object