mybatis_plus整合到springboot使用

概述

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具。

https://baomidou.com/

https://baomidou.com/pages/24112f/

使用

准备sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `user`
(
id BIGINT NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);

INSERT INTO `user` (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

项目搭建

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-boot-mybatisplus-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-mybatisplus-test</name>
<description>spring-boot-mybatisplus-test</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.properties

1
2
3
4
5
6
7
8
9
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useSSL=true\
&useUnicode=true\
&characterEncoding=UTF-8\
&useServerPrepStmts=true\
&serverTimezone=Asia/Shanghai\
&zeroDateTimeBehavior=CONVERT_TO_NULL
spring.datasource.username=root
spring.datasource.password=123456

在idea中添加mybatisX插件:

https://baomidou.com/pages/ba5b24/

添加数据源-mysql- 选择数据库-在user表上右键点击 myabtisx-generator

选择模块

输入base package

点击下一步

annotaion使用 mybatisplus3

options 加上lombok

template使用mybatisplus3

点击finsh生成代码。

这样mapper 、 xml 、 service层的代码都有了。

添加@MapperScan注解扫描mapper:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@MapperScan("com.example.springbootmybatisplustest.mapper")
public class SpringBootMybatisplusTestApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisplusTestApplication.class, args);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/list")
public Map<String, Object> list() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功");
result.put("data", userService.list());
return result;
}

}

测试

http://localhost:8080/list

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
{
"msg": "成功",
"code": 200,
"data": [{
"id": 1,
"name": "Jone",
"age": 18,
"email": "test1@baomidou.com"
}, {
"id": 2,
"name": "Jack",
"age": 20,
"email": "test2@baomidou.com"
}, {
"id": 3,
"name": "Tom",
"age": 28,
"email": "test3@baomidou.com"
}, {
"id": 4,
"name": "Sandy",
"age": 21,
"email": "test4@baomidou.com"
}, {
"id": 5,
"name": "Billie",
"age": 24,
"email": "test5@baomidou.com"
}]
}

有条件的搜索

官网上说用条件构造器,能用还是能用的,但是还觉得代码与数据库字段没有隔离开,所以遇到有条件的搜索还是自定义sql比较好。

如果为了快,其实也无所谓了。

分页搜索

配置分页插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class MybatisPlusConfig {

/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//如果配置多个插件,切记分页最后添加
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
//interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}

无条件的分页搜索

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/page")
public Map<String, Object> page() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功");
Page<User> page = new Page<>();
page.setSize(2);
page.setCurrent(1);
result.put("data", userService.page(page));
return result;
}

有条件的分页搜索

https://baomidou.com/pages/97710a/

1
2
3
4
5
IPage<UserVo> selectPageVo(IPage<?> page, Integer state);
// or (class MyPage extends Ipage<UserVo>{ private Integer state; })
MyPage selectPageVo(MyPage page);
// or
List<UserVo> selectPageVo(IPage<UserVo> page, Integer state);
1
2
3
<select id="selectPageVo" resultType="xxx.xxx.xxx.UserVo">
SELECT id,name FROM user WHERE state=#{state}
</select>

如果返回类型是 IPage 则入参的 IPage 不能为null,因为 返回的IPage == 入参的IPage; 如果想临时不分页,可以在初始化IPage时size参数传 <0 的值;
如果返回类型是 List 则入参的 IPage 可以为 null(为 null 则不分页),但需要你手动 入参的IPage.setRecords(返回的 List);
如果 xml 需要从 page 里取值,需要 page.属性 获取。

生成 countSql 会在 left join的表不参与 where条件的情况下,把 left join优化掉。
所以建议任何带有 left join 的sql,都写标准sql,即给于表一个别名,字段也要 别名.字段。

实践

1
2
3
4
5
<select id="pageBySearch" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/>
from user
where name = #{name}
</select>
1
IPage<User> pageBySearch(IPage<User> page, @Param("name") String name);
1
public IPage<User> pageBySearch(Integer pageNum, Integer pageSize, String name);
1
2
3
4
5
6
7
@Override
public IPage<User> pageBySearch(Integer pageNum, Integer pageSize, String name) {
Page<User> page = new Page<>();
page.setCurrent(pageNum);
page.setSize(pageSize);
return getBaseMapper().pageBySearch(page, name);
}
1
2
3
4
5
6
7
8
@GetMapping("/pageBySearch")
public Map<String, Object> pageBySearch() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功");
result.put("data", userService.pageBySearch(1, 1, "test"));
return result;
}

