1. 概述
本教程将介绍如何将 Hibernate 代理对象 转换为真实的实体对象。首先,我们会说明 Hibernate 在什么情况下会创建代理对象;然后,探讨代理对象的价值所在;最后,模拟一个需要“去代理化”(unproxy)的场景,来展示具体操作。
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.getPayment()
返回的是一个 HibernateProxy
对象,而不是 CreditCardPayment
实例。
相比之下,如果不使用懒加载,则返回的 payment
就是 CreditCardPayment
实例,测试会失败。
✅ 注意:Hibernate 使用字节码增强(bytecode instrumentation)来生成代理对象。
我们可以在调试模式下查看对象结构,例如:
paymentReceipt = {PaymentReceipt@5042}
payment = {Payment$HibernateProxy$CZIczfae@5047} "com.baeldung.jpa.hibernateunproxy.CreditCardPayment@2"
$$_hibernate_interceptor = {ByteBuddyInterceptor@5053}
从中可以看出,Hibernate 使用的是 Byte Buddy 动态生成代理类。
3. Hibernate 代理对象的作用
3.1. 实现延迟加载
我们前面已经提到了,代理对象是实现延迟加载的关键。如果我们移除 @OneToOne
和 @ManyToOne
中的 fetch = FetchType.LAZY
配置:
public class PaymentReceipt {
...
@OneToOne
private Payment payment;
...
}
public abstract class Payment {
...
@ManyToOne
protected WebUser webUser;
...
}
然后执行查询,日志中会看到这样的 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=?
可以看到,Hibernate 为了加载完整对象,执行了多个 JOIN 操作。
而启用懒加载后,SQL 就变得简洁多了:
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=?
✅ 结论:使用代理对象可以有效减少不必要的数据库查询。
3.2. 写入数据时的优化
假设我们要创建一个 Payment
并关联一个 WebUser
。如果不用代理,Hibernate 会先查一次 WebUser
,再插入 Payment
,产生两条 SQL。
但使用代理对象,可以省去 SELECT 查询:
@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);
}
运行后,只有一条 SQL:
insert
into
CreditCardPayment
(amount, webUser_id, cardNumber, id)
values
(?, ?, ?, ?)
✅ 结论:通过代理对象,Hibernate 可以优化写入操作,避免不必要的 SELECT。
4. 场景:为什么需要去代理化?
假设我们的实体继承策略是 Table-per-Class,并且使用了懒加载。当我们获取 PaymentReceipt
时,其关联的 Payment
是一个代理对象。
来看 CreditCardPayment
类:
@Entity
public class CreditCardPayment extends Payment {
private String cardNumber;
...
}
如果我们想访问 cardNumber
字段,就必须将代理对象转换为真实对象。否则,强转会失败:
@Test
public void givenPaymentReceipt_whenCastingPaymentToConcreteClass_thenThrowClassCastException() {
PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
assertThrows(ClassCastException.class, () -> {
CreditCardPayment creditCardPayment = (CreditCardPayment) paymentReceipt.getPayment();
});
}
❌ 因为 payment
是代理对象,直接强转会抛出 ClassCastException
。
5. 如何将代理对象转换为真实实体?
从 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 5.2.10 之前,你需要手动操作:
HibernateProxy hibernateProxy = (HibernateProxy) paymentReceipt.getPayment();
LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
CreditCardPayment unproxiedEntity = (CreditCardPayment) initializer.getImplementation();
虽然也能实现,但不够优雅,推荐使用 Hibernate.unproxy()
。
6. 总结
在这篇文章中,我们学习了:
- ✅ Hibernate 在什么情况下会创建代理对象;
- ✅ 代理对象在懒加载和写入优化中的作用;
- ✅ 为什么有时候必须将代理对象转换为真实对象;
- ✅ 如何使用
Hibernate.unproxy()
方法完成转换。
最后,我们通过多个集成测试验证了这些知识点。
完整代码示例可以在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/persistence-modules/java-jpa-3