Jimmer

关于 Jimmer 这个 ORM 需要自己亲自上手使用才能体会他的强大,我说的不是简单的 Demo,浪子体验的时候写了一些简单的代码,只是感觉很方便,由于浪子很喜欢 DSL,所以不排斥。但当真正写业务的时候才发现远比我想的要强大许多!

如果你使用 Java 很多年了,那么你一定会愛上这款 ORM;如果你是萌新,并且有很好的接纳、学习能力,那么你也会喜欢上这款 ORM 的。

说实话,MyBatis 这种纯写 SQL 的确实有些弱,MP 提供的能力也有限,DSL 并没有联表查询的能力。使用 MyBatis 还不如使用 JPA,使用 JPA 还可以让自己的数据库建模提高提高。

好了,介绍就写到这里。如果之前只接触过 MyBatis/MP,那么数据库的建模可能是弱项,使用 Jimmer 的时候会吃力一些;而如果使用过 JPA 相关的框架,那么使用 Jimmer 较为容易上手,但两者还是有很大的不同。如果使用中遇到问题,尽量阅读 官方文档 或者加作者大大的企鹅群交流。

对了,友情提示,如果想要用着更加舒服,请尽量使用 Kotlin 这门有趣的语言😘,同时推荐使用 Gradle 管理依赖,熟悉 Java 的开发者两天就可以上手😎。

2024.11.3

0.9.9

2024.8.20

0.8.155

功能概览

本文只是简单介绍一些功能,远不如官方文档详细,只是起到一个认识的作用。

Jimmer 不仅仅是一款 ORM,除了 ORM 的核心能力外,作者大大和其他热心开源大佬还在其基础上提出并完善了更加高级的能力。

  • 完善的 save command
  • 超级无敌强大的功能查询(Fetcher 以及 DTO):自定义查询、嵌套(递归)查询
  • 简单、复杂的计算属性
  • Super QBE(Query By Example)
  • 缓存
  • SQL 拦截器
  • 客户端 API、客户端异常(远程异常)
  • 微服务、GraphQL 支持
  • GraphQL 支持
  • 不依赖与其它技术框架,也就是可以和任何框架集成

推荐按照顺序看一下以下视频,可以直观的了解 Jimmer 的设计理念和最终的效果,这对于个人的成长是非常有益的,另一方面,看完以后也可能觉得这个才是好用的 ORM…

映射

一个 ORM 的基础就是数据库与实体之间的映射,尤其是如何以最低的成本解决或者避免 10 亿美元的错误。

Jimmer 使用非常严格的方式处理 null,默认情况下所有字段都非 null(不论是 Java 还是 Kotlin),除非显式的在字段属性上使用 @Nullable 或其它可 null 的相关注解。Kotlin 是 nullsafe 的语言,但是 Java 不是,那么 Jimmer 如何处理 Java 中的映射关系呢?

  • 基本类型非 null: 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 支持;

命名策略

在 Jimmer 中,命名策略是很重要的,属于必看必知系列。不然 Jimmer 默认的命名策略在前期可能会让你无法深入体验这款 ORM 带来的巨大优势,相反,在试用中会踩到许多坑(其实踩坑学习的更快)。

默认的命名策略

开发者都知道,有些数据库是可以配置区分大小写的,所以 Jimmer 内部有一个枚举类配置了两种简单的策略:UPPER_CASELOWER_CASE,当然,推断 Jimmer 最终的命名还要加上另一个概念:snake 命名法。最终的命名结果就是 蛇形命名的全大写或全小写。掌握编程的大家对这些都不会很陌生,直接举例子更加直观:

  • 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

DefaultDatabaseNamingStrategy.UPPER_CASE 是 Jimmer 的默认命名策略。

覆盖策略

1
2
3
4
5
6
7
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setDatabaseNamingStrategy(
DefaultDatabaseNamingStrategy.LOWER_CASE
)
...省略其他配置...
.build();
1
2
3
4
5
6
val sqlClient = newKSqlClient {
setDatabaseNamingStrategy(
DefaultDatabaseNamingStrategy.LOWER_CASE
)
...省略其他配置...
}

