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 使用字節碼 插樁 (instrumentation) 來創建代理對象。
為了驗證這一點,我們可以為集成測試的斷言語句添加斷點,然後在調試模式下運行它。現在,讓我們看看調試器顯示的內容:
paymentReceipt = {PaymentReceipt@5042}
payment = {Payment$HibernateProxy$CZIczfae@5047} "com.baeldung.jpa.hibernateunproxy.CreditCardPayment@2"
$$_hibernate_interceptor = {ByteBuddyInterceptor@5053} 從調試器中可以看出,Hibernate 正在使用 Byte Buddy,它是一個在運行時動態生成 Java 類庫。
3. 為什麼 Hibernate 代理有用?
Hibernate 代理提供了一系列優勢,使其成為 Hibernate 應用程序中常用的模式。以下是一些關鍵原因:
-
避免對象圖層 (Object Graph) 的加載: 代理只在需要時才加載實際的持久化對象。這意味着只有在查詢結果需要被立即使用時,才會創建和加載這些對象。對於大型對象圖層,這可以顯著減少內存使用量和數據庫訪問次數。
-
提高性能: 通過延遲加載,代理可以避免不必要的對象加載,從而提高應用程序的性能。
-
簡化對象圖層管理: 代理可以幫助管理對象圖層,例如,它可以處理對象之間的依賴關係,並確保對象在被垃圾回收之前被正確地釋放。
-
支持多種持久化策略: 代理可以與各種持久化策略一起使用,例如,它可以與緩存、事務管理和數據驗證一起使用。
-
提供更好的可測試性: 代理可以用於模擬持久化對象,從而更容易地對應用程序進行單元測試。
以下是一個簡單的 Java 代碼示例,演示瞭如何使用 Hibernate 代理:
// 假設的 Java 代碼示例
public class User {
private String name;
public String getName() {
return name;
}
}
// 使用 Hibernate 代理的示例
public class UserProxy {
private User user;
public User getUser() {
// 在這裏可以添加邏輯來加載真實的 User 對象
// 例如,使用 Hibernate Session 獲取 User 對象
return user;
}
}
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=?如我們從日誌中可以看出,查詢語句 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 代理進行數據寫入
為了説明,我們使用它來創建一個 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僅執行了一條語句:一個用於創建Payment的INSERT語句。
4. 場景:無需反向代理的需求
鑑於我們的領域模型,假設我們正在檢索一個 支付憑證。 就像我們已經知道的,它與一個 支付 實體相關聯,該實體具有 按類表 的繼承策略和延遲獲取類型。
在我們的案例中,基於填充的數據,與 支付憑證 相關的 支付 實體是 信用卡支付 類型。 然而,由於我們使用了延遲加載,它將是一個代理對象。
現在,讓我們來看一下 信用卡支付 實體:
@Entity
public class CreditCardPayment extends Payment {
private String cardNumber;
...
}確實,無法從CreditCardNumber字段中檢索,而不必先反向代理Payment對象。 無論如何,我們嘗試將Payment對象轉換為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 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 代理的工作原理以及其優勢。然後,我們模擬了需要取消代理對象的情況。
最後,我們運行了多個集成測試,以展示我們的示例並驗證我們的解決方案。