Hibernate 中的 @Subselect 註解
一、簡介
在本教程中,我們將回顧 Hibernate 中的@Subselet
註解、如何使用它以及它的好處。我們也將看到 Hibernate 對註解為@Subselect
的實體的約束及其後果。
2. @Subselect
註解概述
@Subselect
允許我們將不可變實體對應到 SQL 查詢。因此,讓我們稍微展開一下這個解釋,從實體到 SQL 查詢映射的含義開始。
2.1.對應到 SQL 查詢
通常,當我們在 Hibernate 中建立實體時,我們會用@Entity.
該註釋表明這是一個實體,並且應該由持久化上下文管理。我們也可以選擇提供@Table
註解來指示 Hibernate 應該將該實體準確地對應到哪個表。因此,預設情況下,每當我們在 Hibernate 中建立實體時,它都會假設實體直接對應到特定的表。在大多數情況下,這正是我們想要的,但並非總是如此。
有時,我們的實體不會直接對應到資料庫中的特定表,而是 SQL 查詢執行的結果。例如,我們可能有一個Client
實體,其中該實體的每個實例都是 SQL 查詢(或 SQL 視圖)執行的ResultSet
中的一行:
SELECT
u.id as id,
u.firstname as name,
u.lastname as lastname,
r.name as role
FROM users AS u
INNER JOIN roles AS r
ON r.id = u.role_id
WHERE u.type = 'CLIENT'
重要的是資料庫中可能根本沒有專用的clients
表。這就是將實體對應到 SQL 查詢的意義——我們從子選擇 SQL 查詢中取得實體,而不是從表中取得。該查詢可以從任何表中進行選擇並執行其中的任何邏輯——Hibernate 並不關心。
2.2.不變性
因此,我們可能有一個未映射到特定表的實體。直接後果是,不清楚如何執行任何 INSERT/UPDATE 語句。根本不存在我們可以插入記錄的clients
(如上例所示)表。
事實上,Hibernate 並不知道我們執行哪種 SQL 來檢索資料。因此,Hibernate 無法對此類實體執行任何寫入操作 - 它變為唯讀。這裡棘手的事情是,我們仍然可以要求 Hibernate 插入這個實體,但它會失敗,因為不可能(至少根據 ANSI SQL)向子選擇發出 INSERT 。
3. 使用範例
現在,一旦我們了解了@Subselect
註釋的作用,就讓我們嘗試動手嘗試使用它。在這裡,我們有一個簡單的RuntimeConfiguration
:
@Data
@Entity
@Immutable
// language=sql
@Subselect(value = """
SELECT
ss.id,
ss.key,
ss.value,
ss.created_at
FROM system_settings AS ss
INNER JOIN (
SELECT
ss2.key as k2,
MAX(ss2.created_at) as ca2
FROM system_settings ss2
GROUP BY ss2.key
) AS t ON t.k2 = ss.key AND t.ca2 = ss.created_at
WHERE ss.type = 'SYSTEM' AND ss.active IS TRUE
""")
public class RuntimeConfiguration {
@Id
private Long id;
@Column(name = "key")
private String key;
@Column(name = "value")
private String value;
@Column(name = "created_at")
private Instant createdAt;
}
該實體代表我們應用程式的運行時參數。但是,為了傳回屬於我們的應用程式的最新參數集,我們需要對system_settings
表執行特定的 SQL 查詢。正如我們所看到的, @Subselect
註解的主體包含該 SQL 語句。現在,因為每個RuntimeConfiguration
條目本質上都是一個鍵值對,所以我們可能想要實作一個簡單的查詢 - 取得具有特定鍵的最新活動RuntimeConfiguration
記錄。
另請注意,我們用@Immutable
標記了我們的實體。因此,Hibernate 將停用我們實體的任何髒檢查跟踪,以避免意外的 UPDATE 語句。
因此,如果我們想使用特定鍵來取得RuntimeConfiguration
,我們可以這樣做:
@Test
void givenEntityMarkedWithSubselect_whenSelectingRuntimeConfigByKey_thenSelectedSuccessfully() {
String key = "config.enabled";
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<RuntimeConfiguration> query = criteriaBuilder.createQuery(RuntimeConfiguration.class);
var root = query.from(RuntimeConfiguration.class);
RuntimeConfiguration configurationParameter = entityManager
.createQuery(query.select(root).where(criteriaBuilder.equal(root.get("key"), key))).getSingleResult();
Assertions.assertThat(configurationParameter.getValue()).isEqualTo("true");
}
在這裡,我們使用 Hibernate Criteria API 按鍵查詢RuntimeConfiguration
。現在,讓我們檢查 Hibernate 實際產生什麼查詢來滿足我們的請求:
select
rc1_0.id,
rc1_0.created_at,
rc1_0.key,
rc1_0.value
from
( SELECT
ss.id,
ss.key,
ss.value,
ss.created_at
FROM
system_settings AS ss
INNER JOIN
( SELECT
ss2.key as k2, MAX(ss2.created_at) as ca2
FROM
system_settings ss2
GROUP BY
ss2.key ) AS t
ON t.k2 = ss.key
AND t.ca2 = ss.created_at
WHERE
ss.type = 'SYSTEM'
AND ss.active IS TRUE ) rc1_0
where
rc1_0.key=?
正如我們所看到的,Hibernate 只是從@Subselect
中提供的 SQL 語句中選擇記錄。現在,我們提供的每個篩選器都將應用於產生的子選擇記錄集。
4.替代方案
經驗豐富的 Hibernate 開發人員可能會注意到已經有一些方法可以實現類似的結果。其中之一是使用投影映射到 DTO,另一個是視圖映射。這兩者各有優缺點。我們來一一討論。
4.1.投影映射
那麼,我們來談談 DTO 預測。它允許將 SQL 查詢映射到非實體的 DTO 投影中。人們也認為使用 DTO 投影比使用實體更快。 DTO 投影也是不可變的,這意味著 Hibernate 不管理此類實體,也不對其應用任何髒檢查。
儘管上述所有內容都是正確的,但 DTO 預測本身也有其限制。最重要的問題之一是 DTO 預測不支援關聯。這是非常明顯的,因為我們正在處理 DTO 投影,它不是託管實體。這使得 Hibernate 中的 DTO 投影速度更快,但這也意味著持久性上下文不管理這些 DTO。因此,DTO 上不能有任何OneToX
或ManyToX
欄位。
但是,如果我們將實體對應到 SQL 語句,我們仍然會對應實體。它可能管理著協會。因此,此約束不適用於實體到查詢的對應。
另一個重要的、本質的和概念上的區別是@Subselect
允許我們將實體表示為 SQL 查詢。 Hibernate 將執行註解名稱所暗示的操作。它只會使用提供的 SQL 查詢從中進行選擇(因此我們的查詢成為子選擇),然後套用其他篩選器。因此,我們假設要取得實體 X,我們需要執行一些篩選、分組等。然後,如果我們使用 DTO 投影,我們總是必須在每個 JPQL 或本機查詢中編寫篩選器、分組等。當使用@Subselect
時,我們可以指定一次該查詢並從中進行選擇。
4.2.視圖映射
儘管這一點並不廣為人知,但 Hibernate 可以將我們的實體直接對應到 SQL 視圖。這與實體到 SQL 查詢映射非常相似。視圖本身在資料庫中幾乎總是只讀的。不同的 RDBMS 中存在一些例外情況,例如 PostgreSQL 中的簡單視圖,但這完全是特定於供應商的。這意味著我們的實體也是不可變的,我們只會從底層視圖讀取,不會更新/插入任何資料。
一般來說, @Subselect
和實體到視圖映射之間的差異非常小。前者使用我們在註釋中提供的確切 SQL 語句,而後者使用現有視圖。兩種方法都支援託管關聯,因此選擇其中一種完全取決於我們的要求。
5. 結論
在本文中,我們討論瞭如何使用@Subselect
選擇查詢中而不是從特定表中選擇實體。如果我們不想重複 SQL 語句的相同部分來取得實體,這非常方便。但這意味著我們使用 @Subselect 的實體實際上是不可變的,我們不應該嘗試從應用程式程式碼中保留它們。 @Subselect 有一些替代方案,例如,在 Hibernate 中查看實體映射,甚至使用 DTO 投影。它們都有各自的優點和缺點,因此為了做出選擇,我們需要一如既往地遵守要求和常識。
與往常一樣,原始碼可以在 GitHub 上取得。