MyBatis-Plus 入门教程之四

大纲

前言

版本说明

本文的教程内容是基于 MyBatis-Plus 3.5.2 版本编写的,若你使用的是 2.x 或其他版本,可能会有部分知识点、案例代码不兼容,一切以 MyBatis-Plus 官方文档为准。

MyBatis-Plus 逻辑删除

逻辑删除介绍

  • 只对自动注入的 SQL 生效

    • 插入:没有任何限制
    • 删除:会转变为 SQL 更新语句,例如:update user set deleted = 1 where id = 1 and deleted = 0
    • 查找:会追加 where 条件过滤掉已删除的数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段,例如:select id, name, deleted from user where deleted = 0
    • 更新:会追加 where 条件防止更新到已删除的数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段,例如:update user set age = 23 where id = 1 and deleted = 0
  • 字段类型支持说明

    • 支持所有数据类型(推荐使用 IntegerBooleanLocalDateTime 数据类型)
    • 如果数据库表字段使用 datetime,逻辑未删除值和已删除值支持配置为字符串 null,另一个值支持配置为函数来获取值(例如 now()

提示

  • 逻辑删除是为了方便数据恢复和保护数据本身价值的一种方案,但实际就是删除
  • 如果业务上需要频繁将数据查出来显示,那么就不应使用逻辑删除,而是应该以一个状态去表示数据记录的状态

逻辑删除使用案例

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-10

逻辑删除的配置步骤

1、在实体类的属性上添加 @TableLogic 注解
2、使用 Spring XML 配置文件或者 SpringBoot 配置文件的任意一种方式来配置 MyBatis-Plus 的逻辑删除

创建数据库表

1
2
3
4
5
6
7
8
9
10
-- 创建数据库表
CREATE TABLE `t_department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`deleted` int DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入表数据
insert into t_department(id, name, deleted) values(1, '开发部门', 0), (2, '测试部门', 0), (3, '产品部', 1);

添加 @TableLogic 注解

在实体类的属性上添加 @TableLogic 注解

1
2
3
4
5
6
7
8
9
10
11
12
public class Department {

private Long id;

private String name;

@TableLogic
private Integer deleted;

...

}

更改项目的配置文件

Spring XML 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="globalConfig" ref="globalConfig"></property>
</bean>

<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
<property name="dbConfig" ref="dbConfig" />
</bean>

<bean id="dbConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig">
<!-- 全局逻辑删除的实体属性名(since 3.3.0,配置后实体类属性可以不单独配置 @TableLogic 注解)-->
<!-- <property name="logicDeleteField" value="deleted" /> -->
<!-- 逻辑已删除值(默认为 1)-->
<property name="logicDeleteValue" value="1" />
<!-- 逻辑未删除值(默认为 0)-->
<property name="logicNotDeleteValue" value="0" />
</bean>
SpringBoot 配置文件
1
2
3
4
5
6
mybatis-plus:
global-config:
db-config:
# logic-delete-field: deleted # 全局逻辑删除的实体属性名(since 3.3.0,配置后实体类属性可以不单独配置 @TableLogic 注解)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Junit 单元测试代码

1
2
3
4
5
6
/**
* Mapper 接口
*/
public interface DepartmentMapper extends BaseMapper<Department> {

}
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
/**
* Junit 测试代码
*/
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private DepartmentMapper deptMapper;

@Test
public void select() {
List<Department> list = deptMapper.selectList(null);
list.forEach(System.out::println);
}

@Test
public void selectByWrapper() {
QueryWrapper<Department> wrapper = new QueryWrapper<>();
wrapper.like("name", "测试");
List<Department> list = deptMapper.selectList(wrapper);
list.forEach(System.out::println);
}

@Test
public void delete() {
int deleteResult = deptMapper.deleteById(1L);
System.out.println("deleteResult: " + (deleteResult > 0));
}

@Test
public void update() {
Department department = new Department();
department.setName("行政部");
department.setId(1L);
int updateResult = deptMapper.updateById(department);
System.out.println("updateResult: " + (updateResult > 0));
}

}

执行上面的测试代码后,控制台输出的日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
==>  Preparing: SELECT id,name,deleted FROM t_department WHERE deleted=0
==> Parameters:
<== Columns: id, name, deleted
<== Row: 1, 开发部门, 0
<== Row: 2, 测试部门, 0
<== Total: 2

==> Preparing: SELECT id,name,deleted FROM t_department WHERE deleted=0 AND (name LIKE ?)
==> Parameters: %测试%(String)
<== Columns: id, name, deleted
<== Row: 2, 测试部门, 0
<== Total: 1

==> Preparing: UPDATE t_department SET deleted=1 WHERE id=? AND deleted=0
==> Parameters: 1(Long)
<== Updates: 1

==> Preparing: UPDATE t_department SET name=? WHERE id=? AND deleted=0
==> Parameters: 行政部(String), 1(Long)
<== Updates: 0

逻辑删除常见问题

如何插入数据?

删除接口自动填充处理器失效

  • 1、使用 deleteById 方法(推荐)
  • 2、使用 update 方法,即 UpdateWrapper.set(column, value)(推荐)
  • 3、使用 update 方法,即 UpdateWrapper.setSql("column=value")
  • 4、使用 Sql 注入器 注入 com.baomidou.mybatisplus.extension.injector.methods.LogicDeleteByIdWithFill(3.5.0 版本已废弃,推荐使用 deleteById

MyBatis-Plus 通用枚举

通用枚举介绍

数据库表中有些字段的值是固定的,例如性别(男或女),此时可以使用 MyBatis-Plus 的通用枚举来实现,这样可以让 MyBatis 更优雅地使用枚举属性。

通用枚举使用案例

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-11

通用枚举的配置步骤

1、在枚举类的属性上添加 @EnumValue 注解,或者让枚举类实现 IEnum 接口
2、MyBatis-Plus 3.5.2 以下的版本,需要配置枚举包扫描(局部方式),或者更改 MyBatis 默认使用的 EnumTypeHandler 枚举类型处理器(全局方式)

创建数据库表

1
2
3
4
5
6
7
8
9
10
-- 创建数据库表
CREATE TABLE `t_admin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`type` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入表数据
insert into t_admin(id, name, type) values(1, 'Alex', 1);

添加 @EnumValue 注解

在枚举类的属性上添加 @EnumValue 注解,MyBatis-Plus 在插入数据时,会将拥有 @EnumValue 注解的属性的值保存到数据库表里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum AdminType {

ROOT(0, "Root"), ADMIN(1, "Admin");

@EnumValue
private Integer value;

private String description;

public Integer getValue() {
return this.value;
}

public String getDescription() {
return this.description;
}

private AdminType(Integer value, String description) {
this.value = value;
this.description = description;
}

}

或者让枚举类实现 IEnum 接口,然后重写 getValue() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum AdminType implements IEnum<Integer> {

ROOT(0, "Root"), ADMIN(1, "Admin");

private Integer value;

private String description;

@Override
public Integer getValue() {
return this.value;
}

public String getDescription() {
return this.description;
}

private AdminType(Integer value, String description) {
this.value = value;
this.description = description;
}

}

配置枚举包扫描

值得一提的是,从 MyBatis-Plus 3.5.2 版本开始,无需手动配置枚举包扫描。

局部方式

当使用以下的方式配置枚举包扫描后,MyBatis-Plus 提供的 MybatisSqlSessionFactoryBean 会自动扫描包内合法的枚举类(使用了 @EnumValue 注解或者实现了 IEnum 接口),分别为这些枚举类注册使用 MybatisEnumTypeHandler。换句话说,只有指定包下的枚举类会使用新的 TypeHandler。其他包下,或者包内没有做相关改造的枚举类,仍然会使用 MyBatis 默认提供的 DefaultEnumTypeHandler

Spring XML 配置文件
1
2
3
4
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<!-- 枚举包扫描,支持通配符 * 或者 ; 分割 -->
<property name="typeEnumsPackage" value="com.clay.mybatis.enums"/>
</bean>
SpringBoot 配置文件
1
2
3
mybatis-plus:
# 枚举包扫描,支持通配符 * 或者 ; 分割
typeEnumsPackage: com.clay.mybatis.enums
全局方式

此方式用来全局更改 MyBatis 默认使用的 EnumTypeHandler 枚举类型处理器。

Spring XML 配置文件
1
2
3
4
5
6
7
8
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="configuration" ref="configuration"/>
</bean>

<bean id="configuration" class="com.baomidou.mybatisplus.core.MybatisConfiguration">
<!-- 更改 MyBatis 的 DefaultEnumTypeHandler -->
<property name="defaultEnumTypeHandler" value="com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler">
</bean>
SpringBoot 配置文件
1
2
3
4
mybatis-plus:
# 更改 MyBatis 的 DefaultEnumTypeHandler
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
通过 MybatisPlusPropertiesCustomizer 自定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MybatisPlusConfig {

@Bean
public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
return properties -> {
GlobalConfig globalConfig = properties.getGlobalConfig();
globalConfig.setBanner(false);
MybatisConfiguration configuration = new MybatisConfiguration();
// 更改 MyBatis 的 DefaultEnumTypeHandler
configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
properties.setConfiguration(configuration);
};
}
}

