簡介
幾乎任何系統都以某種方式與外部數據存儲一起運行。大多數情況下,外部數據存儲是一個關系數據庫,並且在實現時通常將數據提取任務委托給某些 ORM。 盡管 ORM 包含很多 routine 代碼,但是另一方面也提供了一些新的抽象。
Martin Fowler 寫過一篇關於 ORM 的有趣文章,其中一個主要思想是 “ ORM 幫助我們處理大多數企業應用程序中的一個非常現實的問題... ORM 不是漂亮的工具,但它解決的問題也不是可愛的。我認為他們應該得到更多的尊重和更多的理解”。
在 CUBA 框架中,我們大量地使用了 ORM,因為我們在世界各地都有各種各樣的項目,所以我們非常了解它的局限性。關於 ORM 有很多方面可以討論,但這里我們只關注其中一個:Lazy 和 Eager 方式加載數據。我們將會討論數據獲取的不同方法(主要在 JPA API 和 Spring 中),以及我們如何在 CUBA 中獲取數據,還有我們在 CUBA 中改進 ORM 層所做的研發工作。我們還會了解哪些基本要素需要在使用ORM時考慮,以避免可怕的性能問題。
獲取數據:Lazy 方式還是 Eager 方式?
如果數據模型只包含一個實體,那使用 ORM 不會有問題。我們來看看這個例子。有一個User實體,包含 ID 和 Name 屬性:

要獲取這個實體,我們只需要簡單的使用 EntityManager:

但是如果有實體之間的一對多關系的話,事情就變得有點意思了:

如果我們想從數據庫中獲取用戶記錄,就會出現一個問題:“我們也應該同時獲取一個 address 嗎?”。而“正確”的答案將是:“看情況”。在某些情況下,我們可能需要獲取地址信息。通常,ORM提供兩種獲取數據的選項:Lazy 和 Eager。大多數ORM默認設置使用Lazy模式。因此,當我們編寫以下代碼時:

我們會遇到所謂的 “LazyInitException”,這會讓ORM新手非常困惑。所以這里我們需要解釋“Attach”和“Detach”對象的概念,以及數據庫會話和事務。
那么,一個實體實例應該關聯到一個數據庫會話,這樣我們才能夠獲取詳細信息屬性(比如 User.addresses)。但是在這種情況下,我們遇到了另一個問題,事務會變得越來越長,因此,會增加了數據庫死鎖的風險。可是,如果將我們的代碼拆分為一系列短事務的話,又會由於非常短的單獨查詢語句數量的急劇增加,導致數據庫“數百萬只蚊子死亡” - 太多小事務。
如上所述,我們可能需要獲取地址屬性或者不需要,因此只是在某些情況下或者更多的條件判斷之后使用 Address 集合。 嗯....看起來變得越來越復雜了。
好的,另一種獲取類型會有幫助嗎?

並沒有想象的那么好。雖然使用 Eager 方式能避免煩人的懶加載初始化異常,也不需要檢查實例是 Attach 還是 Detach。但是這里我們會遇到性能問題,因為我們並不是針對所有情況都需要用戶的地址信息,可是用 Eager 方式會始終獲取地址信息。那還有其他辦法嗎?
Spring JDBC
一些開發人員對ORM非常惱火,因此使用Spring JDBC並切換到“半自動”映射。在這種情況下,我們會為唯一用例創建唯一查詢語句,並返回包含僅對特定用例有效的屬性的對象。
這種方式給了我們很大的靈活性。比如,我們可以只獲取一個屬性:

或者整個對象:

您也可以使用 ResultSetExtractor 獲取 addresses,但需要涉及編寫一些額外的代碼,還需要知道如何編寫 SQL join 語句以避免 n+1 select 問題。
好吧,又變得復雜了。使用 Spring JDBC 您可以控制所有查詢並控制結果映射,但是您必須編寫更多代碼,學習 SQL 並了解數據庫查詢語句的執行方式。雖然我認為了解 SQL 基礎知識對於幾乎每個開發人員來說都是一項必要的技能,但有些人並不這么認為,而我也不打算與他們爭論。因為現在我們知道 x86 匯編程序對每個人來說都不是一項至關重要的技能。我們只是考慮如何能簡化開發。
JPA EntityGraph
我們回過頭想想,我們到底要實現什么樣的目標?好像就是確切地說明在不同的用例中我們需要獲取哪些實體屬性。JPA 2.1 引入了一個新的 API - Entity Graph。這個API背后的想法很簡單 - 您只需編寫幾個注解來描述應該獲取的內容。我們來看看這個例子:

