封面画师:T5-茨舞(微博)     封面ID:69983911

1. MyBatis-Plus概述

1.1 为什么要学习?

Hibernate是一种全自动化的ORM框架,但是MyBatis不是,人们认为MyBatis属于半自动化的ORM框架,因为需要手写SQL语句。针对复杂的SQL进行手写倒也还好,但是一些简单的SQL还要手写就变得十分麻烦(我知道有注解,闭嘴!),同时还会让XML映射文件变得十分臃肿,那么有没有方法可以省略简单SQL的书写呢?

这时候,一个“天降猛男”登场了——MyBatis-Plus。💪

MyBatis-Plus可以很好地帮我们简化操作,对于一些简单的CRUD操作,不再需要我们编写。当然,除了这个最“肤浅”的作用外,MyBatis-Plus还有很多其他的特性,总之就是一句话:为了简化操作,为了偷懒。😂

MyBatis-Plus由国人开发,文档很详细,编码符合国人习惯,荣获【2019年度开源中国最受欢迎软件,开发工具类 TOP1】,还能简化开发,所以…不学一个?

官方文档:官网链接1 或者 官网链接2

PS:本文中,MP是MyBatis-Plus的简写。

2.2 MyBatis-Plus概述

官网:MyBatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

MyBatis-Plus概述

基友搭配,效率翻倍! 😆

1.3 特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求 ( 言外之意,简单的CRUD操作不再需要我们书写
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

2. 快速开始

官方快速开始地址:MyBatis-Plus-QuickStart

拓展使用第三方组件的步骤:

  • 导入对应的依赖
  • 编写对应的配置
  • 研究代码的编写
  • 提高技术的拓展

步骤

1、创建一个数据库mybatis_plus

2、创建user表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DROP TABLE IF EXISTS user;

CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
-- 在正式的开发中,version(乐观锁)、deleted(逻辑删除)、gmt_created、gmt_modified
DELETE FROM user;

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');

创建user表

3、使用IDEA创建一个SpringBoot项目mybatis_plus,选择web和lombok依赖进行导入。

4、导入其他依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<!-- 3.0.3版本移除 对 mybatis-plus-generator 包(代码生成器使用)的依赖,自己按需引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>Latest Version</version>
</dependency>

说明:使用MyBatis-Plus可以节省代码的编写,尽量 不要同时 导入MyBatis-Plus和MyBatis,避免存在依赖错误。

5、连接数据库

1
2
3
4
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

没使用MyBatis-Plus时,我们需要编写pojo、dao、service、controller,但是使用了Mybatis-Plus后,就不需要这么麻烦了。

6、使用Mybatis-Plus后:

  • pojo
1
2
3
4
5
6
7
8
9
10
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

private Long id; // 对应数据库的主键
private String name;
private Integer age;
private String email;
}
  • mapper接口
1
2
3
4
5
// 使用Mybatis-Plus后,只需要在对应的Mapper上继承BaseMapper即可
@Repository // 表示持久层
public interface UserMapper extends BaseMapper<User> {
// 这个时候,简单的CURD已经编写完成了
}

注意:编写完mapper接口后,还需要在主启动类上扫描mapper文件夹: @MapperScan("com.yang.mapper")

  • 编写测试类进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class MybatisPlusApplicationTests {

@Autowired
private UserMapper userMapper;

@Test
void contextLoads() {
// 查询全部用户
// selectList的参数是Wrapper,这是一个条件构造器
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}

}

查询结果:

查询全部用户

思考

  • 我们没有编写SQL,是怎么查询出来的? MyBatis-Plus已经帮我们写好了
  • SQL操作的方法又是拿来的? MyBatis-Plus已经帮我们写好了

3. 配置日志

使用MyBatis-Plus后,部分SQL是不可见的,我们希望知道它是如何执行的,这个时候就需要查看日志!

在配置文件中进行配置:

1
2
3
# MyBatis-Plus配置日志
# 使用默认控制台输出,不需要导入依赖
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
控制台日志

配置日志后,在以后的学习中,我们可以查看日志,观察MyBatis-Plus的SQL执行。

4. 数据基本操作

所有操作,均在测试类中进行。

4.1 数据插入

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testInsert(){
User user = new User();
// 注意:这里没有插入id
user.setName("默烦");
user.setAge(18);
user.setEmail("cy.mofan@qq.com");

int result = userMapper.insert(user);
// 受影响的行数
System.out.println(result);
System.out.println(user);
}

