Jimmer

个人使用 Jimmer 写了一些小 Demo,忽然发现以前使用的 MyBatis 是什么鬼!这里必须提的是,如果之前只接触过 MyBatis/MP,可能数据库的建模是弱项;而如果使用过 Hibernate,那可能建模有一定基础。使用 Jimmer 时需要把数据库和实体映射做好,即数据建模完善,这样使用起来很舒服。

对了,还有一点儿,如果想要用着更加舒服,请尽量使用 Kotlin 这门有趣的语言,熟悉 Java 的开发者两天就可以上手。

本文只做一个简单的说明。源代码示例见:https://github.com/jhlzlove/gradle-project-example/tree/main/jimmer-example

2024.5.6

0.8.130 版本

由于作者大大提供了 Jimmer SpringBoot Starter,集成该 lib 使用简单,前提是使用 SpringBoot,本文不讲这种方式,需要请看作者大大的官方文档:https://babyfish-ct.github.io/jimmer-doc/zh

概览

Jimmer 不仅仅是一款 ORM,除了 ORM 外,作者大大和其他热心开源大佬还提供了许多其他的功能。

  • 强大的功能查询:自定义查询、嵌套(递归)查询
  • DTO,主要针对 Output DTO
  • 自动生成客户端结构化代码方便前后端对接
  • 远程异常
  • 计算属性
  • 缓存
  • 微服务
  • 不依赖与其它技术框架

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

映射

一个 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_CASELOWER_CASE

当然还要引入另一个概念:snake 命名法。大家对这些都不会很陌生,直接举例子更加直观:

  • UPPER_CASE 策略下:

    类型 示例 示例默认映射 说明
    tableName BootStore BOOK_STORE 表名:添加下划线 u_snake(ClassName)
    sequenceName BookStore BOOK_STORE_ID_SEQ 序列名称:添加 _ID_SEQ 后缀u_snake(ClassName)_ID_SEQ
    columnName firstName FIRST_NAME 字段名称:添加下划线 u_snake(ClassName))
    foreignKeyColumnName parentNode PARENT_NODE_ID 外键列名:添加 _ID 后缀 u_snake(ClassName)_ID
    middleTableName Book::authors BOOK_AUTHOR_MAPPING 中间表:添加 _MAPPING 后缀 u_snake(SourceClassName)_u_snake(TargetClassName)_MAPPING
    middleTableBackRefColumnName Book::authors BOOK_ID 中间表的反向引用:当前表名添加 _ID 后缀 u_snake(SourceClassName)_ID
    middleTableTargetRefColumnName 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
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)
...省略其他配置...
}

计算属性

@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 表示实际的表列名;
  • 建议认真考虑 @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 语言本质上是对象抓取器的另外一种表达方式。
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 {

}

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