真假外键

  • 真外键:数据库真实建立的、存在的外键;
  • 假/伪外键:数据库没有建立约束、依赖于中间表的外键;
  • Jimmer 的默认策略是 ForeignKeyType.AUTO

修改全局外键策略

1
2
3
4
5
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setForeignKeyEnabledByDefault(false)
...省略其他配置...
.build();
1
2
3
4
val sqlClient = newKSqlClient {
setForeignKeyEnabledByDefault(false)
...省略其他配置...
}

多对多和多对一等关系区分主动方和从东方。主动方是包含外键的表,关联注解时 不包含 mappedBy;从动方的表字段不包含外键,不能主动控制关联关系,关联注解中 包含 mappedBy

计算属性

@Formula 简单计算属性为实现简单而快速的计算而设计,例如字符串的拼接:可以使用 SQL 语句层面的,也可以使用对应实体的方式。

简单计算属性,拼接字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import org.babyfish.jimmer.sql.*;

@Entity
public interface Author {

@Formula(sql = "concat(%alias.FIRST_NAME, ' ', %alias.LAST_NAME)")
String fullName();

...省略其他属性...

/**
* 上面的方式和下面的方式最终结果相同,使用的方式不同
*/
String firstName();

String lastName();

@Formula(dependencies = {"firstName", "lastName"})
default String fullName() {
return firstName() + ' ' + lastName();
}

/**
* 依赖于关联属性
*/
@ManyToMany
List<Author> authors();

@Formula(dependencies = "authors")
default int authorCount() {
return authors().size();
}

@Formula(dependencies = {"authors.firstName", "authors.lastName"})
default List<String> authorNames() {
return authors()
.stream()
.map(author -> author.firstName() + ' ' + author.lastName())
.collect(Collectors.toList());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.babyfish.jimmer.sql.*

@Entity
interface Author {

@Formula(sql = "concat(%alias.FIRST_NAME, ' ', %alias.LAST_NAME)")
val fullName: String

...省略其他属性...

/**
* 上面的方式和下面的方式最终结果相同,使用的方式不同
*/
val firstName: String

val lastName: String

@Formula(dependencies = ["firstName", "lastName"])
val fullName: String
get() = "$firstName $lastName"

/**
* 依赖于关联属性
*/
@ManyToMany
val authors: List<Author>

@Formula(dependencies = "authors")
val authorCount: Int
get() = authors.size

@Formula(dependencies = ["authors.firstName", "authors.lastName"])
val authorNames: List<String>
get() = authors.map { "${it.firstName} ${it.lastName}" }
}

除了上面的三种简单计算外,还有一种,相当于把需要进行计算的字段抽出去然后使用 @Embeddable 注解,使用方式和关联属性一样。基于 SQL 的简单计算属性内部有一个特殊的占位符 %alias。这是因为用户无法事先知道当前表在最终SQL中的别名,所以,Jimmer在这里约定 %alias 表示实际的表列名。

计算属性除了可以这样使用,也适用于这样一种情况:数据库中不存在,但是依赖于数据本身的信息而需要额外设置一个字段返回。 这种时候我们经常使用 @org.babyfish.jimmer.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 充分考虑了用户可能遇到的问题,如果你的项目是多模块的,实体类不能引用到这个类,那么可以使用 @Transient(ref = "customerDataResolver") 的方式。

Jimmer 注解

Jimmer 的注解大多和 JPA 类似,也有一部分是作者为了解决某些问题而独创的,这里就不列举了,官方文档虽然没有注解列表,但是每一篇文档使用时都做了详细的介绍,这部分自行查看即可。注意,和 JPA 框架混合使用不要导错了包。

查询利器——对象抓取器 Fetcher

查询在 Jimmer 中是无敌的!!!

Jimmer 提供的查询的 Wrapper(暂时应该可以这么说吧)非常强大,可以实现 GraphQL 的效果,并且不再需要自己创建很多的 dto 文件映射,转而创建的是 newFetcher,在 Fetcher 中可以定义我们查询返回的字段。并且使用此功能可以为客户端生成对应结构的服务端返回体,利用自动化脚本可以实现前后端免对接。

如果前端不使用 Jimmer 的能力,那么后端只要做好数据建模,在业务层直接嵌套查询,Fetcher 都不需要创建(项目不大的情况)😂不过为了好维护,还是推荐创建的,分类管理。

Fetcher 中还可以递归查询(例如自关联的树结构),日常业务构建树结构变得超级简单;而且可以联合查询多层嵌套关系,想抓取什么信息完全由自己定义。但是这一切的前提是:建模准确,注意是准确。频繁的添加、修改字段什么业务都可能遇到,这从来就不是问题,更不是接口。这也就是 Jimmer 对实体建模要求较高的原因,本文开头也指明了。

  • 一般情况下,我们自己的服务只需要使用 Fetcher(除了 QBE 查询),并不很需要下面所讲的 DTO。0.9 版本做了优化 QBE 使用的限制减小了。
  • 在 Fetcher 中,还可以使用 @DefaultFetcherOwner@FetchBy 等注解为前端自动生成结构化实体、Javadoc 形式的注释说明也会被 Jimmer 读取并显示在 swagger 文档和生成的前端代码中(Jimmer 做的增强)。
  • DTO 语言本质上也是 Fetcher。

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:主要用来做 QBE 查询,定义时使用 specification关键字声明,最终 DTO 文件实现 *Specification 接口,specification 中所有属性都默认为 null。

  • *Specification:Java 中是 JSpecification,Kotlin 中是 KSpecification
  • DTO 语言本质上是对象抓取器(Fetcher)的另外一种表达方式。

Specification 不提供和实体对象相互转化的能力,View 和 Input 具备和实体相互转换的能力。查询返回如果不是服务端自己用,而是作为 http 请求的返回值,最好使用 Fetcher,直接返回动态对象,而不是使用 DTO,并且还可以利用 Jimmer 生成客户端代码的能力。

person.dto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// output dto:生成的 DTO 实现了 View 接口
Person {

}

// input dto:生成的 DTO 实现了 Input 接口
input PersonAndOtherInfo {

}

// specification dto:生成的 DTO 实现了 *Specification 接口
specification PersonSpec {

}

拦截器

Jimmer 中提供了拦截器辅助处理业务中的一些复杂需求,在此,用户有一次修改被保存数据的机会,尤其是为某些缺失的属性赋值(比如 JPA 中的审计功能,也就是创建时间、更新时间、修改人什么的)。

数据库默认值只能提供业务无关的默认值规则。但是在 Jimmer 中,拦截器可以根据业务上下文相关信息提供默认值,比如,当前用户在权限系统中的身份信息。用户可以根据这类业务上下问信息提供和业务紧密结合的默认值,这是数据库级别默认值无法实现的。

unloaded 和 null

jimmer 中有 unloaded 和 null 的区别,这里点一下。

最简单的理解 unladed 就是没有,不存在,定义对象的时候有属性,但是赋值时没有该属性。所以在 jimmer 中访问 unloaded 属性会报错。
而 null 是传了属性字段,并且该字段的值为 null。

null 的二义性在 Java 中特别常见,有时有业务意义,有时又要严格防止出现 NPE,相信 javaer 都有体会。由于在 Jimmer 中严格区分两种含义,规避了二义性,所以 Jimmer 的可空判断精准有力,与前面的非空策略遥相呼应。虽然非空策略主要是用来做数据库映射检查以及 DTO 语言的增强,但并不妨碍这也是引入的 unload 的基础。

这个特性可以让你在 jimmer 中构造树状结构时,可直接根据顶层的父节点是否为 null 直接递归构建(自关联)。


本站由 江湖浪子 使用 Stellar 1.29.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。