Jimmer: 一個面向Java和Kotlin的革命性ORM

語言: CN / TW / HK

大家好,我開發了一個開源ORM,整合我多年開發經驗於一體,解決系統開發中一直困擾著開發人員的深層次問題。

相關連結: - 文件: https://babyfish-ct.github.io/jimmer/zh/ - 影片 - 全面介紹: https://www.bilibili.com/video/BV1kd4y1A7K3 - 多表連線專題: https://www.bilibili.com/video/BV19t4y177PX - 效能: https://babyfish-ct.github.io/jimmer/zh/docs/benchmark - 專案: https://github.com/babyfish-ct/jimmer

1. 本文的討論前提

OLTP型別專案很大一部分操作是都針對資料庫原始資料,這時軟體系統中的物件結構和資料庫的中資料結構大體一致,是本文討論的場景。

而因業務計算而引入的計算指標相關的資料型別,和資料庫的原始結構並不相同,並非本文的討論範場景。

2. 現有技術流派的缺陷

現在,使用者訪問關係型資料庫的框架很多,總體上分為兩個派別

  • 傳統ORM派,以JPA, Exposed, Ktorm為代表。
  • DTO Mapper派,以MyBatis, JOOQ為代表。

以上兩派都有各自的優缺點,Jimmer完美融合兩派之長,走出了截然不同的第三條路。因此並不能比把Jimmer上述兩個派中的任何方案做簡單對比。

2.1. 以JPA為代表的傳統ORM派

在傳統ORM中,開發人員建立實體類,和資料庫表結構直接對應。從對映的角度講,非常簡單。

傳統ORM注重維護物件之間的關係,以JPA為例 java List<Book> books = entityManager .createQuery( "select book from Book book " + "left join fetch book.store " + "left join fetch book.authors" ).getResultList(); 這個例子中的join fetch是JPA的一個特色功能,可以利用SQL JOIN使返回的Book物件不再是孤單物件,而是附帶了關聯屬性storeauthors

通過可選的join fetch(或其他技巧,不同的ORM框架手段不盡相同),傳統ORM既可以返回孤單的資料物件,也可以返回帶關聯的複雜物件,這其實一種對返回資料結構的裁剪能力

這種裁切能力是以物件為粒度的,但是,返回的資料結構中每個物件都是完整的,也就是說缺少普通屬性級別的裁剪能力。

無法做到普通屬性級的裁剪,當物件屬性很多導致查詢所有列效率很低,或需要對低許可權使用者進行重要屬性脫敏時,會成為問題。很不幸,現實中的專案就是這樣的。

雖然Hibernate從3.x開始,普通(非關聯)屬性也可以被設定為lazy。然而,這個特性是為lob屬性而設計,並非為了實現普通屬性級的裁剪而設計,靈活度非常有限。不予討論

如果想要讓傳統ORM精確地實施屬性級的裁切,會使用這樣的程式碼

java List<BookDTO> bookDTOs = entityManager .createQuery( "select new BookDTO(book.id, book.name) " + "from Book book" ).getResultList();

在這個例子中,我們只想查詢id和name屬性,為此,不得不構建一個全新的型別BookDTO用作只有兩個屬性的殘缺物件的載體。在我們獲得普通屬性級裁剪能力的同時,因BookDTO是一個普通物件而非實體物件,喪失了物件級的裁剪能力。

也正是因為這種用法喪失了ORM的核心能力,在傳統ORM中實踐中屬於非主流用法,很少使用。

傳統ORM的另外一個問題是,返回的資料複雜度很高,難以直接使用。

對於未載入的lazy屬性,開發人員很容易在Json序列化中忽略他們,這不是問題。

真正麻煩的是物件之間存在雙向關聯,而前端和微服務客戶端更期望看到只有單向關聯的物件樹。

比如TreeNode實體同時具備向上的parent屬性和向下的childNodes屬性。

  • 有些業務可以需要查詢某個節點和其所有下級,返回aggregateRoot->childNodes->childNodes->...這樣的資料結構;
  • 而有些業務查詢某個節點和其所有上級,返回aggregateRoot->parent->parent->...這樣的資料結構。

所以,你無法簡單地規定parentchildNodes中,哪個是對外暴露的,哪個是對外隱藏的。你無法簡單地通過@JsonIgnore註解來解決這個問題,這是一個非常棘手的問題。

2.2. 以MyBatis為代表的DTO Mapper派

通過上文描述,我們知道,傳統ORM有兩個缺點。

  1. 便於發揮傳統ORM能力的主流方法,雖然有靈活的物件級裁剪能力,但同時也喪失了普通屬性級的裁剪能力。
  2. ORM返回的實體物件過於複雜,難以直接返回,無法和HTTP互動。

這兩個問題,都是資料物件表達能力弱導致的,其實可以通過定義特定業務所需的DTO類解決。

既然人們註定需要定義特定業務相關的DTO型別,為什麼還要編寫程式碼把ORM實體轉換為DTO呢?為什麼不直接實現從SQL結果到DTO的對映呢?