然后运行测试:

数据插入

我们发现:我们明明没有设置ID,插入时居然自动生成了ID,而且生成的ID似乎有点“ 奇怪 ”。

数据库插入的ID应该是全局唯一的,这个时候我们需要了解一些主键生成策略。👇

4.2 主键生成策略

可以使用UUID、自增ID、雪花算法、Redis、ZK等方式来生成主键。

默认 ID_WORKER 全局唯一ID

参考链接:分布式系统唯一ID生成方案汇总

MyBatis-Plus默认采用的使用推特的雪花算法

雪花算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。


我们可以前往我们的User实体类,然后在id属性上加一个注解:@TableId,点击进入这个注解:

1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface TableId {
String value() default "";

IdType type() default IdType.NONE;
}

点击并进入Idtype

1
2
3
4
5
6
7
8
9
10
11
12
public enum IdType {
AUTO(0), // 主键自增
NONE(1), // 未设置主键
INPUT(2), // 手动输入
ID_WORKER(3), // 默认,雪花算法
UUID(4), // UUID
ID_WORKER_STR(5); // ID_WORKER的字符串表示法

private int key;
private IdType(int key) {this.key = key;}
public int getKey() {return this.key;}
}

我们发现这是一个枚举类,其中的ID_WORKER就是默认生成策略。

主键自增

使用MyBatis-Plus,设置组件生成策略为 自增 的方法:

1、实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO)

2、 数据库字段一定要是自增的

数据库主键设置为自增

3、注解添加完成、数据库字段修改(记得保存)都完成后,我们在此运行插入测试:

自增主键测试

手动输入

使用MyBatis-Plus,设置组件生成策略为 手动输入 的方法:

只需要在实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO)即可。

如果我们设置组件生成策略为 手动输入 ,但是没有输入,这个时候日志就会显示插入的是null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testInsert(){
User user = new User();
// user.setName("默烦");
user.setId(6L);
user.setName("mofan");
user.setAge(18);
user.setEmail("cy.mofan@qq.com");

int result = userMapper.insert(user);
// 受影响的行数
System.out.println(result);
System.out.println(user);
}

运行结果:

手动输入主键测试

4.3 数据更新

更新单个数据

根据ID更新单个数据:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testUpdate(){
User user = new User();
user.setId(6L);
user.setName("Yang");

// updateById 的参数类型是对象!
int result = userMapper.updateById(user);
System.out.println(result);
System.out.println(user);
}

控制台日志打印:

根据主键更新单个数据

数据库显示:

更新单个数据数据库结果

更新多个数据

根据ID更新多个数据:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testUpdate(){
User user = new User();
user.setId(6L);
user.setName("mofan");
user.setAge(20);

int result = userMapper.updateById(user);
System.out.println(result);
System.out.println(user);
}

控制台日志打印:

根据主键更新多个数据

数据库显示:

更新多个数据数据库结果

MP(以下 MyBatis-Plus 简写为 MP)会自动根据给对象设置的属性值来更新数据。

更新字段值为 null

有两种可供方式可供参考:

  1. 使用 LambdaUpdateWrapper
  2. 使用 UpdateWrapper

现有如下的实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
@Setter
@TableName("tb_test_null")
public class TestNull extends BaseEntity {
@Serial
private static final long serialVersionUID = -1332169884425770306L;

@TableField("name")
private String name;

@TableField("age")
private Integer age;

@TableField("empty_field")
private String emptyField;
}

先插入一条数据:

1
2
3
4
5
6
7
8
9
10
private static final String UNIQUE_NAME = "mofan";

@BeforeEach
public void init() {
TestNull testNull = new TestNull();
testNull.setName(UNIQUE_NAME);
testNull.setAge(21);
testNull.setEmptyField("");
testNullDao.insert(testNull);
}

然后尝试将 ageemptyField 值更新为 null

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
private List<TestNull> getMofanTestNull() {
LambdaQueryWrapper<TestNull> wrapper = Wrappers.lambdaQuery(TestNull.class)
.eq(TestNull::getName, UNIQUE_NAME);
return testNullDao.selectList(wrapper);
}

