概覽
Jimmer是一個Java/Kotlin雙語框架
-
包含一個革命性的ORM
-
以此ORM為基礎打造了一套綜合性方案解決方案,包括
-
DTO語言
-
更全面更強大的緩存機制,以及高度自動化的緩存一致性
-
更強大客戶端文檔和代碼生成能力,包括Jimmer獨創的遠程異常
-
快速創建GraphQL服務
-
跨越微服務的遠程實體關聯
-
ORM部分
當前技術生態下,訪問關系型數據庫技術體系存在很大缺陷,請看下圖。
1. 以JPA為代表的靜態語言ORM
優點
便捷,代碼安全(本身基于強類型語言,大部分代碼是安全的。如果結合QueryDSL使用,則可以保證所有代碼都是安全的)
缺點
缺乏靈活性
即使JPA從2.1開始支持EntityGraph,控制被查詢數據格式的靈活性仍然非常有限。該方案粒度仍然太粗,控制能力遠沒GraphQL這類技術的細膩。
保存對象時,細節行為受普通屬性的insertable、updateable和關聯屬性的cascade配置的控制,這類配置在實體類型中被硬編碼固化,被保存的數據結構的格式是固定的,沒有靈活性。
如果要發揮ORM的優勢,就必須查詢對象的大部分非關聯屬性 (少數@Basic(fetch = FetchType.Lazy)屬性除外,它們多為lob設計);如果只想查詢一部分屬性,就必須放棄對象查詢,轉而使用這些屬性的多列查詢,喪失ORM本該有的便捷性和核心價值。
3. 以為ActiveRecord (Ruby) 為代表的動態語言ORM
優點
基于動態語言的ORM,只需將動態語言對象結構的靈活性和ORM的實現結合起來,就能兼顧便捷和靈活。
缺點
動態語言雖然既便捷又靈活,但是代碼缺乏可維護性且不利于多人協同開發是眾所周知的缺點。
現代軟件往往是復雜的,需要團隊協作來完成,是否利于團隊成員之間協同,遠比個人對編程的認知重要。
這里,不想過多地討論動靜之爭,但是有一點需要指出:既然選擇了靜態語言Java/Kotlin,就應該以靜態語言的方式使用它, 而不能使用以jFinal為代表的將靜態語言當成動態語言用的方案,更不能在應用中頻繁地使用java.util.Map來代替數據對象。 這類做法違背了選擇Java/Kotlin的初衷,如果一定要怎么做,為什么不直接選動態語言呢?
4. 以MyBatis為代表的輕量級SQL Builder/Mapper
優點
直接編寫SQL,隨意且靈活;本身是強類型框架,具有代碼安全性 (MyBatis生態也有強類型SQL DSL擴展,可以解決原生SQL字符串導致的代碼不安全問題)
缺點
便捷性的嚴重缺失,重復勞動量極大。
MyBatis沒有統一實體的概念,而是面對具體業務場景DTO,實現ResultSet和這些DTO的映射。由于業務場景多,各DTO類型之間相似卻不同,冗余度很高,導致重復勞動量極高。
除了以孤單對象為載體的CRUD外,對多個對象彼此關聯而成的復雜數據結構的支持較弱,缺乏必要抽象,導致太多繁重的低級任務被推卸給開發人員 (不少開發人員長期被這類繁重的任務所累,但自己一直沒察覺)。
原生SQL真的是最好方案嗎?
這個派別最引以為豪的觀點是:“直接書寫SQL會帶來更直接的控制力,這種直接控制力優于任何ORM”。在這個領域長期的技術停滯中,不少開發人員對此深信不疑。
根本原因
上文中,我們闡述了關系型數據庫領域的三種常見方案,但無論如何選擇,我們都無法兼顧便捷性、靈活性和代碼安全性。為什么會導致這樣呢?
就JVM生態而言,POJO是導致這個問題的根本原因。
POJO*(也可以叫結構體)*缺乏必要的靈活性和表達力,卻幾乎被所有的JVM框架作為數據模型和核心,嚴重限制了JVM生態的技術創新。
因此,在Jimmer中,ORM實體對象并非POJO。而是另外一種獨特的萬能數據對象*(后文會介紹)*,這種獨特的實體對象撐起了Jimmer所有上層重大的變革,是整個框架的基石。
事實上,Jimmer實體對象不僅可以應用在ORM領域,它幾乎可以用在任何以結構化數據維護為目的的場景,并提升各種技術棧的表達力。
目前,Jimmer實體僅在關系型數據庫訪問領域發揮出作用,只是因為精力不夠所致。
完整的功能
在本文開頭我們提到了,革命性的ORM只是Jimmer的一部分,Jimmer實際的能力范圍早已超越了一個ORM。
現在,我們給出Jimmer的功能示意圖,并逐個講解
Business Model
在信息類系統中,存在兩種對象。
實體:實體對象是全局統一的,對象之間的存在豐富彼此關聯。
實體對象往往和數據庫非常接近,具備極高的穩定性。
DTO:針對特定業務的輸入/輸出對象,通常是從全局實體關系網上撕下來的一個局部碎片,該碎片的大小和形狀非常靈活。
DTO類型數量龐大,每一個業務接口對DTO對象的格式都有獨特的需求,彼此可能相似但又不同,具備明顯的。而且易受到需求變化的影響,不穩定。
Entity類型是全局統一數據存儲模型,不易被需求變更影響,相對穩定,被視為高價值類型。
DTO類型作為每個業務輸入/輸出,相對隨意,容易因需求變動而不穩定,被視為低價值類型。
Jimmer主張開發人員把精力集中在高價值的實體模式的設計上;對于低價值的DTO類型,有的時候并不需要,有的時候需要。
即使需要,也可以用極其廉價的方式自動生成。因此,基于Jimmer構建的項目具備優秀的抗需求變動的能力。
Jimmer Entity
Jimmer實體定義和JPA實體很接近。
之前討論過,Jimmer實體并非POJO,所以,被聲明為interface,而非class。
那么,誰負責實現此接口呢?是上圖中的Jimmer Precompiler (對于Java而言,就是APT; 對于Kotlin而言,就是KSP)
Jimmer實體支持兩個重要特征,動態性和不可變性
動態性
Jimmer對象在靜態語言和動態語言之間尋求最佳平衡,把二者的優點結合起來:
- 靜態語言數據對象具有高性能、拼寫安全、類型安全、甚至空安全*(如果使用Kotlin的話)*的優點,Jimmer實體吸收了這些優點。
- 動態語言數據對象具有高度的靈活性,Jimmer實體吸收了這個優點,每個屬性都可以缺失*(但是不能如同動態語言一樣增加屬性,因為這必然會破壞靜態語言的特性,Jimmer也不需要此能力)*
對Jimmer而言,對象缺少某個屬性 (其值未知) 和 對象的某個屬性為null (其值已知) 是完全不同的兩回事。
這種平衡設計,可以在享受靜態語言好處的同時,為數據結構賦予。
這種絕對的靈活性,既可用于表達查詢業務的輸出格式,也可用于表達保存業務的輸入格式。
這導致Jimmer擁有了嶄新的定位:一個為任意形狀數據結構設計的ORM。其所有功能都是為了操作任意形狀的數據結構,而非一個個簡單的實體對象。
不可變性
Jimmer對象是不可變對象。不可變對象的好處是多方面的
Jimmer選擇不可變對象是為了讓數據結構絕不包含循環引用。
這可以保證由Jimmer實體及彼此關聯組合而成的數據結構一定能夠被直接Jackson序列化,并不需要使用詭異的序列化技巧為JSON植入任何特殊的額外信息,任何編程語言都可以輕松理解。
然而,不可變對象也存在缺點。比如,現有一個很深的數據結構,那么基于它按照一些修改的愿望創建出新的數據結構會很困難,難度隨著深度的變大急劇增加。
ORM和很深的數據結構打交道,而Java的record和Kotlin的data class不適合處理很深數據結構。
既對Java和Kotlin進行雙語支持,又善于基于現有深層次數據結構按照一些修改的愿望創建出新的不可變數據結構的方案,目前的JVM生態沒有。
幸運的是,JavaScript/TypeScript領域存在一個足夠強大的方案: immer,可以完美解決這個問題。該方案工作方式如下
基于現有不可變數據結構開啟一個臨時作用域。
在這個作用域內,開發人員可得到一個draft數據結構,該數據結構的形狀和初始值和原數據結構完全一致,且可以被隨意修改,包括修改任意深的子對象。
作用域結束后,draft數據結構會利用收集到的修改行為創建另外一個新的數據結構。其中,未被修改的局部會被優化處理,復用以前的舊對象。
Immer完美結合了不可變對象和可變對象的優點,代碼簡單、功能強大、性能卓越。因此,Jimmer選擇為JVM生態移植了immer,項目名稱也是對其致敬。
Generated DTO Type
前文談到,Jimmer實體在靜態語言數據對象和動態語言數據對象之間尋找最佳平衡,其中動態性帶來了極大的靈活性,并以此決定了整個框架的定位。
Jimmer對象允許某些屬性缺失,對象缺少某個屬性 (其值未知) 和 對象的某個屬性為null (其值已知) 是完全不同的兩回事。
-
對于Jackson序列化而言,缺失的屬性會被自動忽略,就如同我們之前展示的那樣。
如果服務端自己并不使用查詢得到的實體對象,而是直接寫入到Http Response中。對于這種情況,無需DTO,直接使用實體對象很方便。
-
如果直接用Java/Kotlin代碼訪問不存在的屬性,會導致異常。
這并非由Jimmer制造的新問題,而是一個在靜態語言ORM生態中早已存在和被接受的問題。然而,不可否認這的確對靜態語言的安全性形成了破壞。
如果要追求100%的靜態語言安全性,使用DTO對象是唯一的方法。然而,目前JVM生態的DTO映射技術存在很大缺陷。
- 要么顯式地映射屬性*(例如純手工映射和轉化)*,這種做法工作量巨大,枯燥且容易出錯。
- 要么隱式地映射屬性*(例如采用BeanUtils技術)*,這種做法會引入新的不安全問題,即,無法在編譯發現的問題。
即使你使用強大的mapstruct,你所能做的也只是在這兩個極端之間作出選擇而已。
因此,Jimmer提供了DTO語言,用戶使用該語言編寫非常簡單的代碼,編譯項目即可自動生成各種豐富的DTO類型定義。
DTO語言的設計目的,在于
讓生成DTO類型的過程足夠簡單,從而讓DTO類型足夠廉價。
100%符合靜態語言安全性,在編譯時發現所有問題并報錯。
理論概念先到這里
簡單使用
我們做一個簡單的查詢demo,創建Springboot項目
引入依賴
<dependency><groupId>org.babyfish.jimmer</groupId><artifactId>jimmer-spring-boot-starter</artifactId><version>0.8.51</version></dependency>
編寫Model
用戶
@Entity
@Table(name = "User")
public interface User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)int id();String name();@NullableInteger age();@OneToMany(mappedBy = "user")List<UserDetail> details();
}
用戶詳情,一對多
@Entity
@Table(name = "user_detail")
public interface UserDetail {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)int id();@Key // 自己的核心數據自然就是第二個業務鍵String detail();@Key // 父級自然是一個業務鍵@OnDissociate(DissociateAction.DELETE) // 如果脫鉤了,就把自身刪除@ManyToOne@JoinColumn(name = "user_id",foreignKeyType = ForeignKeyType.FAKE)@NullableUser user();@IdView("user")Integer userId();}
配置數據庫鏈接
applicantion.yml
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://myhost:3306/my_jimmerusername: rootpassword: rootjimmer:dialect: org.babyfish.jimmer.sql.dialect.MySqlDialectshow-sql: onpretty-sql: truedatabase-validation:schema: my_jimmer
構建
Maven build
查詢
@RestController
@RequestMapping("/test")
public class TestController {@Autowiredprivate JSqlClient sqlClient;@RequestMapping("/user")public List<User> find(@RequestBody UserSpecification specification){UserTable userTable = UserTable.$;return sqlClient.createQuery(userTable).select(userTable).execute();}
}
超級查詢
使用specification,可以提供靈活的復雜查詢
定義dto
export com.example.myjimmer.entity.User-> package com.example.myjimmer.dto/*UserView {#allScalars(User)details {#allScalars(UserDetail)}
}*/specification UserSpecification {eq(name) as namelike(name) as likename
}
構建
Maven build
使用specification查詢
@RestController
@RequestMapping("/test")
public class TestController {@Autowiredprivate JSqlClient sqlClient;@RequestMapping("/user")public List<User> find(@RequestBody UserSpecification specification){UserTable userTable = UserTable.$;return sqlClient.createQuery(userTable).where(specification).select(userTable).execute();}
}