主键生成器

使用mybatisplus的生成器

在domain实体的@TableId注解:

1
2
@TableId(type = IdType.ASSIGN_ID)
private Long id;
1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/save")
public Map<String, Object> save() {
User user = new User();
user.setName("test");
user.setAge(20);
user.setEmail("test@qq.com");
userService.save(user);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功");
return result;
}

http://localhost:8080/save

查看数据库中ID值。 更多的内容还是看官网吧

自定义ID生成器

https://baomidou.com/pages/568eb2/

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* 雪花算法的组成:
* 1. 1 个 bit 0
* 2. 41 个 bit,表示时间戳
* 3. 5 个 bit,表示机房 id
* 4. 5 个 bit,表示机器 id
* 5. 12 个 bit,表示序号,就是某个机房某台机器上的这一毫秒内同时生成的 id 的序号。
*
*
*
*/
public class SnowFlake {

/**
* 起始的时间戳
*/
private final static long START_STMP = 1480166465631L;

/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5;//数据中心占用的位数



/**
* 每一部分的最大值
*/
/** 数据中心数量 -- 用位运算计算出最大支持的数据中心数量: 31 */
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
/** 机器数量 -- 用位运算计算出最大支持的机器数量: 31 */
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
/** 序列号数量 -- 用位运算计算出 12 位能存储的最大正整数: 4095 */
/*-1L == 1111 1111
-1L << 12 也就是: 1111 1111 0000 0000 0000 0000

-1L 异或运算
0^0 和 1^1=0
1^0 和 0^1=1

1111 1111 0000 0000 0000 0000
A
1111 1111 1111 1111 1111 1111
------------------------------------------------------
0000 0000 1111 1111 1111 1111 换算成十进制:4095*/
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);



/**
* 每一部分向左的位移
*/
/** 机器标识较序列号的偏移量 12位 */
private final static long MACHINE_LEFT = SEQUENCE_BIT;
/** 数据中心较机器标识的偏移量 17位 */
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
/** 时间戳较数据中心的偏移量 */
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

private long datacenterId; //数据中心
private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳

public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);

// << :位移运算服, <<左移运算,>>右移运算,还有不带符号的位移运算 >>>
// 1 << 12:首先把1转为二进制数字 0000 0000 0000 0000 0000 0000 0000 0001
// 然后将上面的二进制数字向左移动 12 位后面补 0 得到 0010 0000 0000 0000 0000 0000 0000 0000
// 最后将得到的二进制数字转回对应类型的十进制
System.out.println(1 << 12);
for (int i = 0; i < (1 << 12); i++) {
System.out.println(snowFlake.nextId());
}

}

/**
* @param datacenterId
* @param machineId
*/
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0(数据中心ID 不能大于 MAX_DATACENTER_NUM 或小于 0)");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0(机器ID 不能大于 MAX_MACHINE_NUM 或小于 0)");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}

/**
* 产生下一个ID
*/
public synchronized long nextId() {
// 获取当前的时间戳
long currStmp = getNewstmp();

// 如果当前时间戳小于上次时间戳,则抛出异常
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id(时钟回拨。拒绝生成 id)");
}

// 如果当前时间戳等于上次的时间戳,说明是在同一毫秒内调用的雪花算法
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
/* & 算法:1 & 1 =1,其余况都返回 0 (相同为1,其他为0)
4095:0000 0000 1111 1111 1111 1111
&
4096:0000 0001 0000 0000 0000 0000
------------------------------------
等于:0000 0000 0000 0000 0000 0000
*/
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大 4095
if (sequence == 0L) {
// 获取下一毫秒的时间戳给到当前时间戳
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}

// 当前时间戳存档记录,用于下次产生 id 时对比,是否为相同时间戳
lastStmp = currStmp;