Junit 单元测试代码

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
public class Admin {

private Long id;

private String name;

private AdminType type;

...

}
  • Mapper 接口
1
2
3
public interface AdminMapper extends BaseMapper<Admin> {

}
  • Junit 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private AdminMapper adminMapper;

@Test
public void insert() {
Admin admin = new Admin();
admin.setName("David");
admin.setType(AdminType.ROOT);
adminMapper.insert(admin);
System.out.println(admin);
}

@Test
public void select() {
Admin admin = adminMapper.selectById(1L);
System.out.println(admin);
}

}

执行上面的测试代码后,MyBatis-Plus 发出的 SQL 语句如下:

1
2
3
4
5
6
7
8
9
==>  Preparing: INSERT INTO t_admin ( name, type ) VALUES ( ?, ? )
==> Parameters: David(String), 0(Integer)
<== Updates: 1

==> Preparing: SELECT id,name,type FROM t_admin WHERE id=?
==> Parameters: 1(Long)
<== Columns: id, name, type
<== Row: 1, Alex, 1
<== Total: 1

序列化枚举值为前端返回值

MyBatis-Plus SQL 注入器

SQL 注入器介绍

Mybatis-Plus 的 SQL 注入器(SqlInjector)可以自定义各种 SQL 语句,并注入到 MyBatis 全局中,这相当于自定义 Mybatis-Plus 自动注入的方法(例如通用的 CRUD 方法)。之前需要在 SQL 映射文件中配置的 SQL 语句,现在可以通过扩展 SqlInjector 来加载到 MyBatis 环境。在 MyBatis-Plus 中自定义自己的通用方法,可以实现接口 ISqlInjector,也可以继承抽象类 AbstractSqlInjector,或者继承默认实现类 DefaultSqlInjector 来注入通用方法。SQL 注入器的作用是可以在 Mapper 层自定义通用方法,然后将 SQL 模板自动注入到 MyBatis-Plus 中,这样就可以不用在 SQL 映射文件中重复编写 SQL 语句。

