【MyBatis-Plus】一文上手MyBatis-Plus

638 阅读13分钟

官方文档

mp.baomidou.com/guide/

重要特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响。

  • 损耗小:启动即会自动注入基本CURD,性能基本无损耗,直接面向对象操作。

  • 支持主键自动生成:支持多达4种主键策略(内含分布式唯一ID生成器Sequence),可自由配置。完美解决主键问题。

  • 支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、SQLite、SQLServer等多种数据库。

  • 内置分页插件:基于MyBatis物理分页。开发者无需关心具体操作,配置好插件之后,写分页等同于普通List查询。

  • 内置性能分析插件:可输出Sql语句以及其执行时间,建议开发测试时启用该功能。

  • 内置Sql注入剥离器:支持Sql注入剥离,有效预防Sql注入攻击。

上手

1. 环境配置

  1. 引入依赖

    <!-- MySQL依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- MyBatis-Plus依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.0.5</version>
    </dependency>
    

    注意:

    1. 使用MyBatis-Plus之前,必须已经引入MySQL和配置数据库连接。

    2. Spring Boot 2.1.x以下版本中引入MySQL,parent中维护的版本是MySQL5Spring Boot 2.1.x以上版本中引入MySQL,parent中维护的版本是MySQL8

  2. 配置数据库参数

    spring:
      # MySQL8数据库配置
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
    
  3. 配置MyBatis-Plus日志

    # 配置MP日志
    mybatis-plus:
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
  4. 创建mapper包

  5. 在启动类设置包扫描

    @SpringBootApplication
    @MapperScan("org.aydenbryan.demo.mapper")
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
  6. 在entity包中创建Customer类

    @Data
    public class Customer {
        private Long id;
        private String username;
        private String password;
    }
    

    注意:

    1. 在数据库中id一般用INT或者BIGINT存储,其中BIGINT对应Java中Long类型。
    2. 这里没有设置主键自增长,组件自增策略将在后文详细说明。
  7. 在mapper包中创建CustomerMapper接口

    @Repository
    public interface CustomerMapper extends BaseMapper<Customer> {
        
    }
    

    注意:

    1. @Repository如果不加,那么在给CustomerMapper实例@Autowired的时候IDEA会报错。因为CustomerMapper是一个接口,并且没有实现类,IDEA在预编译时找不到能够给实例注入的类(接口不能构造实例)。但是运行时,MyBatis-Plus会动态根据CustomerMapper接口构造实例。

2. 插入操作

  1. 插入一个对象

    @Test
    public void testInsert(Customer customer) {
        // int insert(T var1); 
        int result = customerMapper.insert(customer);
        System.out.println(result);
    }
    

3. 删除操作

  1. 根据id删除条目

    @Test
    public void testDeleteById(long id) {
        // int deleteById(Serializable var1);
        int result = customerMapper.deleteById(id);
        System.out.println(result);
    }
    
  2. 根据多个id批量删除条目

    @Test
    public void testDeleteBatchIds() {
        // int deleteBatchIds(@Param("coll") Collection<? extends Serializable> var1);
        int result = customerMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));
        System.out.println(result);
    }
    
  3. 简单条件删除

    @Test
    public void testDeleteByMap() {
        // 构造删除条件
        Map<String, Object> conditions = new HashMap<>();
        conditions.put("username", "aydenbryan");
        // int deleteByMap(@Param("cm") Map<String, Object> var1);
        customerMapper.deleteByMap(conditions);
    }
    

4. 更新操作

  1. 根据id更新条目

    @Test
    public void testUpdateById(Customer customer) {
        customer.setUsername("new username");
        // int updateById(@Param("et") T var1);
        int result = customerMapper.updateById(customer);
        System.out.println(result);
    }
    

    注意:

    1. 传入的对象id不能为空,而且必须能在表中找到相应的条目。
    2. 需要修改条目中的哪一个字段直接setter即可,MP会自动保留没有修改的字段。不会像MyBatis把没有修改的字段全部赋值为null,要想保留就要写大量if-else判断。