@Test
public void testLambdaWrapperUpdateNull() {
Optional<TestNull> optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();

var wrapper = Wrappers.lambdaUpdate(TestNull.class)
.set(TestNull::getEmptyField, null)
.set(TestNull::getAge, null);

optional.ifPresent(i -> testNullDao.update(i, wrapper));

optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();

optional.ifPresent(
entity -> assertThat(entity)
.extracting(TestNull::getEmptyField, TestNull::getAge)
.containsOnlyNulls()
);
}

@Test
public void testWrapperUpdateNull() {
Optional<TestNull> optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();
if (optional.isEmpty()) {
Assertions.fail();
}

TestNull entity = optional.get();
var wrapper = new UpdateWrapper<>(entity);
wrapper.set("empty_field", null).set("age", null);
testNullDao.update(entity, wrapper);

optional = getMofanTestNull().stream()
.filter(i -> UNIQUE_NAME.equals(i.getName()))
.findFirst();
optional.ifPresent(
i -> assertThat(i)
.extracting(TestNull::getEmptyField, TestNull::getAge)
.containsOnlyNulls()
);
}

4.4 自动填充

创建时间、更新时间,对于这两个字段的操作我们希望是自动完成而不是需要手动编写。

阿里巴巴开发手册说过:任何数据库表都必须有两个字段:gmt_created(创建时间)、gmt_modified(修改时间),而且需要这两个字段的更新自动化。

自动填充有两种方式:数据库级别、代码级别

数据库级别(实际开发不允许)

1、在表中新增两个字段create_timeupdate_time,并将这两个字段类型设置为timestamp,开启根据当前时间戳更新:

数据库新增两个字段

2、同步实体类:

1
2
3
// 数据库使用下划线,实体类使用驼峰命名,依旧可以进行匹配
private Date createTime;
private Date updateTime;

3、执行更新测试操作:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testUpdate(){
User user = new User();
user.setId(6L);
user.setName("mofan");
user.setAge(19);

int result = userMapper.updateById(user);
System.out.println(result);
System.out.println(user);
}

4、查看结果:

数据库级别自动填充结果

代码级别

1、由于前面的测试,我们需要移除数据库的默认值和更新操作:

移除数据库级别的自动填充

2、实体类的属性上增加注解

1
2
3
4
5
// 字段填充内容
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

3、编写处理器来处理这些注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Component // 处理器添加到容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("开始进行插入...");
// setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject)
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}

// 更新的填充策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("开始进行更新...");
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}

4、测试插入:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testInsert(){
User user = new User();
user.setName("mofan");
user.setAge(17);
user.setEmail("cy.mofan@qq.com");

int result = userMapper.insert(user); // 自动生成ID
System.out.println(result);
System.out.println(user);
}

5、观察插入数据:

代码自动填充结果

4.5 乐观锁

与乐观锁相对的有:悲观锁

乐观锁:一个“乐观”的锁,它总是认为不会出问题,无论干什么都不会加锁。如果出了问题,再次更新值测试

悲观锁:一个“悲观”的锁,它总是认为会出现问题,无论干什么都会加上锁,然后再去操作!

在这主要讲解 乐观锁 机制。

乐观锁插件的适用场景

官方解释:乐观锁插件

意图:

当要更新一条记录的时候,希望这条记录没有被别人更新

乐观锁实现方式:

  • 取出记录时,获取当前version
  • 更新时,带上这个version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果version不对,就更新失败
1
2
3
4
5
-- 假设有两个线程A、B同时修改一条记录
update user set name = "mofan", version = version + 1
where id = 6 and version = 1
-- 但是B抢先完成了修改,version修改成2
-- 这个时候A就不能修改数据了,实现线程通信的安全

测试MP的乐观锁插件

1、给数据库增加version字段,并设置默认值为1

添加version字段

2、给实体类增加相应的字段,并添加注解@Version

1
2
@Version
private Integer version;

3、注册组件

