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

語言: CN / TW / HK

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

相關鏈接: - 文檔: http://babyfish-ct.github.io/jimmer/zh/ - 視頻 - 全面介紹: http://www.bilibili.com/video/BV1kd4y1A7K3 - 多表連接專題: http://www.bilibili.com/video/BV19t4y177PX - 性能: http://babyfish-ct.github.io/jimmer/zh/docs/benchmark - 項目: http://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更便捷更實用的多表連接操作。