return (currStmp - START_STMP) << TIMESTMP_LEFT // 时间戳部分 22
| datacenterId << DATACENTER_LEFT // 数据中心部分 17
| machineId << MACHINE_LEFT // 机器标识部分 12
| sequence; // 序列号部分
}

/**
* @return 获取当前时间戳
*/
private long getNewstmp() {
return System.currentTimeMillis();
}


/**
* @return 获取下一时间的时间戳
*/
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}


}
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Slf4j
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
SnowFlake snowFlake = new SnowFlake(2, 3);
long id = snowFlake.nextId();
log.info("生成的ID{}", id);
//返回生成的id值即可.
return id;
}
}

同样User实体的ID字段

1
2
@TableId(type = IdType.ASSIGN_ID)
private Long id;

测试controller不变。

发现数据成功保存,并打印生成ID的日志。

逻辑删除插件

添加数据库表的字段:

1
2
ALTER TABLE `user` 
ADD COLUMN `deleted` int(0) DEFAULT(0) COMMENT '逻辑删除字段' AFTER `email`;

重新生成mapper、xml。

全局配置

1
2
3
4
5
6
# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以不domain实体类的属性上加@TableLogic)
mybatis-plus.global-config.db-config.logic-delete-field=deleted
# 逻辑已删除值(默认为 1)
mybatis-plus.global-config.db-config.logic-delete-value=1
# 逻辑未删除值(默认为 0)
mybatis-plus.global-config.db-config.logic-not-delete-value=0

每个表字段不一致

1
2
3
4
5
   /**
* 逻辑删除字段
*/
@TableLogic
private Integer deleted;

测试

1
2
3
4
5
6
7
8
@GetMapping("/delete")
public Map<String, Object> delete() {
userService.removeById(1L);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功");
return result;
}

发现如果字段一开始为null不能逻辑删除。默认值为0可以逻辑删除。

乐观锁插件

1
2
ALTER TABLE `user` 
ADD COLUMN `version` int(0) NULL DEFAULT 0 COMMENT '版本号' AFTER `deleted`;

相应的sql与实体要改或者生成。

1
2
@Version
private Integer version;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MybatisPlusConfig {

/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
//如果配置多个插件,切记分页最后添加
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
//interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/updateByVersion")
public Map<String, Object> updateByVersion() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "成功");
User user = new User();
user.setId(1L);
user.setName("test");
user.setVersion(1);
result.put("data", userService.updateById(user));
return result;
}

乐观锁实现方式:

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败

mybatisplus乐观锁插件不支持null。必须要转version字段。

说明:

  • 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整数类型下 newVersion = oldVersion + 1
  • newVersion 会回写到 entity
  • 仅支持 updateById(id)update(entity, wrapper) 方法
  • update(entity, wrapper) 方法下, wrapper 不能复用!!!

通用枚举

感觉上也不好用,因为一会儿返回是数值类型,一会儿返回是字符串类型,这时前端会不会懵了。回显再修改提交。

生成器

如果idea不支持database及mybatisx。(比如mac版本的)

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.4</version>
</dependency>

<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
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 com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.sql.Types;
import java.util.Collections;

public class MybatisPlusGenerator {

public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/test", "root", "root123456")
.globalConfig(builder -> {
builder.author("hqc") // 设置作者
//.enableSwagger() // 开启 swagger 模式
.fileOverride()
.outputDir("/Users/qichuanhan/Documents/ideaworkspace/rocketmq-test/src/main/java"); // 指定输出目录
})
.dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
// 自定义类型转换
return DbColumnType.INTEGER;
}
return typeRegistry.getColumnType(metaInfo);

}))
.packageConfig(builder -> {
builder.parent("com.example.rocketmqtest") // 设置父包名
//.moduleName("rocketmq-test") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "/Users/qichuanhan/Documents/ideaworkspace/rocketmq-test/src/main/resources/mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("user"); // 设置需要生成的表名
//.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}

}

运行main方法。


mybatis_plus整合到springboot使用
http://hanqichuan.com/2023/11/02/mybatis/mybatis_plus整合到springboot使用/
作者
韩启川
发布于
2023年11月2日
许可协议