注入器和自定义 Mapper 方法 + 编写 SQL 映射文件的区别

使用 SQL 注入器,通常是使用自定义 Mapper(如 CustomBaseMapper)接口继承 BaseMapper 接口,我们自定义的通用方法就写在 CustomBaseMapper 里。然后我们业务相关的 Mapper 继承 CustomBaseMapper 后,就可以直接使用自定义的通用方法了。这一切只需要我们自定义一个 SQL 模板,注入到 MyBatis-Plus 即可,而不是换一张表就得重新编写一次 SQL 映射文件。

SQL 注入器使用案例

提示

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-14

定义 Mapper 接口

定义 Mapper 接口(继承自 BaseMapper),新增自定义的通用方法,以后其他业务 Mapper 都可以继承自这个 CustomBaseMapper 接口

1
2
3
4
5
public interface CustomBaseMapper<T> extends BaseMapper<T> {

List<T> findAll();

}

定义方法类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FindAll extends AbstractMethod {

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 执行的 SQL 语句
String sql = "select * from " + tableInfo.getTableName();

// 方法名称,必须与 Mapper 接口中的方法名一致
String method = "findAll";

// 添加 MappedStatement(作用相当于编写 SQL 映射文件)
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
MappedStatement mappedStatement = this.addSelectMappedStatementForOther(mapperClass, method, sqlSource, modelClass);
return mappedStatement;
}

}