1
2
3
4
5
6
7
8
9
10
11
12
@EnableTransactionManagement    // 默认已添加该注解
@Configuration
// 移动主配置文件下扫描mapper文件夹的注解(非必须,仅仅为了美观,与乐观锁配置无关)
@MapperScan("com.yang.mapper")
public class MyBatisPlusConfig {

// 注册乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}

4、编写测试方法进行测试:

1
2
3
4
5
6
7
8
9
10
11
// 测试乐观锁成功
@Test
public void testOptimisticLocker(){
// 查询一个用户
User user = userMapper.selectById(1l);
// 修改信息
user.setName("mofan");
user.setEmail("cy.mofan@qq.com");
// 更新信息
userMapper.updateById(user);
}

数据库结果:

乐观锁修改成功


我们再来测试一下修改失败的情况,主要模拟多线程情况下线程插队的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 测试乐观锁失败 多线程下
@Test
public void testOptimisticLocker2(){
// 查询一个用户
User user = userMapper.selectById(1l);
// 修改信息
user.setName("mofan111");
user.setEmail("cy.mofan@qq.com");

// 模拟另一个线程插队操作
User user2 = userMapper.selectById(1l);
// 修改信息
user2.setName("mofan222");
user2.setEmail("cy.mofan@qq.com");
userMapper.updateById(user2);

// 自旋锁来尝试多次提交
userMapper.updateById(user); // 如果没乐观锁,就会覆盖插队线程值
}

如果没有乐观锁,最后一次的修改会覆盖前一次的修改,但是有了乐观锁就不会出现这种情况了。查看数据库修改结果:

乐观锁修改失败

查看数据库后,结果与我们预期一样。

4.6 数据查询

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
// 根据id查询单个用户
@Test
public void testSelectById(){
User user = userMapper.selectById(1l);
System.out.println(user);
}
// 根据id查询多个用户
@Test
public void testSelectBatchId(){
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}

// 条件查询之一 使用map进行操作
@Test
public void testSelectByBatchIds(){
HashMap<String, Object> map = new HashMap<>();
// 自定义查询
map.put("name","默烦");
map.put("age","18");

// 查询name = "默烦",age=18的所有用户
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}

4.7 数据分页查询

分页查询的方法有很多,比如:

1、原始的 limit 进行分页

2、第三方插件进行分页,比如:PageHelper

3、MP内置的分页插件

使用MP内置的分页插件

1、配置拦截器组件

官方配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Spring boot方式
@EnableTransactionManagement
@Configuration
// 指明需要被扫描的 Mapper 接口文件
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {

@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}

我测试用的配置:

1
2
3
4
5
6
7
8
9
10
// 分页组件
@Bean
public PaginationInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
final PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 单页分页条数限制
innerInterceptor.setMaxLimit(150L);
interceptor.addInnerInterceptor(innerInterceptor);
return interceptor;
}

2、直接使用Page对象即可

1
2
3
4
5
6
7
8
9
// 测试分页查询
@Test
public void testPage(){
// 当前页:2 页面数据量:5条
Page<User> page = new Page<>(2,5);
userMapper.selectPage(page, null);
System.out.println("总记录数为:"+page.getTotal());
page.getRecords().forEach(System.out::println);
}

控制台输出:

分页查询控制台输出

数据库数据对比:

分页查询数据库数据对比

4.8 数据删除

三种基本的删除:删除单个数据、删除多个数据、根据条件删除。

删除单个数据

1
2
3
4
5
// 删除单个数据
@Test
public void testDeleteById(){
userMapper.deleteById(1272430143247347718L);
}

删除数据前的数据库:参考分页查询最后一张数据库示例图。

删除数据后的数据库:

删除单个数据

删除多个数据

1
2
3
4
5
// 删除多个数据
@Test
public void testDeleteBatchById(){
userMapper.deleteBatchIds(Arrays.asList(1272430143247347715L,1272430143247347716L));
}

删除数据后的数据库:

删除多个数据

根据条件删除

1
2
3
4
5
6
7
// 根据条件删除
@Test
public void testDeleteMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("name","默烦");
userMapper.deleteByMap(map);
}

删除数据后的数据库:

条件删除数据

4.9 逻辑删除

物理删除:从数据库中直接移除数据。

逻辑删除:数据没有从数据库中移除,只是打了个标志让用户看不到。(学过HBase可以参考HBase的删除逻辑)

应用场景:管理员可以查看被删除的数据。

测试逻辑删除

1、在数据表中增加逻辑删除字段delete,默认值为0,表示未被删除。

数据库增加逻辑删除字段

如果在数据库级别没有设置默认值,那么需要添加自动填充,如:

1
2
3
4
5
@Override
public void insertFill(MetaObject metaObject) {
...
this.setFieldValByName("deleted", 0, metaObject);
}

2、实体类中增加属性,并添加注解@TableLogic

1
2
3
4
// 自动填充时使用
// @TableField(fill = FieldFill.INSERT, value = "deleted")
@TableLogic
private Integer deleted;

3、编写配置文件