對於這個實體,我們描述了兩個實體圖(entity graphs) - user-only-entity-graph 不加載addresses屬性(標記為lazy),而第二個實體圖需要 ORM 獲取 addresses。但是如果我們將屬性標記為 Eager,則將忽略實體圖設置並獲取屬性。
因此,從JPA 2.1開始,您可以通過以下方式選擇實體:

這種方法極大地簡化了開發人員的工作,無需“觸摸” Lazy 屬性並創建長事務。最棒的是,實體圖可以在生成SQL的級別使用,因此不會從數據庫中將額外數據提取到Java應用程序。但是這里仍然存在問題:在外部調用時不方便知道提取了哪些屬性。有一個API可以檢查: 使用 PersistenceUnit 類檢查屬性是否加載:

但是這樣做很無聊。我們有辦法簡化並且不顯示無法獲取的屬性嗎?
Spring Projections
Spring Framework 提供了一個名為 Projections 的出色工具(它與Hibernate的 Projections 不同)。如果我們只想獲取實體的某些屬性,我們可以指定一個接口,Spring 將從數據庫中選擇接口“實例”。我們來看看這個例子。如果我們定義以下接口:

然后定義 Spring JPA repository 以獲取我們的用戶實體:

在這種情況下,在調用 findByName 方法之后,我們將無法訪問未獲取的屬性!同樣的原則也適用於 detail entity 類。因此,您可以通過這種方式獲取 master 記錄和 detail 記錄。此外,在大多數情況下,Spring 能生成 “合適的” SQL 並僅提取 projection 中指定的屬性,也就是說 projections 能像實體圖一樣工作。
這是一個非常強大的概念,您可以使用 SpEL 表達式,使用類而不是接口,等等。如果您感興趣,可以在文檔中了解更多信息。
Projections 的唯一問題是它們在底層使用 map 實現,所以是只讀的。因此,您可以為 projection 定義 setter 方法,但無法使用 CRUD repositories 和 EntityManager 來保存更改。您可以將 projection 視為DTO,但是您必須編寫自己的 DTO-to-entity 轉換代碼。
CUBA 的實現
從CUBA框架開發開始,我們嘗試優化與處理數據庫的代碼。在框架中,我們使用 EclipseLink 來實現數據訪問層API。關於 EclipseLink 的好處 - 它從一開始就支持部分實體加載,這就是我們首先選擇它而不選 Hibernate 的原因。在 JPA 2.1 成為標准之前,使用這個 ORM 就能准確指定加載哪些屬性。因此,我們在 CUBA 框架中加入了內部的 “Entity Graph” 概念 - CUBA 視圖。視圖非常強大 - 您可以對視圖進行擴展、組合等等。創建 CUBA 視圖背后的第二個原因 - 我們希望使用短事務,並主要使用 detach 對象,否則,我們沒辦法讓豐富的web UI 以響應式並且快速的方式運行。
在CUBA中,視圖描述存儲在 XML 文件中,如下所示:

此視圖會告訴 CUBA DataManager 使用其 name 屬性獲取User實體,並在查詢級別獲取地址時使用 address-street-only-view 視圖。視圖定義之后,您可以在使用 DataManager 類獲取實體時使用視圖:

