前言

最近发现了这个新框架,想着学习一下发现网上的教程几乎全是对官网的复制粘贴…… 很多细节要点都没有告诉你,很容易让人看的一头雾水,所以自己跟着官网文档做了一个示例Demo,写个教程做记录,也分享给需要的各位(后面会使用flex指代mybatis-flexplus指代mybatis-plus

常见问题

这里我把常见问题放在前面是因为有很多坑,所以我想先把我踩过的坑列出来,提前了解,不然后面问题很多不好解释。这些坑我也都是从官网找的对应解决方法,在这里放出来同时加深一下印象。

如何使用分页

以前我们mybatis-plus会搭配pagehelper-spring-boot-starter依赖来实现分页,但是使用mybatis-flex后,请注意,将分页依赖更换为pagehelper依赖 原因是pagehelper-spring-boot-starter依赖的mybatis-spring-boot-starter会使flex启动异常,导致查询时参数赋值不上,会产生《No value specified for parameter xxxx》错误。当然也可以使用自带的分页,本文使用自带的分页进行演示!!!

热部署产生的异常

在使用selectOneByQuery这个方法时,查询获取的对象不能成功转换为对应的实体类,报《ClassNotFoundException》错误,但返回的确实是对应类型。原因是启用了热部署造成的,解决办法也很简单,在resource下建立一个META-INF目录,新建文件命名spring-devtools.properties,内容写入:

1
2
3
4
5
restart.include.mapper=/mapper-[\\w-\\.].jar 

restart.include.pagehelper=/pagehelper-[\\w-\\.].jar

restart.include.mybatis-flex=/mybatis-flex-[\\w-\\.]+jar

启动报错Property ‘sqlSessionFactory’ or ‘sqlSessionTemplate’ are required

添加hikari连接池依赖即可解决

SpringBoot2.x版本:

1
2
3
4
5
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>

SpringBoot3.x版本:

1
2
3
4
5
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>

如果使用的是 druid 数据库连接池,则需要添加数据源类型的配置spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

为什么要使用mybatis-flex?

这里我直接搬官网的说明了。简而言之,flex相对于我们使用的plus而言,支持了多表查询!这是我认为很大的一个亮点;然后就是对于字段的更细一步的处理、自带多数据源切换、自带分批批量插入以及性能上的提升等等;代码写法也进行了更新,例如下图,flex是内部使用APT(Annotation Processing Tool)生成的字段,不容易出错且美观,而plus则是全程使用字符串完成。在性能上,根据官网的描述,flex的查询比plus快了近10倍

image-20240104133401778

功能或特点 MyBatis-Flex MyBatis-Plus Fluent-MyBatis
对 entity 的基本增删改查
分页查询
分页查询之总量缓存
分页查询无 SQL 解析设计(更轻量,及更高性能)
多表查询: from 多张表
多表查询: left join、inner join 等等
多表查询: union,union all
单主键配置
多种 id 生成策略
支持多主键、复合主键
字段的 typeHandler 配置
除了 MyBatis,无其他第三方依赖(更轻量)
QueryWrapper 是否支持在微服务项目下进行 RPC 传输 未知
逻辑删除
乐观锁
SQL 审计
数据填充
数据脱敏 ✔️ (收费)
字段权限 ✔️ (收费)
字段加密 ✔️ (收费)
字典回写 ✔️ (收费)
Db + Row
Entity 监听
多数据源支持 借助其他框架或收费
多数据源是否支持 Spring 的事务管理,比如 @TransactionalTransactionTemplate
多数据源是否支持 “非Spring” 项目
多租户
动态表名
动态 Schema

springBoot引入mybatis-flex

环境

下面开始说一下我这里所使用的环境:

Java:17

SpringBoot:3.0.5

mybatis-flex:1.7.6

mysql:8.0.33

我是基于SpringBoot3.0.5版本进行测试的,如果出现问题,首先排查版本,其次去官网的faq部分去排查。mybatis-flex常见问题,还有问题可以在本文章下留言或者去github留言mybatis-flex github 项目是23年3月左右开源的,是一个很年轻的项目,还需要大家多多使用,磨合一段时间

依赖

依赖我使用的都是最新版本

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
42
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

<!--dev-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!--mybatis-flex-->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>1.7.6</version>
</dependency>

<!--mybatis-flex APT-->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-processor</artifactId>
<version>1.7.6</version>
<scope>provided</scope>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>

配置

通过mybatis-flex属性配置各种内容,这里我配置了2个数据源,分别是sora01sora02。以往我们使用多数据源往往需要引入dynamic依赖,而现在直接内部集成了,这个就很棒了。后面的跟mybatis-plus是一样的,通过mapper-locations配置mapper文件地址,同时开启sql日志打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 1234
mybatis-flex:
datasource:
sora01:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sora33?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
sora02:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sora-xxl?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
mapper-locations: classpath:mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

我们在选择数据源的时候有四种方式,分别是代码中使用在类上或方法上使用@UseDataSource注解以及直接在Table注解上配置。四个配置的优先级如下:

1
`DataSourceKey.use()` > `@UseDataSource()在方法上` > `@UseDataSource()在类上` >`@Table(dataSource="...")`

同时,现在数据库和实体类字段之间的驼峰转换从以前的一刀切改为了表级别的粒度。

在mybatis-plus中,我们需要通过在配置文件内配置驼峰转换,这样我们一整个模块都会应用。

1
2
configuration:
map-underscore-to-camel-case: true

而在flex中,我们可以直接在Table注解上配置是否启用驼峰转换,如下图则关闭驼峰转换(默认是开的)

1
@Table(value = "sora_user", camelToUnderline = false)

前置准备

首先是实体类,通过Table注解标注表名,Id注解表明主键使用lombok生成getset方法

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
42
43
44
45
46
47
48
49
@Data
@Table("sora_user")
public class User implements Serializable {
/**
* 主键
*/
@Id(keyType = KeyType.Auto)
private String id;
/**
* 用户名
*/
private String name;
/**
* 性别
*/
private String sex;
/**
* 出生日期
*/
private Date birthday;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 权限id
*/
private Integer roleLevel;
/**
* 创建时间
*/
private Date createTime;
/**
* 是否被封禁
*/
private String banned;
/**
* 是否被禁用
*/
private String disabled;
}

之后我们需要让mapper接口实现BaseMapper接口并制定对应的实体类,如果是plus的话,还需要对service以及service实现类实现指定的接口和继承指定的类。

1
2
3
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

编译项目获取APT文件

注意:我们上面说flex使用了APT技术。所以在使用之前必须先编译项目,官网给出的说明如下:

image-20240104164352964

我习惯创建完实体类直接启动一次项目,这样更不容易出错,生成的APT文件在target文件夹下的generated-sources目录内。默认会在你原本的类名后面跟Def

image-20240104164652764

如果文件生成了但代码内使用不了参考下图内的解决办法

image-20240104164829785

下面是User类生成后的文件,包含了当前类所有的字段。在后面拼接条件时,可以随意获取想要的字段

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.sora.domain.table;

import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.table.TableDef;

// Auto generate by mybatis-flex, do not modify it.
public class UserTableDef extends TableDef {

/**
* @Classname User
@Description User
@Date 2024/01/04 10:10
@Author by Sora33
*/
public static final UserTableDef USER = new UserTableDef();

/**
* 主键
*/
public final QueryColumn ID = new QueryColumn(this, "id");

/**
* 性别
*/
public final QueryColumn SEX = new QueryColumn(this, "sex");

/**
* 用户名
*/
public final QueryColumn NAME = new QueryColumn(this, "name");

/**
* 邮箱
*/
public final QueryColumn EMAIL = new QueryColumn(this, "email");

/**
* 手机号
*/
public final QueryColumn PHONE = new QueryColumn(this, "phone");

/**
* 是否被封禁
*/
public final QueryColumn BANNED = new QueryColumn(this, "banned");

/**
* 出生日期
*/
public final QueryColumn BIRTHDAY = new QueryColumn(this, "birthday");

/**
* 是否被禁用
*/
public final QueryColumn DISABLED = new QueryColumn(this, "disabled");

/**
* 密码
*/
public final QueryColumn PASSWORD = new QueryColumn(this, "password");

/**
* 权限id
*/
public final QueryColumn ROLE_LEVEL = new QueryColumn(this, "role_level");

/**
* 创建时间
*/
public final QueryColumn CREATE_TIME = new QueryColumn(this, "create_time");

/**
* 所有字段。
*/
public final QueryColumn ALL_COLUMNS = new QueryColumn(this, "*");

/**
* 默认字段,不包含逻辑删除或者 large 等字段。
*/
public final QueryColumn[] DEFAULT_COLUMNS = new QueryColumn[]{ID, SEX, NAME, EMAIL, PHONE, BANNED, BIRTHDAY, DISABLED, PASSWORD, ROLE_LEVEL, CREATE_TIME};

public UserTableDef() {
super("", "sora_user");
}

}

最终我们在代码内只需要引入就可以使用了,注意在代码内使用的时候必须全部为大写;

同时要注意驼峰,如果原始类为User则使用USER;原始类为sora_user_log则使用SORA_USER_LOG

1
2
3
4
5
6
7
import static com.sora.domain.table.SoraUserLogTableDef.SORA_USER_LOG;
import static com.sora.domain.table.UserTableDef.USER;

// 使用USER
QueryWrapper.create().select(USER.ALL_COLUMNS).where(USER.NAME.like("33"))
// 使用soraUserLog
QueryWrapper.create().select(SORA_USER_LOG.ALL_COLUMNS).where(SORA_USER_LOG.NAME.like("33"))

查询

第一个查询示例

这里先写一组简单的示例,包含了where动态条件拼接、and与or的用法、分页查询

首先是查询的发起,直接使用QueryWrapper.create()后面拼接条件即可。下面我对每个条件进行解释说明

select:sql返回的列,这里我使用ALL_COLUMNS表示返回所有的列

where:在where内,我们可以直接使用字段名和值去进行匹配,也可以动态拼接,通过后面的when参数内的值决定是否拼接该条件,为true则拼接

and:和where用法一样,条件的拼接。和where一样,如果条件的值为null,则不会拼接该条件

or:对数据进行or匹配。注意or语句里面可以继续嵌套or,这样拼接出来的语句会带上括号。下面会展示不嵌套的sql帮助理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 选择数据源
@UseDataSource("sora01")
public Result select() {
QueryWrapper queryWrapper = QueryWrapper.create()
.select(USER.ALL_COLUMNS)
.where(USER.NAME.like("33"))
.and(USER.NAME.like("test").when(false))
.and(USER.BANNED.eq(null))
.or(USER.SEX.eq("1").or(USER.SEX.eq("0")));
// 发起带有分页的调用 查询第1页,每页2条内容
Page<User> paginate = userMapper.paginate(1, 2, queryWrapper);
// 发起普通的查询
List<User> userList = userMapper.selectListByQuery(queryWrapper);
System.out.println("分页查询结果:" + paginate);
System.out.println("普通查询结果:" + userList);
return Result.success(paginate);
}

拼接出的sql为

1
SELECT * FROM `sora_user` WHERE `name` LIKE '%33%' OR (`sex` = '1' OR `sex` = '0')

现在我把or语句的嵌套取消

1
2
3
4
5
6
7
QueryWrapper queryWrapper = QueryWrapper.create()
.select(USER.ALL_COLUMNS)
.where(USER.NAME.like("33"))
.and(USER.NAME.like("test").when(false))
.and(USER.BANNED.eq(null))
.or(USER.SEX.eq("1"))
.or(USER.SEX.eq("0"));

可以看到,取消嵌套的or是不带括号的

1
SELECT * FROM `sora_user` WHERE `name` LIKE '%33%' OR `sex` = '1' OR `sex` = '0'

分页的结果如下,获取到正确的总数及对应页数的数据

image-20240104160304015

普通查询则查询出所有数据

image-20240104160345807

第二个查询示例

第二个查询包含两表联查、自定义展示列、外键连接以及条件筛选过滤。因为两表联查返回的结果我们需要一个对象来接受,这里我就创建了一个SoraUserLogRes对象,同时创建对应的mapper,使用该对象来接受两表联查的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 选择数据源
@UseDataSource("sora01")
public Result select() {
QueryWrapper queryWrapper = QueryWrapper.create()
.select(USER.ID.as("id"),
USER.NAME.as("name"),
SORA_USER_LOG.LOG_TYPE.as("logType"),
SORA_USER_LOG.CLASS_PATH.as("classPath")
)
.from(USER)
.where(USER.NAME.eq("sora33"))
.innerJoin(SORA_USER_LOG)
.on(USER.ID.eq(SORA_USER_LOG.USER_ID));
List<SoraUserLogRes> userList = soraUserResMapper.selectListByQuery(queryWrapper);
return Result.success(userList);
}

我们直接看结果,成功接收到两表联查的结果,并且只有我们需要的四个列有值

image-20240104163135084

第三个查询示例

第三个查询主要展示聚合函数以及分组。这里按照性别分组,将聚合函数计算的结果使用AS重命名为email,这样就会把结果赋值到实体类的email字段上

1
2
3
4
5
6
7
8
9
// 选择数据源
@UseDataSource("sora01")
public Result select() {
QueryWrapper queryWrapper = QueryWrapper.create()
.select(USER.SEX, count(USER.NAME).as("email"))
.groupBy(USER.SEX);
List<User> userList = userMapper.selectListByQuery(queryWrapper);
return Result.success(userList);
}

可以得出性别为男的有1人,女的为2人

image-20240104164219879

链式查询

通过QueryChain并配置对应mapper即可进行链式查询

1
2
3
4
5
6
7
8
9
10
11
// 选择数据源
@UseDataSource("sora01")
public Result select() {
List<User> userList = QueryChain.of(userMapper)
.select(USER.ALL_COLUMNS)
.where(USER.NAME.like("33"))
.and(USER.BANNED.eq(null))
.and(USER.SEX.eq("1").or(USER.SEX.eq("0")))
.list();
return Result.success(userList);
}

新增

新增一条数据

具体代码如下,和plus一样,我们需要创建一个对象,之后调用insert方法插入即可(这里的insertSelective方法和insert区别在于insert不会忽略null,所以默认值会被覆盖掉,我们在创建表时如果表有默认值最好使用insertSelective方法来进行插入)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 选择数据源
@UseDataSource("sora01")
public Result insert() {
User user = new User();
user.setEmail("xxx@email.com");
user.setName("com");
user.setPassword("pwd");
user.setEmail("xxx@email.com");
user.setPhone("19935768765");
user.setId(IdUtil.getSnowflakeNextIdStr());
int insert = userMapper.insertSelective(user);
return Result.success(insert);
}

添加后的数据

image-20240105105838122

批量新增

新增数据我们可以调用insertBatch接口,并且可以控制每次新增的数据个数

这里我创建一个10w数据量的集合,并演示一次性插入和分10次插入的效率

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
// 选择数据源
@UseDataSource("sora01")
public Result insert() {
// 生成一个10w条记录的用户集合
List<User> userList = IntStream.range(0, 100000)
.mapToObj(i -> {
User user = new User();
user.setEmail("xxx@email.com");
user.setName("com" + i);
user.setSex("9");
user.setPassword("pwd");
user.setPhone("19935768765");
user.setEmail("xxx@email.com");
user.setId(IdUtil.getSnowflakeNextIdStr());
return user;
})
.toList();

StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 一次性插入
int insert = userMapper.insertBatch(userList);
// 分10次插入
int insertByTen = userMapper.insertBatch(userList,userList.size() / 10);
stopWatch.stop();
logger.info("添加[{}]条数据,耗时[{}]MS", userList.size(), stopWatch.getLastTaskTimeMillis());
return Result.success(insert);
}

首先来看一下一次性插入的时间,耗时46秒

image-20240105112802810

下面是分10次插入的时间,共执行了10次sql,耗时6秒,速度快了近8倍。所以对于数据量大的插入,分批插入效果会更好

image-20240105112859819

修改

第一种方式

我们需要先获取修改的对象,使用UpdateChain对象,通过链式调用设置好我们要修改的值,最后通过where设置条件,最后调用update方法完成修改。

setRaw和set的区别:setRaw是sql拼接,下面Birthday字段和email字段都是通过sql拼接赋值,而set相当于占位符赋值。例如sex和phone,phone这里我使用add方法,在原本phone的基础上+100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 选择数据源
@UseDataSource("sora01")
public Result update() {
QueryWrapper wrapper = QueryWrapper.create()
.where(USER.ID.eq("1743103286858469376"));
User user = userMapper.selectOneByQuery(wrapper);
boolean update = UpdateChain.of(user)
.setRaw(USER.BIRTHDAY, "now()")
.setRaw(USER.EMAIL, "CONCAT(email,'update')")
.set(USER.PHONE, USER.PHONE.add(100))
.set(USER.SEX, "1")
.where(USER.ID.eq("1743103286858469376"))
.update();
return Result.success(update);
}

第二种方式

和第一种方式相比,第二种方式只需要根据id,通过flex的UpdateEntity类创建对象即可进行修改

1
2
3
4
5
6
7
8
9
10
11
12
// 选择数据源
@UseDataSource("sora01")
public Result update() {
User user = UpdateEntity.of(User.class, "1743143044292800512");
UpdateWrapper.of(user)
.setRaw(USER.BIRTHDAY, "now()")
.setRaw(USER.EMAIL, "CONCAT(email,'update')")
.set(USER.PHONE, USER.PHONE.add(100))
.set(USER.SEX, "1");
int update = userMapper.update(user);
return Result.success(update);
}

修改后的数据:

image-20240105105920044

第三种方式

通过update方法,传入一个包含主键id的对象完成修改

1
2
3
4
5
6
7
8
// 选择数据源
@UseDataSource("sora01")
public Result update() {
User user = new User();
// 修改赋值逻辑……
int update = userMapper.update(user);
return Result.success(update);
}

删除

删除我们一般根据id删除,也可以自己配置条件,根据条件进行删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 选择数据源
@UseDataSource("sora01")
public Result delete() {
// 1 通过设置条件进行删除
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.where(USER.SEX.eq(2));
userMapper.deleteByQuery(queryWrapper);
// 2 直接通过id删除
userMapper.deleteById("1743103286858469376");
// 3 删除所有sex为0的数据
userMapper.deleteByCondition(USER.SEX.eq(0));
return Result.success();
}

批量删除

1
2
// 4 批量删除
userMapper.deleteBatchByIds(new ArrayList<>());

结束语

对flex的初步认识与使用到这里就结束了,当然flex还有很多强大的功能,这里贴上官网地址

mybatis-flex官网

QueryWrapper进阶