1
2
3
4
5
6
7
mybatis-plus:
global-config:
# 配置逻辑删除
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

4、进行测试

1
2
3
4
@Test
public void testDeleteById(){
userMapper.deleteById(1L);
}

控制台输出信息:

逻辑删除控制台输出

数据表信息:

逻辑删除数据库数据

我们在进行查询,看看能够查询到被逻辑删除的数据:

1
2
3
4
5
@Test
public void testSelectById(){
User user = userMapper.selectById(1l);
System.out.println(user);
}

控制台打印信息:

逻辑删除后查询数据

我们发现,引进逻辑删除后再进行查询会自动过滤被逻辑删除的数据。

5. 条件构造器

官方参考文档:条件构造器

5.1 QueryWrapper

我们写一些复杂的SQL时,就可以用它来替代!

前置:创建一个测试类WrapperTest

1
2
3
4
5
6
7
@SpringBootTest
public class WrapperTest {

@Autowired
private UserMapper userMapper;
// ...
}

当前数据表数据:

wrapper_当前数据表信息

查询name不为空、email不为空、年龄大于等于12的用户

1
2
3
4
5
6
7
8
9
10
@Test
void contextLoads() {
// 查询name不为空、email不为空、年龄大于等于12的用户
// 与map对比学习
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age",12);
userMapper.selectList(wrapper).forEach(System.out::println);
}
wrapper_条件查询所有数据

查询name是默烦的一个用户

1
2
3
4
5
6
7
8
9
@Test
void test2(){
// 查询name是默烦的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name","默烦");
// 查询一个数据
User user = userMapper.selectOne(wrapper);
System.out.println(user);
}
wrapper_条件查询单个数据

查询age范围是 20 ~ 30 的用户

1
2
3
4
5
6
7
8
@Test
void test3(){
// 查询age范围是 20 ~ 30 的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age",20,30);
Integer count = userMapper.selectCount(wrapper);
System.out.println("用户数量为"+count);
}

wrapper_范围查询

模糊查询

1
2
3
4
5
6
7
8
9
10
// 模糊查询
@Test
void test4(){
QueryWrapper<User> wrapper = new QueryWrapper<>();

wrapper.notLike("name","e") // name中不包含e的用户
.likeRight("email","t"); // email以t开头的用户
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}
wrapper_模糊查询

嵌套查询

1
2
3
4
5
6
7
8
@Test
void test5(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// id 在子查询出来
wrapper.inSql("id","select id from user where id < 3");
List<Object> objects = userMapper.selectObjs(wrapper);
objects.forEach(System.out::println);
}
wrapper_嵌套查询

通过id进行降序排序

