现在,有很多ORM框架可供选择,比如:JPA、myBatis、JOOQ、Exposed、Ktorm。 它们够用吗?
或许,一个实现层面更轻量,但功能层面却足够强大的革命性ORM能颠覆人们对ORM的传统认知。于是我创作这个ORM
项目地址:https://github.com/babyfish-ct/jimmer
文档(中英双语)地址:https://babyfish-ct.github.io/jimmer-doc/
视频地址: https://www.bilibili.com/video/BV1dA4y1R7pV/
JDK要求:8
项目分为两个部分:jimmer-core和jimmer-sql。
-
jimmer-core, 定义了一套全新的不可变对象体系,用于定义实体类型,作为ORM的基石。jimmer-sql的强大,有一部分是基于jimmer-core的强大。
作为对kotlin data class的抄袭回应,java14加入了record类型,用于创建不可变对象。即便去观察一些JVM平台的语言,也可以看见越来越多的现代编程语言(如C#9.0)对不可变对象进行了内置支持,不可变对象代表着未来编程语言的发展发向。
因为不可变对象可以在语义层面混淆基于引用的共享和基于值的复制,不用担心数据内部细节被其它人意外修改,尤其是深层次的细节和多人开发的项目。只有数据库和缓存才有足够的分量成为可多方修改的共享数据,让开发人员去思考多方修改的副作用或利用这种副作用。普通的Java对象应该彻底消除这种顾虑,这就是现代编程语言的发展确实越来越喜欢不可变对象原因。
不幸的是,不可变对象有它的问题,由于对象是不可变的,我们不能直接修改它,而需要基于旧对象创建新对象。如果对象只是一个简单对象,开发人员面对的复杂性还很一般,但是,只要对象具有一定的关联深度,“修改”深层次的关联对象将会变成噩梦。为了节约篇幅,本文不讨论这个痛点都多痛,请参考这个链接。
然而,java record(或kotlin data class)并没有回答这个痛点该如何解决。为此jimmer把JS/TS领域一个叫immer的功能移植给了Java。这是目前已知不可变对象最强方案,也是这个项目叫做jimmer的根本原因。
jimmer可以基于已有的不可变对象创建可变的临时代理。由于代理是可变的,你当然可以随意修改,尤其是很深的关联对象。整个过程完成后,临时代理消亡前会利用其收集到的所有用户修改行为去创建另外一个不可变对象。这个过程看起来,就如同直接修改传统的可变对象一样简单,而幕后是基于对象树的copy-on-write策略,只有发生变化的部分会被拷贝,没有变化的部分子树永远在新旧对象之间共享。
为了和ORM配合,不可变对象具备动态性。并不是对象的所有属性都需要初始化,它允许缺少一些属性,这个特性在传统ORM中称为延迟加载,并非所有对象属性,尤其是关联属性,都需要从数据库中查询。
这里的未指定的属性并不是null,而是未知。在直接被代码访问时会导致异常,而在JSON序列化中会被自动忽略,不会异常。
在传统的ORM中,这种信息不完整的对象仅仅可以在ORM内部被产生,返回给用户(比如,Hibernate中lazy的多对一属性返回代理对象,改代理只有id);而在jimmer中,无论是ORM还是用户代码,双方都可以随意可以构建任意形状的信息不完整的动态对象树给给对方用。这是jimmer能提供远强于其他ORM的功能的根本原因所在。
值得一提的是,对象动态性还有一个妙用。定义实体类型时,类型之间允许双向关联,没有设计限制。但是具体业务实现需要创建对象时,对象之间只允许单方关联,保证不出现环形引用,以方便微服务之间的交流和和前端的交流。
类型定义允许双向关联+对象实例之间仅允许单项关联
的目的是为了让开发人员实现业务聚合根设计的滞后化。 -
jimmer-sql,基于jimmer-core动态不可变对象的ORM。
从实现层面讲,jimmer-sql轻量得让人难以置信,除了JDBC外没有任何依赖,甚至连myBatis中那种对数据库连接的SqlSession轻量级封装都没有。
与QueryDsl, JOOQ, JPA Criteria类似,强类型的SQL DSL,绝大部分SQL错误都在编译时刻被报告,而非表现为运行时异常。
然而强类型SQL DSL和Native SQL不冲突,通过优雅的API,在强类型的SQL DSL中混入Native SQL,鼓励开发人员使用特定数据库产品特有的功能,比如分析函数,正则。
除了所有ORM的必须有的功能外,jimmer-sql提供了4个其它远超其他ORM的功能:Save指令、对象抓取器、动态表连接、更智能的分页查询。这4个明显区别于其它ORM的强大的功能,是本文要重点讨论的内容。
本文内容提纲
-
jimmer-core: 让User Bean足够强大
- 使用不可变数据,但支持临时可变代理
- 动态对象
-
jimmer-sql: 基于不可变对象的ORM
-
Save指令:将任意复杂的对象【树】保存到数据库中。
无论复杂业务对数据库的更新有多复杂,只要能通过一颗对象树来表达,都可以一个API调用存入数据库。
-
对象抓取器:从数据库中查询任意复杂的对象【树】。
非GraphQL,但胜似GraphQL。
-
动态表连接
一个myBatis难以实现的高级SQL拼接功能,特别实用。
-
更智能的分页查询
自动根据data-query生成并优化count-query,从此告别复杂分页查询要构建两个查询的问题。
-
1. jimer-core: 让User Bean足够强大
1.1 使用不可变数据,但支持临时可变代理
@Immutable
public interface TreeNode {
String name();
TreeNode parent();
List<TreeNode> childNodes();
}
Annotation processor自动生成一个接口: TreeNodeDraft
. 该接口是可变的,且从TreeNode派生,使用方式如下。
// 第一步: 从头创建全新对象
TreeNode oldTreeNode = TreeNodeDraft.$.produce(root ->
root
.setName("Root")
.addIntoChildNodes(child ->
child.setName("Drinks")
)
.addIntoChildNodes(child ->
child.setName("Breads")
)
);
// 第二步: 基于已有对象,创建新对象
TreeNode newTreeNode = TreeNodeDraft.$.produce(
oldTreeNode, // 现有旧对象
root ->
root // 根代理
.childNodes(false).get(0) // 得到子代理
.setName("Dranks+"); // 修改子代理
);
System.out.println("Old tree node: ");
System.out.println(oldTreeNode);
System.out.println("New tree node: ");
System.out.println(newTreeNode);
最终打印结果如下
Old tree node:
{"name": "Root", childNodes: [{"name": "Drinks"}, {"name": "Breads"}]}
New tree node:
{"name": "Root", childNodes: [{"name": "`Drinks+`"}, {"name": "Breads"}]}
1.2 动态对象.
数据对象的任何属性都可以是未指定的。
- 直接访问未指定的属性会导致异常。
- 使用Jackson序列化,未指定的属性将被忽略,不会抛异常。
TreeNode current = TreeNodeDraft.$.produce(current ->
node
.setName("我")
.setParent(parent -> parent.setName("父亲"))
.addIntoChildNodes(child -> child.setName("儿子"))
);
// 你可以访问被指定的属性
System.out.println(current.name());
System.out.println(current.parent());
System.out.println(current.childNodes());
/*
* 但是你无法访问未被指定的属性,比如
*
* current.parent().parent();
* current.childNodes().get(0).childNodes()
* 因为直接访问未指定的属性会导致异常。
*/
/*
* 最终你会得到这样的JSON
*
* {
* "name": "我",
* parent: {"name": "父亲"},
* childNodes:[
* {"name": "儿子"}
* ]
* }
*
* 因为使用Jackson序列化,未指定的属性将被忽略,不会抛异常。
*/
String json = new ObjectMapper