定义 SQL 注入器

定义 SQL 注入器,将自定义的通用方法注入到 MyBatis-Plus 全局中

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

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
// 获取父类中方法的集合
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
// 添加自定义的通用方法
methodList.add(new FindAll());
return methodList;
}

}

注入 SQL 注入器

Spring XML 配置文件
1
2
3
4
5
6
7
8
9
10
11
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="globalConfig" ref="globalConfig"></property>
</bean>

<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
<!-- 注入 SQL 注入器 -->
<property name="sqlInjector" ref="customSqlInjector" />
</bean>

<!-- 自定义的 SQL 注入器 -->
<bean id="customSqlInjector" class="com.clay.mybatis.injector.CustomSqlInjector" />
SpringBoot 配置类

定义 SpringBoot 配置类,将 SQL 注入器注入 Spring 容器中

1
2
3
4
5
6
7
8
9
10
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MybatisPlusConfig {

@Bean
public CustomSqlInjector customSqlInjector() {
return new CustomSqlInjector();
}

}

Junit 单元测试代码

  • 定义业务 Mapper 接口,继承自 CustomBaseMapper
1
2
3
public interface EmployeeMapper extends CustomBaseMapper<Employee> {

}
  • Junit 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private EmployeeMapper employeeMapper;

@Test
public void findAll() {
List<Employee> list = employeeMapper.findAll();
list.forEach(System.out::println);
}

}

执行上面的测试代码,控制台输出的日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
==>  Preparing: select * from t_employee
==> Parameters:
<== Columns: id, last_name, gender, email, age
<== Row: 1, Jim, 1, jim@gmail.com, 26
<== Row: 2, Peter, 1, peter@gmail.com, 29
<== Row: 3, David, 1, david@gmail.com, 28
<== Row: 4, Tom, 1, tom@gmail.com, 25
<== Total: 4

Employee [id=1, lastName=Jim, gender=1, email=jim@gmail.com, age=26]
Employee [id=2, lastName=Peter, gender=1, email=peter@gmail.com, age=29]
Employee [id=3, lastName=David, gender=1, email=david@gmail.com, age=28]
Employee [id=4, lastName=Tom, gender=1, email=tom@gmail.com, age=25]

EmployeeMapper 接口继承自 CustomBaseMapper 接口,拥有了 findAll 方法;由于 CustomSqlInjector SQL 注入器的存在,会自动将 SQL 模板注入到 MyBatis-Plus 中,这样即使不在 SQL 映射文件中编写 SQL 语句,也能执行对应的 SQL 语句。

MyBatis-Plus 自动填充处理器

自动填充处理器介绍

MyBatis-Plus 自动填充处理器的工作原理:在插入或者更新数据库表数据之前,直接给 Entity 的属性设置值。实现自动填充功能的核心对象是 MetaObjectHandler 接口和 MetaObject 类,其中的 MetaObject 是 MyBatis 提供的一个用于更加方便、更加优雅地访问对象的属性,并给对象的属性设置值的一个对象。MetaObject 还会用于包装对象,支持对 Object 、Map、Collection 等对象进行包装。本质上 MetaObject 获取对象的属性值或者是给对象的属性设置值,最终是要通过 Reflector(反射器) 获取到属性的对应方法的 Invoker 对象,然后执行 invoke 方法。

自动填充处理器的常用用途