1
2
3
4
5
6
7
8
@Test
void test6(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 通过id进行降序排序
wrapper.orderByDesc("id");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
wrapper_数据排序

5.2 LambdaQueryWrapper

在使用 QueryWrapper 时查询字段都是魔法值,都是由开发人员编写的列名。

如果需要重构代码,使用这种方式将十分麻烦,因此在 MP 中有 LambdaQueryWrapper 来解决这个问题。

比如,现将下述代码进行改写:

1
2
3
4
5
6
7
8
9
10
@Test
void contextLoads() {
// 查询name不为空、email不为空、年龄大于等于12的用户
// 与map对比学习
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age",12);
userMapper.selectList(wrapper).forEach(System.out::println);
}

改写后的代码:

1
2
3
4
5
6
7
@Test
public void testLambdaQueryWrapper() {
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery().isNotNull(User::getName)
.isNotNull(User::getEmail)
.ge(User::getAge, 12);
userMapper.selectList(wrapper).forEach(System.out::println);
}

对于 QueryWrapper 对象的获取采用 new 关键字,而 LambdaQueryWrapper 对象的获取则习惯采用 Wrappers 的静态方法获取。

注意: 在使用 MP 的条件构造器时,请注意 and() 方法的使用规则,并区分与 or() 方法的区别。

需要注意的是,在低版本的 MP 中,可能并没有 Wrappers 类(请以参考文档为准)。

6. 代码生成器

官方参考文档:代码生成器

首先导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- mybatis-plus -->
<!--3.0.7版本移除 对 mybatis-plus-generator 包的依赖,自己按需引入,尽在还需要导入模板依赖-->
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>Latest Version</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</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
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
package com.yang;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

import javax.xml.stream.FactoryConfigurationError;
import java.util.ArrayList;
/**
* @author 默烦
* @date 2020/6/16
*/
// 代码自动生成器
public class MofanCode {
public static void main(String[] args) {
// 构建一个 代码自动生成器 对象
AutoGenerator mpg = new AutoGenerator();
/************配置策略***********/
// 1. 全局配置
// 注意:包别导错了
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dor"); // 获取当前系统目录
gc.setOutputDir(projectPath+"/src/main/java"); // 代码输出目录
gc.setAuthor("默烦");
gc.setOpen(false); // 是否打开资源文件夹
gc.setFileOverride(false); // 是否覆盖原有文件
gc.setServiceName("%sService"); // 去默认Service的I前缀
gc.setIdType(IdType.ID_WORKER); // 主键生成策略
gc.setDateType(DateType.ONLY_DATE); // 日期类型
gc.setSwagger2(true); // 自动配置Swagger文档
mpg.setGlobalConfig(gc);

// 2. 设置数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);

// 3. 包的配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("blog"); // 模块名
pc.setParent("com.yang");
pc.setEntity("pojo");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);

// 4. 策略配置
StrategyConfig strategy = new StrategyConfig();
/*************重点修改************/
strategy.setInclude("user","tags"); // 设置映射的表名
/*************重点修改************/
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true); // 自动生成lombok
strategy.setLogicDeleteFieldName("deleted"); // 设置逻辑删除字段
// 自动填充策略
TableFill create_time = new TableFill("create_time", FieldFill.INSERT);
TableFill update_time = new TableFill("update_time", FieldFill.INSERT_UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(create_time);
tableFills.add(update_time);
strategy.setTableFillList(tableFills);
// 乐观锁设置
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);
// url 设置 localhost:8080/hello_mofan_1
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);

mpg.execute(); // 执行
}
}

以上代码按要求修改即可使用。

7. 拓展批量处理

7.1 批量插入

注意: 拓展的批量插入只适用于 MySQL 数据库。

在 MyBatis-Plus 中存在现成的批量插入方法,我们只需进行简单的拓展即可使用:

1、自定义 Injector 并继承 DefaultSqlInjector 类;

2、重写 getMethodList() 方法,添加现成的批量插入类,并注入 MyBatis-Plus配置类中;

3、最后拓展通用 Mapper 即可。

自定义 Injector 并重写 getMethodList() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mofan
* @date 2021/3/14 21:54
* 支持自定义数据方法注入
*/
public class EasySqlInjector extends DefaultSqlInjector {

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 防止父类的方法不可使用
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}

在 MyBatis-Plus 配置类中注入自定义 Injector:

1
2
3
4
@Bean
public EasySqlInjector easySqlInjector() {
return new EasySqlInjector();
}

拓展通用 Mapper,后续的 Mapper 接口不再继承 BaseMapper,而是继承拓展的通用 Mapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan
* @date 2021/3/14 21:57
* 拓展通用 Mapper,支持数据批量插入
*/
public interface MybatisBaseMapper<T> extends BaseMapper<T> {

/**
* 批量插入 仅适用于 MySQL
* @param entityList 实体列表
* @return 插入的行数
*/
int insertBatchSomeColumn(List<T> entityList);
}

使用方法举例:

1
2
3
@Mapper
@Repository
public interface UserInfoDao extends MybatisBaseMapper<UserInfo> {}

7.2 批量更新

相较于简单的批量插入拓展,在拓展批量更新时需要我们自己编写批量更新模板方法,然后其他步骤就和批量插入的拓展类似。

首先在数据库连接上添加 allowMultiQueries=true 属性:

1
2
3
4
5
6
datasource:
username: xxx
password: xxx
# 项目中已拓展 Mybatis-plus 的批量更新,需要在连接信息中添加 allowMultiQueries=true 表示开启批量更新
url: jdbc:mysql:///xxx?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
driver-class-name: com.mysql.cj.jdbc.Driver