因此DTO Mapper派被開發人員認同,這個流派提出了截然不同的解決方案。開發人員不再定義和資料庫結構直接對應的實體類,而是直接為每個特定業務定義DTO型別,比如:

  • 為表達孤單的Book物件,新建類Book
  • 為表達帶關聯屬性store的Book物件,新建類BookWithStore
  • 為表達帶關聯屬性authors的Book物件,新建類BookWithAuthors
  • 為表達帶關聯屬性storeauthors的Book物件,新建類BookWithStoreAndAuthors

各業務API返回自己需要的DTO物件,每個API都是用特定的SqlResultMapper,把特定的查詢結果對映為特定的DTO。

然而,這個做法同樣問題嚴重

  1. 上面的例子中我們只展示了物件級的裁剪,並未展示屬性級的裁剪,而且物件樹的深度也很淺。如果不是這樣,DTO型別的數量會激增,甚至可以用爆炸來形容。這時,DTO類會多得連取名字都難。開發人員甚至需要結合行業相關的命名約定來避免很長的類名。

  2. DTO太多了,不同的DTO雖然不同,但相同部分也不少,具有高度的冗餘。系統喪失緊湊性,開發成本和測試成本激增。

  3. 一旦引入新的需求,資料庫的結構發生變化,多處冗餘的業務都需要修改。

為避免問題2和3,可對SQL對映片段或業務程式碼儘可能重用,但這會破壞系統的簡單性,程式碼變得難以理解,這是過度使用低價值複用的必然代價。

3. Jimmer的優勢

通過上面的論述,我們知道

  • 傳統ORM派:優點是直接和資料儲存結構對應,提供統一視角;但缺點是隻對返回資料格式進行物件級裁剪,沒有普通屬性級的裁剪,而且返回的資料結構難以直接利用。
  • DTO Mapper派:優點是查詢的到的DTO物件簡單,返回的聚合根所代表的資料結構只包含單向關聯;但缺點是DTO型別數量膨脹嚴重,雖不同但相似,開發成本和測試成本都很高。

Jimmer完美融合兩派之長,走出了截然不同的第三條路。因此並不能比把Jimmer上述兩個派中的任何方案做簡單對比。

3.1. 無DTO模式:動態實體

在Jimmer中

  • 實體物件是動態的,任何物件屬性,無論是普通屬性還是關聯屬性,都可以缺失。 > 對Jimmer的實體物件而言,不指定某個屬性和把某個屬性指定為null,完全是兩碼事。

  • 在Java或Kotlin程式碼中直接讀取物件的缺失屬性會導致異常;然而,在JSON序列化時,缺失屬性會被自動忽略,不會異常。

  • 雖然宣告實體型別時,不同型別之間可以定義雙向關聯;然而,某個具體業務需要例項化物件時,實體物件之間只能建立單向關聯,保證任何資料結構都能用一個簡單的聚合根物件來表達。

動態實體本身不是DTO,但它具備DTO物件的所有特質,無DTO勝似DTO,任何實體物件樹都可以直接參與HTTP互動。 動態實體是整個ORM的架構基礎。

3.2. 查詢任意複雜的資料結構

完美支援物件級別和屬性級別的物件形狀裁切能力,使用者可以從完整的關係模型中圈定出一個區域性資料結構,即一個任意複雜的樹結構,以返回動態實體樹的方式,查詢整個資料結構。

讓RDBMS具備類似於GraphQL功能。即使你的專案和GraphQL技術毫無關係,你的RDMBS也擁有它的一切優勢。 Jimmer比GraphQL做得更好,它甚至支援自關聯屬性的遞迴查詢。

3.3. 修改任意複雜的資料結構

使用者可以向Jimmer傳遞任意複雜的動態物件樹,將整棵樹作為一個整體用一句話儲存。

可以理解成GraphQL的逆功能。

3.3. 強大的快取機制

  • 對使用者的快取技術選型不做任何限制,使用者可以選用任何快取技術。
  • 內部支援物件快取和關聯快取,在複雜資料結構查詢中,二者在幕後按需有機結合。最終給使用者呈現出的效果,就是任意複雜資料結構的快取,而非簡單物件的快取。
  • 自動保證快取的資料一致性,只要在接受到資料庫binlog推送後簡單呼叫Jimmer的API即可。
  • 快取機制對開發人員100%透明,是否採用快取,對業務程式碼沒有任何影響。

雖然RDBMS具備無以倫比的表達能力,但它有一個明顯的缺點:按關係導航追蹤其它資料,效能不理想。 關聯快取可以在很大程度上緩解這個問題,讓RDBMS如虎添翼。

3.4 比原生SQL更實用的強型別SQL DSL

  • 在編譯時發現拼寫錯誤和型別匹配錯誤。
  • 強型別SQL DSL可以原生SQL表示式隨意混合,在統一和抽象不同的資料庫的同時,允許發揮特定資料庫產品獨有的能力。
  • 以放棄實際專案中幾乎不可能被用到的個別SQL寫法為代價,提供比原生SQL更便捷更實用的多表連線操作。