Jimmer
个人使用 Jimmer 写了一些小 Demo,忽然发现以前使用的 MyBatis 是什么鬼!这里必须提的是,如果之前只接触过 MyBatis/MP,可能数据库的建模是弱项;而如果使用过 Hibernate,那可能建模有一定基础。使用 Jimmer 时需要把数据库和实体映射做好,即数据建模完善,这样使用起来很舒服。
对了,还有一点儿,如果想要用着更加舒服,请尽量使用 Kotlin 这门有趣的语言,熟悉 Java 的开发者两天就可以上手。
本文只做一个简单的说明。源代码示例见:https://github.com/jhlzlove/gradle-project-example/tree/main/jimmer-example
0.8.130 版本
由于作者大大提供了 Jimmer SpringBoot Starter,集成该 lib 使用简单,前提是使用 SpringBoot,本文不讲这种方式,需要请看作者大大的官方文档:https://babyfish-ct.github.io/jimmer-doc/zh
概览
Jimmer 不仅仅是一款 ORM,除了 ORM 外,作者大大和其他热心开源大佬还提供了许多其他的功能。
- 强大的功能查询:自定义查询、嵌套(递归)查询
- DTO,主要针对 Output DTO
- 自动生成客户端结构化代码方便前后端对接
- 远程异常
- 计算属性
- 缓存
- 微服务
- 不依赖与其它技术框架
推荐按照顺序看一下以下视频,可以直观的了解 Jimmer 的设计理念和最终的效果,这对于个人的成长是非常有益的,另一方面,你也可能觉得这个才是好用的 ORM…
- Jimmer之任意复杂动态查询
- Jimmer之DTO语言
- Jimmer0.6x: 前后端免对接+spring starter,让REST媲美GraphQL
- Jimmer新版客户端,质变
- Jimmer-0.7之计算属性
- Jimmer0.7之一句话保存任意复杂数据结构(含递归)
映射
一个 ORM 的基础就是数据库与实体之间的映射,Jimmer 使用非常严格的方式处理 null,默认所有字段为非 null,除非显式的在字段上使用 @Nullable
或其它可 Null
的 相关注释。Kotlin 是空安全的语言,但是 Java 不是,那么 Jimmer 如何处理 Java 中的映射关系呢?
- 如果属性类型为 boolean、char、byte、short、int、long、float 或 double,则非 null;
- 如果属性类型为 Boolean、Character、Byte、Short、Integer、Long、Float 或 Double,则可 null;
来自 Jimmer 作者大大的话:
推荐使用
org.jetbrains.annotations.Nullable
,因为
- 虽然可识别的 annotation 不受限制,但是如果使用了未被 Jimmer annotation processor 默认包含的 annotation,需要将其依赖添加到 annotation processor 中,这终归是麻烦;
org.jetbrains.annotations.Nullable
受 Intellij 支持;
@Id
非 null
一对多和多对多属性必须非 null
命名策略
在 Jimmer 中,命名策略是很重要的,不然 Jimmer 默认的命名策略可能会让你无法深入体验这款 ORM 带来的巨大优势,当然,这的前提是你没有看作者大大的文档。
默认的命名策略
开发者都知道,有些数据库是可以配置区分大小写的,所以 Jimmer 内部有一个枚举类配置了两种简单的策略:UPPER_CASE
、LOWER_CASE
。
当然还要引入另一个概念:snake 命名法。大家对这些都不会很陌生,直接举例子更加直观:
-
UPPER_CASE 策略下:
类型 示例 示例默认映射 说明 tableName BootStore BOOK_STORE 表名:添加下划线 u_snake(ClassName) sequenceName BookStore BOOK_STORE_ID_SEQ 序列名称:添加 _ID_SEQ
后缀u_snake(ClassName)_ID_SEQcolumnName firstName FIRST_NAME 字段名称:添加下划线 u_snake(ClassName)) foreignKeyColumnName parentNode PARENT_NODE_ID 外键列名:添加 _ID
后缀 u_snake(ClassName)_IDmiddleTableName Book::authors BOOK_AUTHOR_MAPPING 中间表:添加 _MAPPING
后缀 u_snake(SourceClassName)_u_snake(TargetClassName)_MAPPINGmiddleTableBackRefColumnName Book::authors BOOK_ID 中间表的反向引用:当前表名添加 _ID
后缀 u_snake(SourceClassName)_IDmiddleTableTargetRefColumnName Book::authors AUTHOR_ID 中间表的另一张表名添加 _ID
后缀 u_snake(TargetClassName)_ID -
LOWER_CASE 策略下:
类型 举例 默认映射 说明 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 { |
计算属性
@Formula
简单计算属性为实现简单而快速的计算而设计,例如字符串的拼接:可以使用 SQL 语句层面的,也可以使用对应实体的方式。
1 | import org.babyfish.jimmer.sql.*; |
1 | import org.babyfish.jimmer.sql.* |
- 除了上面的三种简单计算外,还有一种,相当于把需要进行计算的字段抽出去然后使用
@Embeddable
注解,使用方式和关联属性一样;- 基于 SQL 的简单计算属性内部有一个特殊的占位符
%alias
。这是因为用户无法事先知道当前表在最终SQL中的别名,所以,Jimmer在这里约定%alias
表示实际的表列名;- 建议认真考虑
@Formula
计算属性应该基于 Java/Kotlin 计算还是基于 SQL 计算
@Transient
处理复杂的计算。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 相关注解
作者大大使用的 ORM 很多,对 Hibernate 了解很深,所以注解大多和 Hibernate 相同,当然也有一些是作者为了解决某些问题而独有的,这里就不列举了,注意混合使用不要导错了包。
查询利器——对象抓取器 Fetcher
查询在 Jimmer 中是无敌的!!!
Jimmer 提供的查询的 Wrapper(暂时应该可以这么说吧)非常强大,可以实现 GraphQL 的效果,并且不再需要自己创建很多的 dto 文件映射,转而创建的是 newFetcher,在 Fetcher 中可以定义我们查询返回的字段。并且使用此功能可以为客户端生成对应结构的服务端返回体,利用自动化脚本可以实现前后端免对接。
Fetcher 中还可以递归查询(例如自关联的树结构),可以联合查询多层嵌套关系。这也就是 Jimmer 对实体建模要求特别高的原因,本文开头也指明了。
DTO
说起 DTO(本文全部统称 DTO,管他什么 O),想必很多 Java 程序员都被折磨过,以前使用 MyBatis 的时候,大段大段的 SQL,返回前端的字段有时候只差了几个就得新建一个文件以此与查询的 SQL 字段建立映射…于是后来有了 MapStruct 后好了一点儿。
在 Jimmer 这个新框架中,作者借用 apt(Java) 和 ksp(Kotlin) 的能力会自动生成一些 DTO 文件,作者把这些 DTO 文件分为三类:View、Input、Specification,以此区分不同的功能,这三种类型生成的 DTO 实现了不同的接口,可以在不同的业务中灵活的转换。由于可以自动生成 DTO 文件,所以这些文件极其廉价,可以说是用完即丢,至于定义嘛,就类似 gPRC 那种,Jimmer 有自己的语法,非常的简单。IDEA 中有 FallenAngel 大佬开发的插件,安装后使用 DTO 功能如虎添翼。
DTO 只是 Jimmer ORM 额外提供的一个工具,但是这个工具却很常用,所以也是 Jimmer 提供的核心功能之一。
View:查询返回前端的实体(output DTO),定义时不用任何关键字声明,最终 DTO 文件实现 View
接口
Input:保存时入参实体(input DTO),定义时使用 input
关键字声明,最终 DTO 文件实现 Input
接口,不能定义无法保存的属性
Specification:本身和 DTO 关系不大,可以用作查询参数,支持 QBE 查询,定义时使用 specification
关键字声明,最终 DTO 文件实现 *Specification
接口,specification 中所有属性都默认可 null
- *Specification:Java 中是
JSpecification,Kotlin
中是KSpecification
。- Specification 不提供和实体对象相互转化的能力,View 和 Input 具备和实体相互转换的能力。
- 查询返回如果不是服务端自己用,而是作为 http 请求的返回值,最好使用 Fetcher,直接返回动态对象,而不是使用 DTO,并且还可以利用 Jimmer 生成客户端代码的能力。
- DTO 语言本质上是对象抓取器的另外一种表达方式。
1 | // output dto:生成的 DTO 实现了 View 接口 |