批量更新模板方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author mofan
* @date 2021/4/14 15:38
* 批量更新方法实现,条件为主键,选择性更新
*/
@Slf4j
public class UpdateBatchMethod extends AbstractMethod{
private static final long serialVersionUID = 1147163282820238330L;

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = "<script>\n<foreach collection=\"list\" item=\"item\" separator=\";\">\nupdate %s %s where %s=#{%s} %s\n</foreach>\n</script>";
String additional = tableInfo.isWithVersion() ? tableInfo.getVersionFieldInfo().getVersionOli("item", "item.") : "" + tableInfo.getLogicDeleteSql(true, true);
String setSql = sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "item", "item.");
String sqlResult = String.format(sql, tableInfo.getTableName(), setSql, tableInfo.getKeyColumn(), "item." + tableInfo.getKeyProperty(), additional);
// log.debug("sqlResult----->{}", sqlResult);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
// 第三个参数必须和MybatisBaseMapper的自定义方法名一致
return this.addUpdateMappedStatement(mapperClass, modelClass, "updateBatch", sqlSource);
}
}

在自定义的 Injector 添加拓展的批量更新模板类:

1
2
3
4
5
6
7
8
9
10
11
public class EasySqlInjector extends DefaultSqlInjector {

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 防止父类的方法不可使用
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new InsertBatchSomeColumn());
methodList.add(new UpdateBatchMethod());
return methodList;
}
}

最后拓展通用 Mapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface MybatisBaseMapper<T> extends BaseMapper<T> {

/**
* 批量插入 仅适用于 MySQL
* @param entityList 实体列表
* @return 插入的行数
*/
int insertBatchSomeColumn(List<T> entityList);

/**
* 自定义批量更新,条件为主键
* @param entityList 实体列表
* @return 是否成功
*/
int updateBatch(@Param("list") List<T> entityList);
}

批量更新使用注意事项

1、仅测试过 MySQL 数据库,对于其他数据库的支持尚不明确;

2、无法做到批量更新字段值为 NULL,针对这种情况请自行编写 XML,比如:

1
2
3
4
5
6
7
8
9
10
11
<!--  重置当年考试科目信息 (将 campus、classroomName 列设置为 null,used 列设置为 false)  -->
<update id="resetExamSubject">
<foreach collection="list" item="item" index="index" close="" open="" separator=";">
UPDATE tb_exam_subject
<set>
examination_location = NULL,
used = false
</set>
WHERE id = ${item.id} AND is_deleted = 0
</foreach>
</update>

8. 通用枚举

在 JDK 1.5 之后引入了枚举,如果要在 MP 中实现通用枚举也很简单,只需要配置枚举类的路径,然后使用注解即可。

为了规范枚举的使用,习惯让自定义枚举继承一个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author mofan
* 枚举信息,一般来说,每个枚举类都应该实现这个接口
*/
public interface EnumInfo extends Serializable {
/**
* 获取枚举值对应的代码
* @return 对应的代码
*/
String code();

/**
* 获取枚举值对应的信息
* @return 对应的信息
*/
String message();
}

假设有这样一个枚举:

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
/**
* @author mofan
* 专业类型
*/
public enum MajorTypeEnum implements EnumInfo {
/**
* 学术型
*/
ACADEMIC("Academic", "学术型"),

/**
* 专业学位
*/
PROFESSIONAL_DEGREE("ProfessionalDegree", "专业学位");

private String code;

private String message;

MajorTypeEnum(String code, String message) {
this.code = code;
this.message = message;
}

@Override
public String code() {
return this.code;
}

@Override
public String message() {
return this.message;
}
}

这个枚举类所在的包路径为:indi.mofan.common.enums

现在我想让这个枚举保存到数据库中的值是 code,序列化后得到的是 message。那么首先需要在配置文件中指定枚举所在的包路径:

3.5.2 版本开始,无需设置扫描的枚举包!

1
2
3
mybatis-plus: 
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
type-enums-package: indi.mofan.common.enums

然后在 code 属性上使用 @EnumValue 注解:

1
2
@EnumValue
private String code;

在 message 上使用 @JsonValue 注解即可:

1
2
@JsonValue
private String message;

9. 常用配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mybatis-plus:
# 映射文件位置
mapper-locations: classpath:indi/mofan/mybatis/*.xml
configuration:
# 映射驼峰(实体属性名)到下划线(列名)
map-underscore-to-camel-case: true
# MyBatis-Plus配置日志
# 使用默认控制台输出,不需要导入依赖
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
# 配置逻辑删除
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 设置实体类所对应的表的统一前缀
table-prefix: tb_
# 设置统一的主键生成策略
id-type: assign_id
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
type-enums-package: indi.mofan.common.enums

注意: 在 SpringBoot 项目中,我们常把 MyBatis 的 SQL 映射文件放在 resources 目录下。