项目介绍 ✦
1. 概述
Jimmer是一个Java/Kotlin双语框架
-
包含一个革命性的ORM
-
以此ORM为基础打造了一套综合性方案解决方案,包括
- DTO语言
- 更全面更强大的缓存机制,以及高度自动化的缓存一致性
- 更强大客户端文档和代码生成能力,包括Jimmer独创的远程异常
- 快速创建GraphQL服务
- 跨越微服务的远程实体关联
2. ORM部分
现有痛点
当前技术生态下,访问关系型数据库技术体系存在很大缺陷,请看下图。
-
以JPA为代表的静态语言ORM
-
以为ActiveRecord (Ruby) 为代表的动态语言ORM
-
以MyBatis为代表的轻量级SQL Builder/Mapper
根本原因
上文中,我们阐述了关系型数据库领域的三种常见方案,但无论如何选择,我们都无法兼顾便捷性、灵活性和代码安全性。为什么会导致这样呢?
就JVM生态而言,POJO是导致这个问题的根本原因。
POJO*(也可以叫结构体)*缺乏必要的灵活性和表达力,却几乎被所有的JVM框架作为数据模型和核心,严重限制了JVM生态的技术创新。
因此,在Jimmer中,ORM实体对象并非POJO。而是另外一种独特的万能数据对象*(后文会介绍)*,这种独特的实体对象撑起了Jimmer所有上层重大的变革,是整个框架的基石。
事实上,Jimmer实体对象不仅可以应用在ORM领域,它几乎可以用在任何以结构化数据维护为目的的场景,并提升各种技术栈的表达力。
目前,Jimmer实体仅在关系型数据库访问领域发挥出作用,只是因为精力不够所致。
3. 完整的功能
在本文开头我们提到了,革命性的ORM只是Jimmer的一部分,Jimmer实际的能力范围早已超越了一个ORM。
现在,我们给出Jimmer的功能示意图,并逐个讲解
3.1. Business Model
在信息类系统中,存在两种对象。
-
实体:实体对象是全局统一的,对象之间的存在丰富彼此关联。
实体对象往往和数据库非常接近,具备极高的稳定性。
-
DTO:针对特定业务的输入/输出对象,通常是从全局实体关系网上撕下来的一个局部碎片,该碎片的大小和形状非常灵活。
DTO类型数量庞大,每一个业务接口对DTO对象的格式都有独特的需求,彼此可能相似但又不同,具备明显的
。而且易受到需求变化的影响,不稳定。
Entity类型是全局统一数据存储模型,不易被需求变更影响,相对稳定,被视为高价值类型。
DTO类型作为每个业务输入/输出,相对随意,容易因需求变动而不稳定,被视为低价值类型。
Jimmer主张开发人员把精力集中在高价值的实体模式的设计上;对于低价值的DTO类型,有的时候并不需要,有的时候需要。 即使需要,也可以用极其廉价的方式自动生成。因此,基于Jimmer构建的项目具备优秀的抗需求变动的能力。
3.1.1. 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,项目名称也是对其致敬。
-
3.1.2. Generated DTO Type
前文谈到,Jimmer实体在静态语言数据对象和动态语言数据对象之间寻找最佳平衡,其中动态性带来了极大的灵活性,并以此决定了整个框架的定位。
Jimmer对象允许某些属性缺失,对象缺少某个属性 (其值未知) 和 对象的某个属性为null (其值已知) 是完全不同的两回事。
-
对于Jackson序列化而言,缺失的属性会被自动忽略,就如同我们之前展示的那样。
如果服务端自己并不使用查询得到的实体对象,而是直接写入到Http Response中。对于这种情况,无需DTO,直接使用实体对象很方便。
-
如果直接用Java/Kotlin代码访问不存在的属性,会导致异常。
如果服务端自己要使用查询得到的实体对象,这会带来风险,尽管Jimmer实体在其他方面依旧保留了静态语言的特色,比如拼写安全、类型安全、甚至空安全*(如果使用kotlin)*。
以JPA为例,从Hibernate3开始,lazy配置不再局限于关联属性,而是可以用于标量属性。后来演化为JPA之
@Basic
注解的fetch
参数, 请参考这这里。这和Jimmer对象任何属性都可以缺失有一定相似性,只是Jimmer将此特征推广到了任何属性。所以,Jimmer的这个异常和org.hibernate.LazyInitializationException有一定相似性。
所以,这并非由Jimmer制造的新问题,而是一个在静态语言ORM生态中早已存在和被接受的问题。然而,不可否认这的确对静态语言的安全性形成了破坏。
如果要追求100%的静态语言安全性,使用
DTO
对象是唯一的方法。然而,目前JVM生态的DTO映射技术存在很大缺陷。- 要么显式地映射属性*(例如纯手工映射和转化)*,这种做法工作量巨大,枯燥且容易出错。
- 要么隐式地映射属性*(例如采用BeanUtils技术)*,这种做法会引入新的不安全问题,即,无法在编译发现的问题。
即使你使用强大的mapstruct,你所能做的也只是在这两个极端之间作出选择而已。
因此,Jimmer提供了DTO语言,用户使用该语言编写非常简单的代码,编译项目即可自动生成各种丰富的DTO类型定义。
提示DTO语言的设计目的,在于
-
让生成DTO类型的过程足够简单,从而让DTO类型足够廉价。
-
100%符合静态语言安全性,在编译时发现所有问题并报错。
在任何子项目中 (并不限制为实体定义子项目),开发人员都可以在
src/main/dto
目录下随意建立扩展名为dto
的文件, 廉价的自动生成各种DTO类型。这种以极低成本快速生成的DTO类型可以和Jimmer实体对象彼此转换;因此,任何两种DTO类型都可以以Jimmer实体为中间媒介彼此转换。
参考链接:DTO语言
3.2. Fetcher ★
Fetcher,是Jimmer三个最基础的核心功能之一 (另外两个是Save Command和SQL DSL)。
Jimmer为查询任意形状的数据结构而设计,能像GraphQL那样细腻地控制被查询数据的格式。
首先,通过这个动画来感受一下Jimmer随意控制被查询数据结构形状的能力 (建立初步印象即可,不用看得太细)。
前文提到,既可以直接使用实体对象,也可以使用被廉价生成的DTO对象。Fetcher对这两种数据对象的查询都提供了一流的支持。让我们通过3个场景来展示其用法
-
查询残缺对象
所谓残缺对象,就是指查询对象的部分属性,其信息量还不如一个孤单对象丰富 。
-
附带关联对象
选定一个实体作为聚合根,不但要查询聚合根对象,还要查询其关联对象,且深度和广度都不受限制,这种格式控制能力的细腻程度和GraphQL一样 。
-
递归查询
如果实体包含自关联属性,可以进行递归查询 (截止到目前为止,GraphQL不支持递归查询) 。
无需开发人员做任何工作,ORM本身就具备了可媲美GraphQL的强大能力。所以,无论用Jimmer来构建REST服务,还是GraphQL服务,查询相关任务都非常简单。
-
快速构建REST服务
由服务端控制返回对象的形状。如果某个HTTP API需要返回的数据结构形状是什么,开发人员都有两种选择:直接使用实体类型,或用DTO语言廉价地生成DTO类型。
无论如何选择,客户端都是被动地接受服务端返回的数据格式。即使,客户端需要的数据结构形状种类非常多,对基于Jimmer开发的服务端影响很小。
如果开发人员选择直接返回实体对象而非DTO。这时服务端没有
, 这对服务端不是问题;但是对于客户端而言,这是非常糟糕的。Jimmer为客户端生成Open Api文档和TypeScript代码,如果服务端开发人员选择直接返回实体对象, 则可用
@FetchBy
注解修饰Web方法的返回类型,即可在Open Api和TypeScript代码中为客户端定义DTO类型。 -
快速构建GraphQL服务
通常情况下,提供GraphQL服务工作量不小,开发人员要花很大的精力去支持GraphQL对象之间丰富的关联。
然而,基于Jimmer实现GraphQL是非常容易的,因为ORM本身已经有了和GraphQL类似的能力,开发人员只需为GraphQL查询API实现聚合根对象的查询即可,GraphQL对象之间丰富的关联由Jimmer自动实现。
参考连接
连接 | 描述 |
---|---|
快速预览/查询任意形状 | 快速预览:查询任意形状 |
jimmer-examples/tree/main/java/jimmer-sql | 例子项目,使用Java和Jimmer构建REST服务 |
jimmer-examples/tree/main/kotlin/jimmer-sql-kt | 例子项目,使用Kotlin和Jimmer构建REST服务 |
jimmer-examples/tree/main/java/jimmer-sql-graphql | 例子项目,使用Java和Jimmer构建GraphQL服务 |
jimmer-examples/tree/main/kotlin/jimmer-sql-graphql-kt | 例子项目,使用Kotlin和Jimmer构建GraphQL服务 |
jimmer-examples/tree/main/java/jimmer-cloud | 例子项目,使用Java和Jimmer支持跨越微服务边界的ORM远程关联 |
jimmer-examples/tree/main/kotlin/jimmer-cloud-kt | 例子项目,使用Kotlin和Jimmer支持跨越微服务边界的ORM远程关联 |
3.3. Save Command ★
Save Command,是Jimmer三个最基础的核心功能之一 (另外两个是Fetcher和SQL DSL)。
保存指令专为为复杂表单设计。无论表单多么复杂,本质上就是任意形状的数据结构,Jimmer让开发人员只需使用一个方法调用就可以把任意形状的数据结构写入数据库。
无论用户传入的数据结构的复杂度如何,Jimmer都会从数据库查询出同等复杂度的数据结构并找出二者差异 (这并非真正的工作机制,但是从用户视角如此理解这个功能没问题),
并执行insert
、update
和delete
语句更新不一样的地方。
然而,如果允许客户端上传代表任意形状的数据结构的实体对象,那么客户端将获得不受限制的数据修改能力,这会严重破安全性。所以,必须通过DTO语言生成Input DTO,再用Input DTO作为Web Api的输入参数。即
-
动态实体作为内部机制,让保存指令能保存任意形状的数据结构,从功能层面支持无限种可能。
-
Input DTO作为安全卫士,严格限制用户的输入格式,只对外暴露有限的数据录入能力。
接下来,我们通过4个案例,展示保存指令的基本用法:
-
保存孤单对象
这是最简单的情况,因为孤单的对象不存在任何关联数据。
-
保存短关联
所谓短关联,指只改变当前对象和其他对象之间的关联关系,不进一步修改关联对象。 对于UI界面而言,引用关联 (一对一和多对一) 表现为单选菜单; 集合关联 (一对多和多对多) 表现为多选菜单 。
-
保存长关联
所谓长关联,指不仅要改变当前对象和其他对象之间的关联关系,还要进一步修改关联对象。 对于UI界面而言,形式多样,以表单内嵌子表最为常见 (一对多) :
-
递归保存树形结构
这个例子稍有不同,需要在保存之前稍微需改一下根节点的数据。
参考连接
连接 | 描述 |
---|---|
快速预览/查询任意形状 | 快速预览:保存任意形状 |
jimmer-examples/tree/main/java/save-command | 例子项目,使用Java和Jimmer展示Save Command的各种场景 |
jimmer-examples/tree/main/kotlin/save-command-kt | 例子项目,使用Kotlin和Jimmer展示Save Command的各种场景 |
jimmer-examples/tree/main/java/jimmer-sql | 例子项目,使用Java和Jimmer构建REST服务 |
jimmer-examples/tree/main/kotlin/jimmer-sql-kt | 例子项目,使用Kotlin和Jimmer构建REST服务 |
jimmer-examples/tree/main/java/jimmer-sql-graphql | 例子项目,使用Java和Jimmer构建GraphQL服务 |
jimmer-examples/tree/main/kotlin/jimmer-sql-graphql-kt | 例子项目,使用Kotlin和Jimmer构建GraphQL服务 |
3.4. SQL DSL ★
SQL DSL,是Jimmer三个最基础的核心功能之一 (另外两个是Fetcher和Save Command)。
Jimmer SQL DSL为任意复杂的动态SQL而设计。
现在,整个JVM生态有几十种SQL DSL。其中,以ORM风格的QueryDSL和NativeSQL风格的JOOQ最为有名。那么Jimmer的SQL DSL有什么特色呢?
-
Jimmer的SQL DSL天生为任意复杂的动态SQL而设计,包括两个 强大的能力:动态表连接、隐式子查询。
原生SQL并不利于构建复杂的动态查询,Jimmer SQL DSL的目的是对此给出相应的方案;而所有SQL DSL都能做到的强类型安全性只是附带效果。
-
非常智能的分页支持,开发人员只需要用DSL构建普通的列表查询,Jimmer自动生成分页前的总行数查询,并自动结合二者完成分页查询。
-
Jimmer的SQL DSL可以嵌入原生SQL表达式。
篇幅有限,本文只讨论上面的第一点。让我们通过三个小例子来了解Jimmer SQL DSL的基本能力:
-
简单的动态查询
简单的动态查询,建立初步印象。
-
动态表连接
为引用关联 (一对一或多对一) 属性的关联对象动态添加SQL条件。
-
隐式子查询
Jimmer支持功能更完整的普通子查询。但是,一部分因实体关联紧密相关的子查询可以写成这种更简单的隐式子查询。
为集合关联 (一对多或多对多) 属性的关联对象动态添加SQL条件。
事实上,用户还可以利用DTO语言的编写specification DTO,让Jimmer自动生成查询条件参数,以及你在上面看到的所有动态查询行为。
这个更为便捷的功能叫超级QBE
。这里,我们采用这个功能来替代上面介绍过的所有功能。
参考链接
3.5 Trigger
这个功能类似于数据库的后置触发器,在数据库变更发生后通知应用程序。
Trigger不但能通知开发人员哪些实 体对象发生了什么变化,还对变化事件的信息做了ORM映射,以通知开发人员哪些实体关联发生了变化。
Trigger为另外一个功能:缓存,打下了坚实的基础。
Jimmer支持两种各不同类型的触发器: BinLog触发器和Transaction触发器
BinLog触发器 | Transaction触发器(默认关闭) | |
---|---|---|
工作原理 | 通过整合业内成熟的CDC方案(比如:maxwell,debezium)发现数据的变化 | 靠Jimmer自身的能力发现数据库的变化 |
通知时机 | 事务提交后 | 事务提交前 |
优点 | 能感知因任何原因导致的数据库变化,包括绕过系统的数据库变化 | 原变更和触发器导致的新变更要么都成功要么都失败 |
缺点 | 事务提交后,CDC服务推送存在轻微延迟 | 只能感知当前JVM进程自己通过Jimmer对数据库的修改导致的变更,对其他任何原因导致的数据库变更无能为力 |
当前事务的持续时间会被拖长,相关资源的的解锁操作也会被滞后 | ||
会导致Jimmer对数据库的修改行为内部需要更多的额外查询 | ||
适用场景 | 绝大部分场景,包含但不限于:缓存同步,异构系统数据同步 | 必须和主业务参与同一个事务的附加行为 |
参考链接:触发器
3.6. Cache
Jimmer的缓存功能非常强大,具备以下3个特点:
-
支持多级缓存,且每一级的缓存都允许用户选择自己喜欢的技术。
-
显著增加缓存的适用范围。
不再局限于最常见的
,还支持以下三种缓存id->为对象
缓存,即-
id->关联id
缓存,即关联属性缓存能显著提高按照关联在不同对象之间导航的性能,对Jimmer中查询任意复杂的数据结构这个核心功能很有帮助。
-
id->计算结果
缓存,即计算属性缓存为复杂计算属性而设计,避免多次查询重复计算, 这同样对Jimmer中查询任意复杂的数据结构这个核心功能很有帮助。
-
还可以和权限系统相结合,既然不同身份的用户看到的数据库数据是不同的,那么他们看到的缓存数据也应该不同, 即
为不同身份的用户存储不同的数据非常耗费内存,绝对不能用于JVM内部的缓存,对于Redis这类外部缓存,也要谨慎使用,仅对非常重要的数据才开启。
信息Q: 为什么长久以来,业务系统的缓存主要局限于对象缓存,缺乏多样性?
A: 在底层框架没有为缓存一致性实现高度自动化的前提下,利用业务代码维护其他类型的缓存的成本过于高昂。
-
-
高度自动化的缓存一致性。
Jimmer为缓存一致性提供了高度的自动化支持,只需启用了Jimmer Trigger即可 (无论是BinLog触发器还是Transaction触发器)。
-
对
对象缓存
和关联属性缓存
而言,其一致性维护是全自动的,无需开发人员进行任何干预。 -
对
计算属性缓存
和多视图缓存
而言,需要开发人员的帮助,但开发需要做的事非常简单。
-
参考链接: 缓存篇
3.7. Global Filter
全局过滤器,在SQL DSL体系之外以插件的方式为实体添加固有的SQL过滤条件。最常见的适用场景为基于行的权限管理。
全局过滤器非常灵活,很容易和任何IOC框架*(比如Spring)*结合,从而自由地访问业务上下文的信息,从而实现和业务高度紧密结合的过滤逻辑。
参考链接:全局过滤器
3.8. Draft Interceptor
Draft Interceptor类似于数据库的前置触发器,在数据库变更发生前,给予用户最后一次机会调整即将保存的数据。
Draft Interceptor非常灵活,很容易和任何IOC框架*(比如Spring)*结合,从而自由地访问业务上下文的信息,从而实现和业务高度紧密结合的数据调整逻辑。
参考链接:拦截器
3.9. Logical Deletion
所谓逻辑删除,指业务层面的数据删除操作并不会导致其从数据库中被真正删除,只是修改数据的一个字段,将其标注为“已删除”即可。
逻辑删除也提供一个内置的Global Filter,该过滤器会导致被标注为“已删除”得数据不会被任何查询获得,也不会被任何修改语句影响。
Jimmer的逻辑删除,既支持实体表,也支持中间关联表。
3.10. Remote Associations
Jimmer实体支持远程关联,即,跨越微服务边界的ORM关联。
当查询特定形状复杂的数据结构时,如果数据结构的形状跨越了多个微服务,那么Jimmer会自动从不同的微服务查询数据并组装成一个整体返回给开发人员。
Remote Associations只是在有限时间内为微服务体系开发的首个功能,目的在于向开发人员证明ORM技术体系在微服务技术体系下依旧可以很强大。
等Jimmer逐渐完善发布1.0正式版本后,将会在微服务技术体系添加更多的功能,为该体系下更多的繁琐的细节进行抽象和简化。
4. 生态
接下来介绍Jimmer的第三方生态,这些作品全部由Jimmer用户贡献,在此表达诚挚的谢意
-
DTO语言Intellij插件
-
实体模型生成工具
以下工具专注于根据数据库生成实体模型
项目类型 语言支持 项目地址 Intellij插件 Java&Kotlin https://github.com/ClearPlume/jimmer-generator Intellij插件 Java&Kotlin https://github.com/huyaro/CodeGenX Maven插件 Java https://github.com/TokgoRonin/code-generator-jimmer Gradle插件 Java&Kotlin https://github.com/Enaium/jimmer-gradle 模型设计器 Java&Kotlin https://pot-mot.github.io/jimmer-code-gen-doc -
Quarkus扩展
-
Solon扩展
-
Gradle插件
5. 注意事项
使用Jimmer开发是,需要留留意一个注意事项,请参考这里