這就像魔法一樣。由於不加載未使用的屬性,能節省大量的網絡流量,但是跟 JPA Entity Graph 類似,有一個小問題:我們不知道加載了用戶實體的哪些屬性。在 CUBA 中,我們非常討厭 “IllegalStateException: Cannot get unfetched attribute [...] from detached object”。與 JPA 一樣,您可以檢查屬性是否已獲取,但是為每個正在加載的實體編寫這些檢查是一項無聊的工作,開發人員對此有些不滿。
CUBA 視圖接口初探
如果我們能夠充分利用兩種方式呢?我們決定實現使用 Spring 方法的所謂實體接口,不同的是,這些接口在應用程序啟動期間轉換為 CUBA 視圖,然后可以在 DataManager 中使用。這個想法非常簡單:您定義了一個指定實體圖的接口(或一組接口)。它看起來像 Spring Projections 並且像 Entity Graph 一樣工作:

請注意,如果 AddressStreetOnly 接口只在一種情況下使用,則可以聲明其為內部接口。
然后在 CUBA 應用程序啟動期間(事實上,主要是Spring Context 初始化的過程中),我們為 CUBA 視圖創建一個程序化表示,並將它們存儲在 Spring Context 的內部 repository bean中。
之后我們需要調整 DataManager,然后,除了CUBA 視圖的字符串名稱之外它還可以接受類名,加載時,我們只需傳遞上面定義的接口類:

跟 Hibernate 一樣,我們生成代理,為從數據庫中提取的每個實例實現實體視圖。當您嘗試獲取屬性的值時,代理會將調用轉發給具體的實體執行。
通過這個實現,我們試着能一石二鳥:
l 未在接口中聲明的數據不會加載到Java應用程序代碼,從而節省了服務器資源
l 開發人員只使用被提取的屬性,因此不再出現 “UnfetchedAttribute” 錯誤(在 Hibernate中也稱為 LazyInitException)。
與 Spring Projections 相比,實體視圖能包裝實體並實現了 CUBA 的 Entity 接口,因此可以將它們視為實體:您可以更新屬性並將更改保存到數據庫。
這里還有 “ 第三只鳥” - 你可以定義一個只包含 getter 的 “只讀”接口,能完全阻止實體在API級別進行修改。
此外,我們可以在 detach 的實體上實現一些操作,例如將此用戶的名稱轉換為小寫:

在這種情況下,可以從實體模型移除所有需要計算的屬性,因此不會在實體內部將數據獲取邏輯與特定於用例的業務邏輯混合使用。
另一個有趣的機會 - 您可以繼承接口。這使您可以使用不同的屬性集准備多個視圖,然后根據需要混合它們。例如,您可以擁有一個包含用戶名和電子郵件的界面,另一個界面包含用戶名和地址。如果你需要一個應該包含用戶名,電子郵件和地址的第三個視圖界面,你可以通過組合兩者來實現 - 這要歸功於Java中的多接口繼承。請注意,您可以將此第三個接口傳遞給使用第一個或第二個接口的方法,這里也能使用OOP原則。
我們還在視圖之間實現了實體轉換 - 每個實體視圖都有reload()方法,它接受另一個視圖類作為參數:

UserFullView 可能包含其他屬性,因此使用時,會從數據庫重新加載實體。實體重新加載是一個 Lazy 的過程,只有在嘗試獲取實體屬性值時才會執行。我們故意這樣做是因為在 CUBA 中我們有一個 web 模塊,可以呈現豐富的 UI,並且可能包含自定義的 REST 控制器。在此模塊中,跟 core 模塊(也叫中間件)使用相同的實體,但是 web 模塊可以部署在單獨的服務器上。因此,每個實體重新加載都會通過 core 模塊向數據庫發出附加請求。所以,通過引入實體重新懶加載,我們節省了一些網絡流量和數據庫查詢。
PoC 代碼可以從GitHub下載 - 隨意試試看。
結論
ORM 將在不久的將來在企業應用程序中大量使用。我們只需要提供一些將數據庫行轉換為 Java 對象的東西。當然,在復雜,高負載的應用程序中,我們也能繼續看到獨特的解決方案,但只要 RDBMS 存在,ORM 也會繼續存在。
在 CUBA 框架中,我們試圖簡化 ORM 的使用,使開發人員盡可能輕松。在接下來的版本中,我們將引入更多更改。我不確定這些是視圖接口還是其他東西,但我很確定一件事 - 在下一版本中我們將簡化 CUBA 中 ORM 的使用方法。

