Hibernate 中 @ConcreteProxy 的使用指南
1. 概述
在使用 Hibernate 時,我們經常使用延遲載入來最佳化效能,即僅在存取相關實體時才取得它們。為了實現這一點,Hibernate 會產生代理對象,這些物件在實際實體初始化之前一直取代它們。
然而,在使用繼承層次結構時,這些代理人的類型被定義為宣告的關聯類型,而不是實際的實體子類別。這會導致類型檢查和類型轉換操作失敗。
在本教程中,我們將探討如何使用 Hibernate 的@ConcreteProxy註解來正確地延遲載入繼承層次結構中的多態關聯。
2. 項目設定
在探索 Hibernate 中的@ConcreteProxy註解之前,讓我們先設定一個簡單的應用程序,我們將在本教程中使用它。
2.1. 依賴關係
首先,讓我們將Hibernate 依賴項新增到專案的pom.xml檔案中:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.0.Final</version>
</dependency>
此依賴項為我們提供了 Hibernate ORM 的核心功能,包括我們在本教程中討論的@ConcreteProxy註解。
或者,如果我們正在建立 Spring Boot 應用程序,我們可以使用Spring Data JPA starter ,它已經包含了此依賴項。
另外,請注意@ConcreteProxy是在 6.6.0.Final 版本中引入的,因此在按照本教學操作時,我們需要確保至少使用此版本。
2.2 定義一個簡單的繼承層次結構
接下來,為了示範代理程式類型解析問題及其解決方案,我們將定義一個簡單的繼承層次結構,並建模一個基本的嚮導管理模式。
首先,讓我們使用單表繼承策略來建立一個抽象的HogwartsHouse實體:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
abstract class HogwartsHouse {
@Id
@GeneratedValue
private Long id;
private String founder;
private String houseColors;
// standard constructors, getters, and setters
}
在這裡,我們定義一個抽象基類,它具有所有霍格華茲學院共有的共同屬性。
接下來,我們創建兩個具體的子類別:
@Entity
class Gryffindor extends HogwartsHouse {
private boolean hasSummonedSword;
// standard constructors, getters, and setters
}
@Entity
class Slytherin extends HogwartsHouse {
private boolean heirOfSlytherin;
// standard constructors, getters, and setters
}
在這裡,我們定義了Gryffindor和Slytherin類,每個類別都繼承自HogwartsHouse ,並具有自己獨特的屬性。
最後,讓我們創建一個與HogwartsHouse具有多對一關係的Wizard實體:
@Entity
class Wizard {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private HogwartsHouse hogwartsHouse;
// standard constructors, getters, and setters
}
請注意,為了重現我們打算研究的代理行為,我們已明確將獲取類型設為LAZY 。
3. 理解代理類型解析的問題
現在我們已經建立了繼承層次結構,接下來讓我們探討一下在嘗試確定延遲載入實體的具體類型時出現的問題:
Long wizardId;
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
try (Session session = sessionFactory.openSession()) {
session.getTransaction().begin();
Gryffindor gryffindor = new Gryffindor("Godric Gryffindor", "Scarlet and Gold", true);
session.persist(gryffindor);
Wizard wizard = new Wizard("Neville Longbottom", gryffindor);
session.persist(wizard);
wizardId = wizard.getId();
session.getTransaction().commit();
}
try (Session session = sessionFactory.openSession()) {
Wizard wizard = session.find(Wizard.class, wizardId);
HogwartsHouse hogwartsHouse = wizard.getHogwartsHouse();
assertThat(hogwartsHouse)
.isInstanceOf(HogwartsHouse.class);
assertThat(hogwartsHouse.getId())
.isNotNull()
.isPositive();
}
在這裡,我們首先持久化一個Gryffindor實例並將其與一個wizard關聯起來。
然後,在另一個會話中,我們檢索wizard並訪問他們的hogwartsHouse 。我們可以對物件進行類型檢查,使其與基底類別匹配,並存取基類特有的欄位。
但是,讓我們看看當我們嘗試檢查hogwartsHouse是否特指Gryffindor學院時會發生什麼:
assertThat(hogwartsHouse)
.isInstanceOf(Gryffindor.class);
當我們執行上述斷言時,會遇到類似如下的錯誤訊息:
Expecting actual:
com.baeldung.concreteproxy.Gryffindor@156b31d
to be an instance of:
com.baeldung.concreteproxy.Gryffindor
but was instance of:
com.baeldung.concreteproxy.HogwartsHouse$HibernateProxy$8pxrMoK7
錯誤訊息表明 Hibernate 創建了一個類型為HogwartsHouse代理,而不是實際的Gryffindor子類別。這是因為 Hibernate 在延遲載入關聯關係時,只知道關係的宣告類型,而不知道資料庫中儲存的實際具體類型。
因此, instanceof檢查失敗,我們無法安全地轉換代理程式以存取子類別特定的屬性。
解決此問題的一種方法是使用 Hibernate 的 ` unproxy()方法,該方法會從代理程式中傳回底層實體物件。然而,這種方法會強制初始化代理,完全違背了延遲載入的初衷。
4. 使用@ConcreteProxy
為了解決我們討論的問題, Hibernate 引入了@ConcreteProxy註解,該註解在創建代理之前確定實體的實際具體類別。
讓我們將此註解應用到我們的抽象基底類別:
@Entity
@ConcreteProxy
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
abstract class HogwartsHouse {
// ...
}
透過這項簡單的改進, Hibernate 現在會在獲取延遲關聯的資料時檢查資料庫中的鑑別器列,以識別正確的子類別。需要注意的是,與先前的方法相比,這會導致查詢速度略微降低。
因此,它會產生一個代理,該代理是特定子類別的實例,從而允許我們安全地對其進行轉換並存取子類別特定的屬性:
assertThat(((Gryffindor) hogwartsHouse).getHasSummonedSword())
.isTrue();
現在,我們可以將惰性關聯轉換為具體的子類,並毫無問題地存取其特定屬性。
5. 結論
在本文中,我們探討了 Hibernate 的@ConcreteProxy註解的優點。
我們建立了一個簡單的繼承層次結構,並研究了延遲載入如何建立類型為聲明的關聯類型而不是實際子類別的代理,這阻止了我們執行類型檢查和轉換操作。
然後,我們透過在基類上新增@ConcreteProxy註解解決了這個問題。
與往常一樣,本文中使用的所有程式碼範例都可以在 GitHub 上找到。