5. 查询操作

  1. 查询一张表中所有条目

    @Test
    public void testSelectList() {
        // List<T> selectList(@Param("ew") Wrapper<T> var1);
        List<Customer> customers = customerMapper.selectList(null);
        customers.forEach(System.out::println);
    }
    
  2. 根据id查询条目

    @Test
    public void testSelectById(long id) {
        // T selectById(Serializable var1);
        Customer customer = customerMapper.selectById(id);
        System.out.println(customer);
    }
    
  3. 根据多个id批量查询条目

    @Test
    public void testSelectBatchIds() {
        // List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> var1);
        List<Customer> customers = customerMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L));
        customers.forEach(System.out::println);
    }
    
  4. 简单条件查询

    该种查询方式很局限,只能实现=关系的查询。

    @Test
    public void testSelectByMap() {
        // 构造查询条件
        Map<String, Object> conditions = new HashMap<>();
        conditions.put("username", "aydenbryan");
        // List<T> selectByMap(@Param("cm") Map<String, Object> var1);
        customerMapper.selectByMap(conditions);
    }
    

Wapper条件构造器

image.png

1. 删除操作

  1. 删除符合条件的条目

    @Test
    public void testDelete() {
        QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();
        queryWrapper
            // `age` >= 18
            .ge("age", 18)
            // `username` IS NULL
            .isNull("username")
            // `password` IS NOT NULL
            .isNotNull("password");
        int result = customerMapper.delete(queryWrapper);
        System.out.println(result);
    }
    

2. 更新操作

  1. 更新符合条件的一个条目

    @Test
    public void testUpdate(Customer customer) {
        UpdateWrapper<Customer> updateWrapper = new UpdateWrapper<>();
        // 更新条件
        updateWrapper
            // `username` LIKE '%h%'
            .like("username", "h")
            // OR
            .or()
            // `age` BETWEEN 18 AND 25
            .between("age", 18, 25);
        int result = customerMapper.update(customer, updateWrapper);
        System.out.println(result);
    }
    

3. 查询操作

  1. 查询符合条件的一个条目

    @Test
    public void testSelectOne() {
        QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();
        // `username` = 'aydenbryan'
        queryWrapper.eq("username", "aydenbryan");
        // T selectOne(@Param("ew") Wrapper<T> var1);
        Customer customer = customerMapper.selectOne(queryWrapper);
        System.out.println(customer);
    }
    

    注意:

    1. 如果查询出来的条目多余1个,那么会抛出异常。
  2. 查询符合条件的多个条目

    @Test
    public void testSelectList() {
        Map<String, Object> conditions = new HashMap<>();
        conditions.put("username", "aydenbryan");
        conditions.put("age", 20);
        QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();
        // `username` = 'aydenbryan' AND `age` = 20
        queryWrapper.allEq(conditions);
        // List<T> selectList(@Param("ew") Wrapper<T> var1);
        List<Customer> customers = customerMapper.selectList(queryWrapper);
        customers.forEach(System.out::println);
    }
    
  3. 查询符合条件的多个条目

    @Test
    public void testSelectMaps() {
        QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();
        queryWrapper
            // `username` NOT LIKE '%e%'
            .notLike("username", "e")
            // `username` LIKE '%t'
            .likeRight("username", "t");
        // List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> var1);
        List<Map<String, Object>> results = customerMapper.selectMaps(queryWrapper);
        results.forEach(System.out::println);
    }
    
  4. 查询符合条件的多个条目并排序

    @Test
    public void testSelectListOrderBy() {
        QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();
        // 指定排序规则和排序字段
        queryWrapper.orderByDesc("id");
        List<Customer> customers = customerMapper.selectList(queryWrapper);
        customers.forEach(System.out::println);
    }
    
  5. 查询符合条件的条目总数

    @Test
    public void selectCount() {
        QueryWrapper<Customer> queryWrapper = new QueryWrapper<>();
        // `age` >= 20 AND `age` <= 30
        queryWrapper.between("age", 20, 30);
        // Integer selectCount(@Param("ew") Wrapper<T> var1);
        int count = customerMapper.selectCount(queryWrapper);
        System.out.println(count);
    }
    

通用Service

1. 说明

MP除了通用的Mapper还是通用的Service。这也减少了相对应的代码工作量,把通用的接口提取到公共,同时也可以自定义接口。其实按照MP的这种思想,可以自己也实现一些通用的Controller

2. 实现

  1. Service接口继承IService

    public interface UserService extends IService<User> {
     
    }
    
  2. ServiceImpl实现类继承ServiceImpl

    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
     
    }
    

唯一ID生成方案

