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 的开发者两天就可以上手😎。

学习资料

也可以观看视频快速学习了解:

必知必会

  1. 默认的命名映射、命名策略、空安全策略、实体定义
  2. 理解 unload 和 null(有 jpa 相关框架的使用经验很好理解)
  3. Jimmer 注解,大多和 JPA 相似,部分为独创的。官方文档虽然没有注解列表,但是使用注解时都做了详细的介绍,这部分自行查看即可。和 JPA 框架混合使用不要导错了包。
  4. Jimmer 的 swagger 功能实现必须使用相关注解(@RequestParam、@RequestBody 等等),否则无法正常访问 html 页面

unload

Jimmer 为实体的属性引入了 unload 的概念,和 Hibernate 的 lazy 类似(LazyInitialization…fuck!)。也就是说访问实体的属性必须查询,否则便会抛出异常:org.babyfish.jimmer.UnloadedException

简单测试的话就是业务代码中没有查询相关字段,此时又恰巧打印这些字段,就会抛出该异常。比如以下代码:

TestController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@AllArgsConstructor
@RequestMapping("/test")
public class TestController {

private final JSqlClient sqlClient;

@GetMapping("/unload")
public void get() {
CompanyTable table = CompanyTable.$;
Company company = sqlClient.createQuery(table)
.where(table.id().eq(1L))
.select(table.fetch(Fetchers.COMPANY_FETCHER
.companyName()
))
.fetchOne();
System.out.println(company.account());
}
}

映射

一个 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_CASELOWER_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
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
6
JSqlClient sqlClient = JSqlClient
.newBuilder()
// 全局使用假外键
.setForeignKeyEnabledByDefault(false)
...省略其他配置...
.build();
1
2
3
4
val sqlClient = newKSqlClient {
setForeignKeyEnabledByDefault(false)
...省略其他配置...
}

主动方与从动方

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

功能概览

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

  • 强大的 save command,一句话完成聚合对象和关联对象的保存;
  • 超级无敌强大 的 SQL 查询(Fetcher),自定义查询、嵌套(递归)查询;
  • DTO 和 Super QBE(Query By Example);
  • 简单、复杂的计算属性;
  • SQL 拦截器;
  • 缓存;
  • 客户端 API、客户端异常(远程异常);
  • 微服务、GraphQL 支持;
  • 技术中立,可以和任何框架集成;

计算属性

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

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

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 表示实际的表列名。

计算属性除了可以这样使用,也适用于这样一种情况:数据库中不存在,但是依赖于数据本身的信息而需要额外设置一个字段返回。 此时可以在 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 生成客户端代码的能力。

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 中,拦截器可以根据业务上下文相关信息提供默认值,比如,当前用户在权限系统中的身份信息。用户可以根据这类业务上下问信息提供和业务紧密结合的默认值,这是数据库级别默认值无法实现的。

客户端能力(需要 Java11+ 编译环境)

Jimmer 提供了一套客户端功能集:

  1. 读取 Java Doc 生成 swagger ui(无需 swagger 依赖),支持 API 分组和隐藏(ApiIgnore)
  2. 使用 @FetchBy + apt/ksp 能力为前端生成服务端返回的结构代码
  3. 更友好的暴露客户端异常,且较好维护

Jimmer 的 swagger 特色实现和生成前端结构化代码需要 Java 11+ 的编译环境(source 和 target 仍然是 8,指定使用的 jdk 为 11+ 就好);客户端异常则不需要。

客户端异常

前两种略过,讲讲客户端异常。通常,我们会自定义很多的业务异常以适应系统的需要,但是大量的自定义异常不利于维护,且非常繁琐;若使用一个或几个异常类又无法准确的表达不同异常的分界。

Jimmer 中,我们可以自定义枚举类作为异常,编译时通过 apt/ksp 能力自动帮助我们生成异常类,日常使用可以友好的暴露给客户端,若使用前后端免对接,那么客户端使用也非常的爽!

本文直接拿 官网的例子 举例。

UserInfoErrorCode.java
1
2
3
4
5
6
7
8
import org.babyfish.jimmer.error.ErrorFamily;

@ErrorFamily
public enum UserInfoErrorCode {
ILLEGAL_USER_NAME,
PASSWORD_TOO_SHORT,
PASSWORDS_NOT_SAME
}

上面定义了一个异常簇,表示与用户相关的所有异常。注解 @ErrorFamily 会被 Jimmer 的预编译器处理,为我们自动生成相关的异常类代码。注意:用作声明异常簇的枚举可以选择用 ErrorCode 或 Error 结尾。 只有以 ErrorCodeError 结尾的枚举定义 Jimmer 在生成异常类代码时才会自动去掉结尾并加上 Exception。否则,生成的异常类名 = 枚举名 + Exception。

所以,上面的代码会生成名为 UserInfoException 的 Java 类。该类生成的具体内容见 官方文档的描述。只需要记住一点:Jimmer 生成的异常类都继承自 CodeBasedRuntimeException,只有这些异常会被自动翻译。

如何使用呢?也很简单,主要就是把异常抛出去!依然使用官网的例子:

UserController.java
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/user")
public void registerUser(
@RequestBody RegisterUserInput input
) throws UserInfoException.IllegalUserName {
if (...某些条件...) {
List<Character> illegalChars = ...略...
throw UserInfoException.illegalUserName(
"The user name is invalid"
);
}
...省略其他代码...
}

可以看到,主要就是在服务端把异常抛出去,这样经过 http 传输到客户端时,Jimmer 把其 格式化“ 了,这为后面生成的前端异常结构代码打下了基础!

后端程序只要这样做就行,前端部分略过,查看官网即可。上面只抛出了异常簇中的一个具体异常,所以生成的前端代码也只包含一种异常。 是可以直接抛出整个异常类的,但为了能够精确的区分异常边界和含义表达,推荐只抛出相关的异常,同时减少客户端需要处理的异常。

生成的前端代码见 官网描述。具有很好的 IDE 提示!

触发器

虽然事务内触发器简单、无需外部依赖,但是无法保证数据的一致性。因此 Jimmer 更加推荐 binlog 触发器,使用 binlog 触发器加上消息队列就可以通过 sqlClient 上的对象获取到修改前后的值,监听数据库的变化。具体见 官网触发器介绍


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