自动填充处理器通常用于对公共字段进行填充,例如数据库表中的创建时间(create_time)以及修改时间(update_time)字段,使用它的好处是可以统一对这些公共字段进行处理,避免编写重复的代码。

自动填充策略说明

  • 使用方法:@TableField(fill = FieldFill.INSERT)
描述
DEFAULT 默认不处理
INSERT 插入时填充字段
UPDATE 更新时填充字段
INSERT_UPDATE 插入和更新时填充字段

自动填充处理器使用案例

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-15

自动填充处理器的实现步骤

  • 1、在需要填充的实体类属性上添加注解,例如 @TableFile(fill = FieldFill.INSERT)
  • 2、实现 MetaObjectHandler 接口,自定义自动填充处理器
  • 3、将自定义填充处理器注入到 MyBatis-Plus 全局中

添加 @TableField 注解

在实体类的属性上添加 @TableField 注解,并通过注解的 fill 属性指定填充策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Employee {

private Long id;

private String lastName;

@TableField(fill = FieldFill.INSERT_UPDATE)
private String gender;

private String email;

private Integer age;

...

定义自动填充处理器

实现 MetaObjectHandler 接口,定义自动填充处理器类

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
public class CustomMetaObjectHandler implements MetaObjectHandler {

/**
* 插入操作,自动填充
*/
@Override
public void insertFill(MetaObject metaObject) {
Object fieldValue = getFieldValByName("gender", metaObject);
if (fieldValue == null) {
System.out.println("******* 插入操作满足自动填充条件 *******");
this.setFieldValByName("gender", "1", metaObject);
}
}

/**
* 更新操作,自动填充
*/
@Override
public void updateFill(MetaObject metaObject) {
Object fieldValue = getFieldValByName("gender", metaObject);
if (fieldValue == null) {
System.out.println("******* 更新操作满足自动填充条件 *******");
this.setFieldValByName("gender", "1", metaObject);
}
}

}

注入自动填充处理器

Spring XML 配置文件
1
2
3
4
5
6
7
8
9
10
11
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="globalConfig" ref="globalConfig"></property>
</bean>

<bean id="globalConfig" class="com.baomidou.mybatisplus.core.config.GlobalConfig">
<!-- 注入自动填充处理器 -->
<property name="metaObjectHandler" ref="customMetaObjectHandler" />
</bean>

<!-- 自定义的自动填充处理器 -->
<bean id="customMetaObjectHandler" class="com.clay.mybatis.handler.CustomMetaObjectHandler" />
SpringBoot 配置类

定义 SpringBoot 配置类,将自动填充处理器注入 Spring 容器中

1
2
3
4
5
6
7
8
9
10
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MybatisPlusConfig {

@Bean
public CustomMetaObjectHandler customMetaObjectHandler() {
return new CustomMetaObjectHandler();
}

}

Junit 单元测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private EmployeeMapper employeeMapper;

@Test
public void insert() {
Employee employee = new Employee();
employee.setEmail("clion@gmail.com");
employee.setLastName("Clion");
employee.setAge(26);
employeeMapper.insert(employee);
}

@Test
public void update() {
Employee employee = new Employee();
employee.setId(1L);
employee.setEmail("empty@gmai.com");
employeeMapper.updateById(employee);
}

}

执行上面的测试代码,控制台输出的日志信息如下:

1
2
3
4
5
6
7
8
9
******* 插入操作满足自动填充条件 *******
==> Preparing: INSERT INTO t_employee ( last_name, gender, email, age ) VALUES ( ?, ?, ?, ? )
==> Parameters: Clion(String), 1(String), clion@gmail.com(String), 26(Integer)
<== Updates: 1

******* 更新操作满足自动填充条件 *******
==> Preparing: UPDATE t_employee SET gender=?, email=? WHERE id=?
==> Parameters: 1(String), empty@gmai.com(String), 1(Long)
<== Updates: 1

观察上面的输出结果,可以发现执行插入和更新操作之前,当实体类的 gender 属性为 NULL 时,其值会被自动填充。