1. 主键自增(Auto)

简单的单体应用中推荐使用主键自增策略。

  1. 表中设置id为主键自增

    image.png

  2. 在entity中配置id自增

    @Data
    public class Customer {
        // 设置主键自增
        @TableId(type = IdType.AUTO)
        private Long id;
        private String username;
        private String password;
    }
    

2. 分布式id生成器(ID_WORKER)

当使用MP插入一个对象,该对象id为空,结果成功插入。MP还自动给该对象的id赋了一个19位的数字。实际上,MP内置了一个基于雪花算法的id生成器,是生成器给我们自动生成的id。

20210313195730.png

MP中默认使用id生成器生成id,无需在数据库和entity中进行任何配置

id生成器生效的前提:

  • 插入对象的id类型数据库中必须是BIGINT,entity中是Long

  • 插入对象的id必须为空

注意:

  1. 如果使用MP的id生成器生成的19位Long型的id,那么传递到前端必须转化成String类型进行处理。因为JavaScript最多只能处理16位的整数类型。如果传递的类型还是Long类型,那么JavaScript就会把后三位数字截断成000,程序就会出错误。所以如果不想对id的类型进行转换,可以一开始就将id设置String类型,这个时候就会用到字符串id生成器

3. 字符串id生成器(ID_WORKER_STR)

分布式系统中推荐使用字符串id生成器生成id,同样也是基于雪花算法。

需要在entity中进行配置

@Data
public class Customer {
    @TableId(type = IdType.ID_WORKER_STR)
    private String id;
    private String username;
    private String password;
}

字符串id生成器生效的前提:

  • 插入对象的id类型数据库中必须是VARCHAR/CHAR(19),entity中是String

  • 插入对象的id必须为空

4. 主键手动插入(INPUT)

需要在entity中进行配置,每次添加对象必须手动给对象中的id赋值。

@Data
public class Customer {
    @TableId(type = IdType.INPUT)
    private String id;
    private String username;
    private String password;
}

使用场景:在表与表是一对一的关系时可以使用。

  • 一个user表和一个id_number表。user表存储的是一个用户的所有信息,而id_number表只存储用户的身份证号。这个时候,id_number表中的每一条和user表中的每一条都是一对一的关系,那么id_number表每一项的id就可以通过user表对应的那一项的id赋值来生成。

  • 数据库优化中,为了查询效率,有的时候会把原来一个表中每一个条目中的大字段拆分出来构成另一个表。但是这两个表每一条都是一一对应的关系,所以分出来的表的id可以通过原表的id赋值来构成。

数据的自动更新

1. MySQL中实现

一般create_timeupdate_time字段的数据自动更新利用MySQL层面的操作来配置。

  1. MySQL配置

    ALTER TABLE customer ADD COLUMN create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER password;
    ALTER TABLE customer ADD COLUMN update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER create_time;
    

    设置后的表结构:

    20210313195510.png

    注意:

    1. 该操作在MySQL Workbench图形化界面上操作会出现错误。
    2. CURRENT_TIMESTEAP:当insert数据时,MySQL会自动设置当前系统时间赋值给该属性字段。
    3. ON UPDATE CURRENT_TIMESTAMP:当update数据时,并且成功发生更改时,MySQL会自动设置当前系统时间赋值给该属性字段。
  2. entity配置

    @Data
    public class Customer {
        private Long id;
        private String username;
        private String password;
        private Date createTime;
        private Date updateTime;
    }
    

2. MP中实现

MP层面的操作去配置字段的数据更新一般都用于业务上的需求,例如每个人过了生日之后,年龄就会自动长一岁。这样的数据更新让数据库操作很不方便。

本案例为了方便还是让MP去自动填充create_timeupdate_time字段。

  1. entity配置

    @Data
    public class Customer {
        private Long id;
        private String username;
        private String password;
        @TableField(fill = FieldFill.INSERT)
        private Timestamp createTime;
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Timestamp updateTime;
    }
    

    注意:

    1. FieldFill.INSERT:创建时自动填充。
    2. FieldFill.INSERT_UPDATE:创建和修改时自动填充。
  2. 建handler包

  3. 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler

    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
        // 插入时自动填充
        @Override
        public void insertFill(MetaObject metaObject) {
            this.setFieldValByName("createTime", new Timestamp(System.currentTimeMillis()), metaObject);
            this.setFieldValByName("updateTime", new Timestamp(System.currentTimeMillis()), metaObject);
        }
    
        // 更新时自动填充
        @Override
        public void updateFill(MetaObject metaObject) {
            this.setFieldValByName("updateTime", new Timestamp(System.currentTimeMillis()), metaObject);
        }
    }
    

