封面画师: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 目录下。