Jimmer
Jimmer 是一个 Java 和 kotlin 的 ORM 框架,无状态设计,编译时框架,超级强大的 DSL 能力,非常推荐上手试试!如果你使用 Java 很多年了,那么你一定会愛上这款 ORM;如果你是萌新,并且有很好的接纳、学习能力,那么你也可能会喜欢上这款 ORM 的。
说实话,MyBatis 这种用模板写 SQL 的框架连表太弱了,弱类型导致 DSL 也硬不起来。而 MP 和 MPJ 提供的能力也有限。说实话,使用 MyBatis 还不如使用 JPA,JPA 还可以让自己的数据库建模提高提高。
好了,废话就说到这里。如果之前只接触过 MyBatis/MP,那么数据库的建模可能是弱项,使用 Jimmer 的时候会吃力一些,需要打打地基;而如果使用过 JPA 相关的框架,那么使用 Jimmer 较为容易上手,但两者还是有很大的不同。如果使用中遇到问题,尽量阅读 官方文档 或者加入作者大大的企鹅群交流。
友情提示,如果想要用着更加舒服,请尽量使用 Kotlin 这门有趣的语言😘,同时推荐使用 Gradle 管理依赖,熟悉 Java 的开发者两天就可以上手😎。
学习资料
也可以观看视频快速学习了解:
必知必会
- 默认的命名映射、命名策略、空安全策略、实体定义
- 理解 unload 和 null(有 jpa 相关框架的使用经验很好理解)
- Jimmer 注解,大多和 JPA 相似,部分为独创的。官方文档虽然没有注解列表,但是使用注解时都做了详细的介绍,这部分自行查看即可。和 JPA 框架混合使用不要导错了包。
- Jimmer 的 swagger 功能实现必须使用相关注解(@RequestParam、@RequestBody 等等),否则无法正常访问 html 页面
unload
Jimmer 为实体的属性引入了 unload 的概念,和 Hibernate 的 lazy 类似(LazyInitialization…fuck!)。也就是说访问实体的属性必须查询,否则便会抛出异常:org.babyfish.jimmer.UnloadedException
。
简单测试的话就是业务代码中没有查询相关字段,此时又恰巧打印这些字段,就会抛出该异常。比如以下代码:
1 |
|
映射
一个 ORM 的基础就是数据库与实体之间的映射(实体的关系),映射处理好开发功能将会无往不利,就像人际关系一样,处理好人生开挂,处理不好就像浪子一样做码农👻
Jimmer 使用非常严格的方式处理 null,默认情况下所有字段都非 null(不论是 Java 还是 Kotlin),除非显式的在字段属性上使用 @Nullable
或其它可 null
的相关注解。Kotlin 是 nullsafe 的语言,但是 Java 不是,那么 Jimmer 如何在 Java 中以最低成本解决或者避免 10 亿美元的错误呢?
- Java 的包装类型全部可 null:Boolean、Character、Byte、Short、Integer、Long、Float 或 Double;
- 基本类型和其它类型默认全部非 null;
命名策略
在 Jimmer 中,命名策略是很重要的,属于必看必知系列。不然 Jimmer 默认的命名策略在前期可能会让你无法深入体验这款 ORM 带来的巨大优势,相反,在试用中会踩到许多坑(其实踩坑学习的更快)。
默认的命名策略
开发者都知道,有些数据库是可以配置区分大小写的,所以 Jimmer 内部有一个枚举类配置了两种简单的策略:UPPER_CASE
、LOWER_CASE
,当然,推断 Jimmer 最终的命名还要加上另一个概念:snake 命名法。最终的命名结果就是 全大写或全小写的蛇形命名风格。默认情况下,Jimmer 采用 全部大写的 snake 命名法,即 DefaultDatabaseNamingStrategy.UPPER_CASE
。
掌握编程的大家对这些都不会很陌生,直接举例子更加直观:
-
UPPER_CASE 策略下:
选项 选项示例 jimmer映射结果 命名策略 tableName BootStore BOOK_STORE 表名:添加下划线 sequenceName BookStore BOOK_STORE_ID_SEQ 序列名称:添加 _ID_SEQ
后缀columnName firstName FIRST_NAME 字段名称:添加下划线 foreignKeyColumnName parentNode PARENT_NODE_ID 外键列名:添加 _ID
后缀middleTableName Book::authors BOOK_AUTHOR_MAPPING 中间表:添加 _MAPPING
后缀middleTableBackRefColumnName Book::authors BOOK_ID 中间表的反向引用:当前表名添加 _ID
后缀middleTableTargetRefColumnName Book::authors AUTHOR_ID 中间表的另一张表名添加 _ID
后缀 -
LOWER_CASE 策略下:
选项 选项示例 jimmer映射结果 说明 tableName BootStore book_store sequenceName BookStore book_store_id_seq columnName firstName first_name foreignKeyColumnName parentNode parent_node_id middleTableName Book::authors book_author_mapping middleTableBackRefColumnName Book::authors book_id middleTableTargetRefColumnName Book::authors author_id
命名策略覆盖
1 | JSqlClient sqlClient = JSqlClient |
1 | val sqlClient = newKSqlClient { |
真假外键
- 真外键:数据库真实建立的、存在的外键;
- 假/伪外键:数据库没有建立约束或者依赖于中间表的外键;
- Jimmer 的默认策略是
ForeignKeyType.AUTO
;
修改全局外键策略
1 | JSqlClient sqlClient = JSqlClient |
1 | val sqlClient = newKSqlClient { |
主动方与从动方
多对多和多对一等关系区分主动方和从动方。主动方是包含外键的表,关联注解时 不包含 mappedBy;从动方的表字段不包含外键,不能主动控制关联关系,关联注解中 包含 mappedBy。
功能概览
本文只是简单介绍一些功能,远不如官方文档详细,只是起到一个认识的作用。
- 强大的 save command,一句话完成聚合对象和关联对象的保存;
- 超级无敌强大 的 SQL 查询(Fetcher),自定义查询、嵌套(递归)查询;
- DTO 和 Super QBE(Query By Example);
- 简单、复杂的计算属性;
- SQL 拦截器;
- 缓存;
- 客户端 API、客户端异常(远程异常);
- 微服务、GraphQL 支持;
- 技术中立,可以和任何框架集成;
- …
计算属性
@Formula
简单计算属性为实现简单而快速的计算而设计,例如字符串的拼接:可以使用 SQL 语句层面的,也可以使用 Jimmer 实体的方式。
简单计算属性,拼接字符串
1 | import org.babyfish.jimmer.sql.*; |
1 | import org.babyfish.jimmer.sql.* |
除了上面的三种简单计算外,还有一种,相当于把需要进行计算的字段抽出去然后使用 @Embeddable 注解,使用方式和关联属性一样。基于 SQL 的简单计算属性内部有一个特殊的占位符 %alias。这是因为用户无法事先知道当前表在最终SQL中的别名,所以,Jimmer在这里约定 %alias
表示实际的表列名。
计算属性除了可以这样使用,也适用于这样一种情况:数据库中不存在,但是依赖于数据本身的信息而需要额外设置一个字段返回。 此时可以在 Jimmer 实体中使用带参数的 @org.babyfish.jimmer.sql.Transient
注解定义一种和数据库表结构无关的属性。当 @Transient
的参数被指定时,表示当前属性是复杂计算属性。Jimmer 为复杂计算属性提供了接口:
- Java:
org.babyfish.jimmer.sql.TransientResolver<ID, V>
- Kotlin:
org.babyfish.jimmer.sql.kt.KTransientResolver<ID, V>
该接口可以让用户自定义数据计算过程,并让此类受到 IOC 容器的托管(例如 Spring、Quarkus 的相关注解)。并且,Jimmer 充分考虑了用户可能遇到的问题:如果你的项目是多模块的,实体类不能直接引用到这个类,那么可以使用 @Transient(ref = "customerDataResolver")
的方式。
查询利器——对象抓取器 Fetcher
查询在 Jimmer 中是无敌的!!!
使用 Fetcher 不再需要创建很多的 dto class 映射,同时可以查询任意的形状!这部分看作者的视频或者亲自使用才会有震撼力和惊喜。强烈推荐这部分自己动手实践!!!
在启用客户端能力时,可以搭配 @DefaultFetcherOwner
、@FetchBy
等注解为客户端生成对应结构的服务端返回体,再利用自动化脚本可以实现前后端免对接,省时省力!
Fetcher 中还可以递归查询(例如自关联的树结构),日常业务构建树结构变得超级简单;而且可以联合查询多层嵌套关系,想抓取什么信息完全由自己定义。但是这一切的前提是:建模准确,注意是准确。频繁的添加、修改字段什么业务都可能遇到,这从来就不是大问题。
DTO 语言
说起 DTO(管他什么 O,梭哈叫 DTO),想必很多 Javaer 都被折磨过,以前使用 MyBatis 的时候,大段大段的 SQL,返回前端的字段有时候只差了几个就得新建一个文件以此与查询的 SQL 字段建立映射…有了 MapStruct 后好了一点儿。反正最后就是大量的 xxxRes、xxxResp 或者类似的其它类…
DTO 语言本质上也是 Fetcher。在 Jimmer 中,作者借用 APT(Java) 和 KSP(Kotlin) 的能力会自动解析 DTO 文件生成一些 class 辅助 Jimmer 完成超级强大的功能。一般情况下,我们自己的服务大多只需要使用 Fetcher(除了 QBE 查询),并不很需要 DTO。Jimmer 0.9+ 版本做了优化,QBE 使用的限制减小了。
DTO 文件分为三大类:View、Input、Specification,以此定义实现不同的功能。三种不同的类型生成的文件实现了不同的接口,可以灵活的转换满足不同业务的需要。DTO 生成的文件极其廉价,可以说是用完即丢。
至于定义嘛,就类似 gPRC 那种,Jimmer 有自己的语法,非常的简单。IDEA 中有 FallenAngel 大佬开发的 插件,安装后使用 DTO 功能更加得心应手。
View:查询返回前端的实体(output DTO),定义时不用任何关键字声明,最终 DTO 文件实现 View
接口
Input:保存时入参实体(input DTO),定义时使用 input
关键字声明,最终 DTO 文件实现 Input
接口,不能定义无法保存的属性。
Specification:主要用来做 QBE 查询,定义时使用 specification
关键字声明,最终 DTO 文件实现 *Specification
接口,specification 中所有属性都默认为 null。
- *Specification:Java 中是
JSpecification,Kotlin
中是KSpecification
,用作查询参数。- DTO 语言本质上是对象抓取器(Fetcher)的另外一种表达方式。
Specification 不提供和实体对象相互转化的能力,View 和 Input 具备和实体相互转换的能力。查询返回如果不是服务端自己用,而是作为 http 请求的返回值,最好使用 Fetcher,直接返回动态对象,而不是使用 DTO,足够简单且还可以利用 Jimmer 生成客户端代码的能力。
1 | // output dto:生成的 DTO 实现了 View 接口 |
拦截器
Jimmer 中提供了拦截器辅助处理业务中的一些复杂需求,在此,用户有一次修改被保存数据的机会,尤其是为某些缺失的属性赋值(比如 JPA 中的审计功能,也就是创建时间、更新时间、修改人什么的)。
数据库默认值只能提供业务无关的默认值规则。但是在 Jimmer 中,拦截器可以根据业务上下文相关信息提供默认值,比如,当前用户在权限系统中的身份信息。用户可以根据这类业务上下问信息提供和业务紧密结合的默认值,这是数据库级别默认值无法实现的。
客户端能力(需要 Java11+ 编译环境)
Jimmer 提供了一套客户端功能集:
- 读取 Java Doc 生成 swagger ui(无需 swagger 依赖),支持 API 分组和隐藏(ApiIgnore)
- 使用
@FetchBy
+ apt/ksp 能力为前端生成服务端返回的结构代码 - 更友好的暴露客户端异常,且较好维护
Jimmer 的 swagger 特色实现和生成前端结构化代码需要 Java 11+ 的编译环境(source 和 target 仍然是 8,指定使用的 jdk 为 11+ 就好);客户端异常则不需要。
客户端异常
前两种略过,讲讲客户端异常。通常,我们会自定义很多的业务异常以适应系统的需要,但是大量的自定义异常不利于维护,且非常繁琐;若使用一个或几个异常类又无法准确的表达不同异常的分界。
Jimmer 中,我们可以自定义枚举类作为异常,编译时通过 apt/ksp 能力自动帮助我们生成异常类,日常使用可以友好的暴露给客户端,若使用前后端免对接,那么客户端使用也非常的爽!
本文直接拿 官网的例子 举例。
1 | import org.babyfish.jimmer.error.ErrorFamily; |
上面定义了一个异常簇,表示与用户相关的所有异常。注解 @ErrorFamily
会被 Jimmer 的预编译器处理,为我们自动生成相关的异常类代码。注意:用作声明异常簇的枚举可以选择用 ErrorCode 或 Error 结尾。 只有以 ErrorCode
或 Error
结尾的枚举定义 Jimmer 在生成异常类代码时才会自动去掉结尾并加上 Exception
。否则,生成的异常类名 = 枚举名 + Exception。
所以,上面的代码会生成名为 UserInfoException
的 Java 类。该类生成的具体内容见 官方文档的描述。只需要记住一点:Jimmer 生成的异常类都继承自 CodeBasedRuntimeException
,只有这些异常会被自动翻译。
如何使用呢?也很简单,主要就是把异常抛出去!依然使用官网的例子:
1 |
|
可以看到,主要就是在服务端把异常抛出去,这样经过 http 传输到客户端时,Jimmer 把其 格式化“ 了,这为后面生成的前端异常结构代码打下了基础!
后端程序只要这样做就行,前端部分略过,查看官网即可。上面只抛出了异常簇中的一个具体异常,所以生成的前端代码也只包含一种异常。 是可以直接抛出整个异常类的,但为了能够精确的区分异常边界和含义表达,推荐只抛出相关的异常,同时减少客户端需要处理的异常。
生成的前端代码见 官网描述。具有很好的 IDE 提示!
触发器
虽然事务内触发器简单、无需外部依赖,但是无法保证数据的一致性。因此 Jimmer 更加推荐 binlog 触发器,使用 binlog 触发器加上消息队列就可以通过 sqlClient 上的对象获取到修改前后的值,监听数据库的变化。具体见 官网触发器介绍。