3. 配置全局时区

如果createTimeupdateTime在数据库中的类型为DATETIME,在java中的数据类型为Date。那么数据库中存储的时间被解析到程序中可能少了八小时。因为默认情况下json时间格式带有时区,并且是世界标准时间,和我们的时间差了八个小时。

解决方法:在application.yml文件中配置

spring:
  ...
    # 配置全局时区
    jackson:
      # 时间显示格式  
      date-format: yyyy-MM-dd HH:mm:ss
      # 时间设置时区
      time-zone: GMT+8

如上文可知,一些字段可以使用MySQL填充默认值,也可以使用MP填充默认值。

MP自动填充和MySQL自动填充的区别

  • MP自动填充是在数据持久层面的实现,它的原理是在将PO存入数据库之前,使用程序将设置自动填充数据的字段填充默认值,然后将PO整体存入数据库。
  • MySQL自动填充是在数据库层面的实现,它的原理是在PO存入数据库之后,数据库内将自动填充数据的字段填充默认值。

假设当前新建了一个Discussion对象需要存入MySQL,Dicussion对象中的commentCount属性使用了MySQL自动填充方案将其初始化为0,那么在使用MP插入Discussion对象后就调用Dicussion对象获取到的commentCount是null,而如果要获取id就可以获取到。因为id使用的是MP层面的填充(ID_WORKER_STR),而此时Dicussion对象还没有进入数据库,因此commentCount还没有被初始化为0。

乐观锁插件

1. 适用场景

当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新。

2. 实现思路

  • 查询记录时,获取当前version字段的值,假设version = 1。

  • 更新时,将获取的version带入SQL,进行版本判断。

    UPDATE customer SET `username` = 'aydenbryan', `version` = `version` + 1 WHERE `id` = 1 AND `version` = 1;
    
  • 如果在SQL判断时,version = 1,那么就说明从获取version后到SQL判断版本前,没有其他线程改变该条目的数据。如果version != 1,那么就说明该条目数据已经被改变,造成更行冲突,不能进行更新操作。

3. MP中实现

MP提供了乐观锁插件,封装了以上的实现过程。

  1. 表中添加字段

    设置version的默认值为1。

    20210313195547.png

  2. entity配置

    需要在entity中加入version字段。

    @Data
    public class Customer {
        private Long id;
        private String username;
        private String password;
        // 锁定版本
        @Version
        private Integer version;
    }
    
  3. config包中创建MybatisPlusConfig类,并且将启动类上的@MapperScan转移到类上方

    @Configuration
    @EnableTransactionManagement
    @MapperScan("org.aydenbryan.demo.mapper")
    public class MybatisPlusConfig {
        // 乐观锁插件
        @Bean
        public OptimisticLockerInterceptor optimisticLockerInterceptor() {
            return new OptimisticLockerInterceptor();
        }
    }
    
  4. 案例

    @Test
    public void testOptimisticLocker() {
        // 获取的数据中需要包含version
        Customer customer = customerMapper.selectById(1370282470657789953L);
        // 修改数据
        customer.setUsername("john1234");
        // 在执行数据更新的时候,MP会自动将获取的version的值和数据库中version的值进行比对,并自增1
        customerMapper.updateById(customer);
    }
    

