1. 概述

本教程将介绍如何将 Hibernate 代理对象 转换为真实的实体对象。首先,我们会说明 Hibernate 在什么情况下会创建代理对象;然后,探讨代理对象的价值所在;最后,模拟一个需要“去代理化”(unproxy)的场景,来展示具体操作。

2. Hibernate 什么时候创建代理对象?

**Hibernate 使用代理对象实现 延迟加载**。为了更直观地说明这一点,我们来看两个实体类:PaymentReceiptPayment

@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


原始标题:How to Convert a Hibernate Proxy to a Real Entity Object | Baeldung