分页插件

  1. config包中创建MybatisPlusConfig类,并且将启动类上的@MapperScan转移到类上方

    @Configuration
    @EnableTransactionManagement
    @MapperScan("org.aydenbryan.demo.mapper")
    public class MybatisPlusConfig {
        // 分页插件
        @Bean
        public PaginationInterceptor paginationInterceptor() {
            return new PaginationInterceptor();
        }
    }
    
  2. 案例一

    该种分页方式只能对一张表的条目进行分页。

    @Test
    public void testSelectPage(int current, int size) {
        // 当前为第current页,每页最多显示size个条目
        Page<Customer> page = new Page<>(current, size);
        // IPage<T> selectPage(IPage<T> var1, @Param("ew") Wrapper<T> var2);
        IPage<Customer> iPage = customerMapper.selectPage(page, null);
        // 当前页码
        System.out.println(iPage.getCurrent());
        // 总页数
        System.out.println(iPage.getPages());
        // 每页条目数
        System.out.println(iPage.getSize());
        // 总条目数
        System.out.println(iPage.getTotal());
        // 当前页的数据集合
        List<Customer> records = iPage.getRecords();
        records.forEach(System.out::println);
    }
    
  3. 案例二

    该种分页方式争对多表关联查询出来的条目进行分页。

    @Test
    public void testSelectMapsPage(int current, int size) {
        // 当前为第current页,每页最多有size个条目
        Page<Customer> page = new Page<>(current, size);
        // IPage<Map<String, Object>> selectMapsPage(IPage<T> var1, @Param("ew") Wrapper<T> var2);
        IPage<Map<String, Object>> iPage = customerMapper.selectMapsPage(page, null);
        // 当前页码
        System.out.println(iPage.getCurrent());
        // 总页数
        System.out.println(iPage.getPages());
        // 每页条目数
        System.out.println(iPage.getSize());
        // 总条目数
        System.out.println(iPage.getTotal());
        // 当前页的数据集合
        List<Map<String, Object>> records = iPage.getRecords();
        records.forEach(System.out::println);
    }
    

逻辑删除

  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据。
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为被删除状态,之后在数据库中仍旧能看到此条数据记录。

如果没有MP环境,那么实现逻辑删除用的是更新的SQL语句,而不是删除的SQL语句。MP封装了操作更新SQL的过程,只需要在MP中进行简单的配置,就可以调用MP提供的物理删除接口实现逻辑删除

在MP进入逻辑删除模式后,所有逻辑删除底层调用的都是更新的sql语句,并且任何对数据的操作都只在is_deleted = 0的范围内进行。(SQL中添加where is_deleted = 0

  1. 表中添加字段

    0代表false,代表没删除。1代表true,代表逻辑删除。

    ALTER TABLE customer ADD COLUMN is_deleted boolean NOT NULL DEFAULT 0 AFTER password;
    

    image.png

  2. entity配置

    @Data
    public class Customer {
        private Long id;
        private String username;
        private String password;
        @TableLogic
        @TableField("is_deleted")
        private Boolean deleted;
    }
    
  3. 配置文件

    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    
  4. config包中创建MybatisPlusConfig类,并且将启动类上的@MapperScan转移到类上方

    3.1.1 之后可以忽略此步骤

    @Configuration
    @EnableTransactionManagement
    @MapperScan("org.aydenbryan.demo.mapper")
    public class MybatisPlusConfig {
        @Bean
        public ISqlInjector sqlInjector() {
            return new LogicSqlInjector();
        }
    }
    
  5. 案例

    @Test
    public void testDeleteById(long id) {
        // 现在为逻辑删除模式
        // int deleteById(Serializable var1);
        int result = customerMapper.deleteById(id);
        System.out.println(result);
    }
    

SQL性能插件

1. 单一环境配置

  1. config包中创建MybatisPlusConfig类,并且将启动类上的@MapperScan转移到类上方

    @Configuration
    @EnableTransactionManagement
    @MapperScan("org.aydenbryan.demo.mapper")
    public class MybatisPlusConfig {
        @Bean
        public PerformanceInterceptor performanceInterceptor() {
            PerformanceInterceptor interceptor = new PerformanceInterceptor();
            // 如果SQL执行时间最大为300ms,如果超过,则抛出异常,提示需要优化SQL
            interceptor.setMaxTime(300);
            // 格式化SQL打印,方便查看
            interceptor.setFormat(true);
            return interceptor;
        }
    }
    

2. 多环境配置

一般只在开发环境(dev),测试环境(test)开启性能分析插件。生产环境(prod)如果开启,将会极大消耗服务器资源。

需要配合着application-dev.ymlapplication-test.yml使用。

  1. config包中创建MybatisPlusConfig类,并且将启动类上的@MapperScan转移到类上方

    @Configuration
    @EnableTransactionManagement
    @MapperScan("org.aydenbryan.demo.mapper")
    public class MybatisPlusConfig {
        @Bean
        @Profile({"dev", "test"})
        public PerformanceInterceptor performanceInterceptor() {
            return new PerformanceInterceptor();
        }
    }