• MyBatis-Plus 学习笔记


    MyBatis-Plus 学习笔记

    1. 简介

    1.1 介绍

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

    就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。

    官网:https://mp.baomidou.com/

    github:https://github.com/baomidou/mybatis-plus

    image-20200729114053137

    1.2 特性

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

    1.3 框架结构

    image-20200729140241886

    2. 快速开始

    环境:

    JDK 1.8

    Maven 3.6.1

    mysql 5.7.27

    MyBtisPlus 3.3.2 (MP 3.3.X 版本相比于以前的版本改动了不少,使用时请注意版本号)

    idea 2019.3

    1. 创建一个 SpringBoot 工程项目

    2. 引入依赖

      <dependencies>
          <!-- mysql -->
          <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <version>5.1.46</version>
          </dependency>
          <!-- druid -->
          <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>druid</artifactId>
              <version>1.1.23</version>
          </dependency>
          <!-- mybatis plus -->
          <dependency>
              <groupId>com.baomidou</groupId>
              <artifactId>mybatis-plus-boot-starter</artifactId>
              <version>3.3.2</version>
          </dependency>
          <!-- lombok -->
          <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
          </dependency>
      
          <!-- SpringBoot 相关依赖 -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-test</artifactId>
              <scope>test</scope>
          </dependency>
      </dependencies>
      
      <!-- SpringBoot -->
      <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.3.0.RELEASE</version>
      </parent>
      
      <build>
          <!-- 插件 -->
          <plugins>
              <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-compiler-plugin</artifactId>
                  <configuration>
                      <source>1.8</source>
                      <target>1.8</target>
                      <encoding>UTF-8</encoding>
                  </configuration>
              </plugin>
              <plugin>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
              </plugin>
          </plugins>
          <!-- 静态资源解析 -->
          <resources>
              <resource>
                  <directory>src/main/java</directory>
                  <includes>
                      <include>**/*.xml</include>
                  </includes>
                  <filtering>true</filtering>
              </resource>
          </resources>
      </build>
      
    3. 创建测试所需要的表,并插入测试数据

      # 创建表
      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)
      );
      
      #插入数据
      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');
      
    4. 编写实体类

      这里使用了 Lombok

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
      
          private Long id;
          private String name;
          private Integer age;
          private String email;
      
      }
      
    5. 配置数据源

      在 application.yml 中配置数据源,这里使用的是 Druid 数据源,也可以使用默认的数据源

      spring:
        datasource:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
          password: root
          username: root
          # 切换成 Druid 数据源
          type: com.alibaba.druid.pool.DruidDataSource
          #Spring Boot 默认是不注入这些属性值的,需要自己绑定
          #druid 数据源专有配置
          initialSize: 5
          minIdle: 5
          maxActive: 20
          maxWait: 60000
          timeBetweenEvictionRunsMillis: 60000
          minEvictableIdleTimeMillis: 300000
          validationQuery: SELECT 1 FROM DUAL
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          poolPreparedStatements: true
      
          #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
          #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
          #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
          filters: stat,wall,log4j
          maxPoolPreparedStatementPerConnectionSize: 20
          useGlobalDataSourceStat: true
          connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      
    6. 创建 mapper 接口

      创建 mapper 接口,并继承 BaseMapper 接口,规定泛型类型

      @Repository // 将 mapper 注册到 Spring 容器中
      public interface UserMapper extends BaseMapper<User> {
      }
      
    7. 在主启动类中扫描接口

      在主启动类添加 @MapperScan() 注解,来扫描我们刚刚创建的 mapper 接口

      @MapperScan("com.xp.mapper")  // 扫描 mapper 接口
      @SpringBootApplication
      public class Application {
          public static void main(String[] args) {
              SpringApplication.run(Application.class, args);
          }
      }
      
    8. 测试

      编写测试类

      @SpringBootTest
      public class ApplicationTest {
      
          @Autowired
          private UserMapper userMapper;
      
          @Test
          void test(){
              // UserMapper 中的 selectList() 方法的参数为 MyBatisPlus 内置的的条件封装器 wrapper,所以不填写就是无任何条件
              List<User> userList = userMapper.selectList(null);
              for (User user : userList) {
                  System.out.println(user);
              }
          }
      
      }
      

      测试结果如下:

      image-20200730143122679

    9. 配置日志

      如果我们想要知道 sql 语句的执行情况,我们可以配置日志。想要开启日志功能,只需配置如下配置即可:

      其中 log-impl 的值可以是其他的日志,不一定是我下面的,也可以配置 log4j

      mybatis-plus:
        configuration:
          log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
      

    3. 注解

    除了 MyBatis 原本的注解,MP 还有一些自己的注解

    3.1 @TableName

    • 描述:表名注解
    属性 类型 必须指定 默认值 描述
    value String "" 表名
    schema String "" schema
    keepGlobalPrefix boolean false 是否保持使用全局的 tablePrefix 的值(如果设置了全局 tablePrefix 且自行设置了 value 的值)
    resultMap String "" xml 中 resultMap 的 id
    autoResultMap boolean false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建并注入)

    关于 ‘autoResultMap’ 的说明

    mp 会自动构建一个 ResultMap 并注入到 mybatis 里(一般用不上)。下面讲两句:因为 mp 底层是 MyBatis ,所以一些 MyBatis 的尝试你要知道,mp 只是帮你注入了常用 crud 到 MyBatis 里,注入之前可以说是动态的(根据你entity的字段以及注解变化而变化),但是注入之后是静态的(等于你写在xml的东西)而对于直接指定 typeHandler,MyBatis 只支持你写在两个地方:

    1. 定义在 resultMap 里,只作用于 select 查询的返回结果封装
    2. 定义在 insertupdate sql 的 {property} 里的 property 后面(例:#{property,typehandler-xxx.xxx.xxx}),只作用域 设置值 。而除了这两种直接指定 typeHandler,MyBatis 有一个全局的扫描你自己的 typeHandler 包的配置,这是根据你的 property 的类型去找 typeHandler 并使用。

    3.2 @Tableld

    • 描述:主键注解
    属性 类型 必须指定 默认值 描述
    value String "" 主键字段名
    type Enum IdType.NONE 主键类型

    IdType

    描述
    AUTO 数据库 ID 自增
    NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
    INPUT insert 前自行 set 主键值
    ASSIGN_ID 分配 ID(主键类型为 Number (Long和Integer)或 String)(since 3.3.0),使用接口 IdentifierGenerator 的方法 nextId (默认实现类为 DefaultIdentifierGenerator 雪花算法)
    ASSIGN_UUID 分配 UUID ,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator 的方法 nextUUID(默认default方法)
    ID_WORKER 分布式全局唯一 ID 长整性类型(please use ASSIGN_ID
    UUID 32 位 UUID 字符串(please use ASSIGN_UUID
    ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please user ASSIGN_ID

    3.3@TableField

    • 描述:字段注解(非主键)
    属性 类型 必须指定 默认值 描述
    value String "" 数据库字段名
    el String "" 映射原生 #{...} 逻辑,相当于写在 xml 里的 #{...}
    exist boolean true 是否位数据库表字段
    condition String "" 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s}
    update String "" 字段 update set 部分注入。例如:update ="%s+1":表示更新时会 set version = version +1(该属性优先级高于 el 属性)
    insertStrategy Enum N DEFAULT 举例 NOT_NULL: insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
    updateStrategy Enum N DEFAULT 举例:IGNORED: update table_a set column=#{columnProperty}
    whereStrategy Enum N DEFAULT 举例:NOT_EMPTY: where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
    fill Enum FieldFill.DEAFULT 字段自动填充策略
    select boolean true 是否进行 select 查询
    keepGloabalFormat boolean false 是否保持使用全局的 format 进行处理
    jdbcType JdbcType JdbcType.UNDEFINED JDBC类型(该默认值不代表会按照该值生效)
    typeHandler Class<? extend TypeHandler> UnknownTypeHandler.class 类型处理器(该默认值不代表会按照该值生效)
    numericScale String "" 指定小数点后保留机位

    关于 ‘jdbcType’ 和 ‘numerucScale’ 的说明

    numericScale 只生肖与 update 的 sql。jdbcTypetypeHandler 如果不配合 @TableName#autoResultMap = true 一起使用,也只生效于 update 的 sql。对于 typeHandler 如果你的字段类型和 set 进去的类型为 equals 关系,则只需要让你的 typeHandler 让 MyBatis 加载到即可,不需要使用注解

    FieldStrategy

    描述
    IGNORED 忽略判断
    NOT_NULL 非 NULL 判断
    NOT_EMPTY 非空判断(只对字符串类型字段。其他类型字段依然为非NULL判断)
    DEFAULT 追随全局配置

    FieldFill

    描述
    DEFAULT 默认不处理
    INSERT 插入时填充字段
    UPDATE 更新时填充字段
    INSERT_UPDATE 插入和更新时填充字段

    3.4 @Version

    • 描述:乐观锁注解。标记 @Version 在字段上

    3.5 @EnumValue

    • 描述:通过枚举类注解(注解在枚举类字段上)

    3.6 @TableLogic

    • 描述:表字段逻辑处理注解(逻辑删除)
    属性 类型 必须指定 默认值 描述
    value String "" 逻辑未删除值
    delval String "" 逻辑删除值

    3.7 @SqlParser

    • 描述:租户注解,支持 method 上以及 mapper 接口上
    属性 类型 必须指定 默认值 描述
    filter boolean false true:表示过滤 SQL 解析,即不会进入 ISqlParser 解析链,否则会紧解析链并追加例如 tenant_id 等条件

    3.8 @KeySequence

    • 描述:序列主键策略 oracle
    • 属性:value、resultMap
    属性 类型 必须指定 默认值 描述
    value String "" 序列名
    clazz Class Long.class id的类型,可以指定 String.class ,这样返回的 Sequence 值是字符串 “1”

    4. CRUD

    4.1 增加

    MP 中提供了插入的方法 insert()

    @SpringBootTest
    public class ApplicationTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void test(){
    
            System.out.println(userMapper.insert(new User(null, "张三", 18, "xp@qq.com")));
    
        }
    
    }
    

    查看我们控制台打印的日志

    image-20200730190032029

    可以发现,这 id 是我们看不懂的东西,可不是我们真正想要的(主键自增)

    那么这个id是怎么生成的呢?

    在我们上面的注解中的 @Tableld 有写道,默认是 IdType.NONE。它是自动生成全局唯一的id。

    如果我们需要修改成为自增,只需在主键上加@Tableld设置 type 修改成 IdType.AUTO

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    	// 设置type=IdType.AUTO,让主键自增
        @TableId(type=IdType.AUTO)
        private Long id;
        private String name;
        private Integer age;
        private String email;
    
    }
    

    需要注意的是,我们要先确定我们的表的主键是已经设置自增的,不然会报错

    image-20200730191458478

    4.2 删除

    1. 普通删除

    // 普通删除
    @Test
    void delete(){
        // 根据id删除
        userMapper.deleteById(7L);
        // 批量删除
        userMapper.deleteBatchIds(Arrays.asList(4L,5L,6L));
        // 通过 map 删除
        Map<String, Object> map = new HashMap<>();
        map.put("name","李四");
        userMapper.deleteByMap(map);
    }
    

    2. 逻辑删除

    首先,我们先在数据库的表中增加 deleted 字段,默认值为0 .(实际开发中是不允许修改数据库的,只是为了测试方便)

    修改实体类,让数据库的表和实体类一一对应,并在 deleted 上添加 @TableLogic 注解(3.3.0版本后可以不需要增加这个注解,只需在配置文件中开启即可)

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        @TableId(type=IdType.AUTO)
        private Long id;
        private String name;
        private Integer age;
        private String email;
        @TableLogic
        private Integer deleted;
    
    }
    

    在 application.yml 中配置逻辑删除

    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以不需要在实体类字段上增加注解)
          logic-delete-value: 1 # 逻辑已删除的值 (默认为1)
          logic-not-delete-value: 0 # 逻辑未删除的值 (默认为0)
    

    测试

    // 逻辑删除
    @Test
    void deleteLogic(){
        // 通过 id 进行逻辑删除
        userMapper.deleteById(2);
        // 批量删除
        userMapper.deleteBatchIds(Arrays.asList(1,8,2));
        // 通过 map 删除
        Map<String, Object> map = new HashMap<>();
        map.put("name","李四");
        userMapper.deleteByMap(map);
        // 查询用户,查看查询语句是否发生了变化
        System.out.println(userMapper.selectById(2));
    }
    

    3. 说明

    只对自动注入的 sql 起效

    • 插入:不作限制
    • 查找:追加 where 条件过滤掉已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
    • 更新:追加 where 条件防止更新到已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
    • 删除:转变为更新

    例如:

    • 删除:update user set deleted=1 where id=1 and deleted=0
    • 查找:select id,name,deleted from user where deleted=0

    字段类型支持说明:

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

    附录:

    • 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
    • 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示

    4. 常见问题

    1. 如何 insert?

      1. 字段在数据库定义默认值(推荐)
      2. insert 前自己 set 值
      3. 使用自动填充功能
    2. 删除接口自动填充功能失效

      1. 使用 update 方法并 UpdateWrapper.set(column,value)(推荐)

      2. 使用 update 方法并 UpdateWrapper.setSql("column=value")

      3. 使用 Sql 注入器 注入

        com.baomidou.mybatisplus.extension.injector.methods.LogicDeleteByIdWithFill(推荐)

    4.3 修改

    1. 根据 id 进行修改

    @SpringBootTest
    public class ApplicationTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void test(){
            System.out.println(userMapper.updateById(new User(6L, "李四", 3, null)));
        }
    
    }
    

    查看控制台日志输出,我们可以发现 MP 的插入方法是根据条件使用动态 SQL 实现的,这样就节省了我们写动态 SQL 的时间

    image-20200730192203894

    2. 条件构造器修改

    MP 提供了修改操作的条件构造器 UpdateWrapper

    @Test
    void queryWrapper(){
        // 使用 updateWrapper, sql中的 where 条件在 updateWrapper 中的 Entity 中设置,也即是.setEntity()这个方法设置查询条件
        // 这里的update方法的第一个参数是设置 set 的值,可以为空, updateWrapper 中也可以设置 set 的值
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        // 这里,setEntity(T entity) 是用来构建 where 后面的条件,若不设置则默认是更改所有的数据
        updateWrapper.set("name","李四").setEntity(new User().setName("张三"));
        userMapper.update(null,updateWrapper);
    }
    

    我们查看控制台日志,可以看到 sql 语句和这两个参数的对应关系

    image-20200731142636623

    4.4 查询

    1. 普通查询

    @Test
    void select(){
        // 根据id查询用户
        User user = userMapper.selectById(1);
        System.out.println(user);
    
        // 批量查询
        List<User> userList = userMapper.selectBatchIds(Arrays.asList(1, 2, 4, 5));
        userList.forEach(System.out::println);
    
        // 条件查询之一 map
        Map<String, Object> map = new HashMap<>();
        map.put("name","李四");
        map.put("age","3");
        List<User> users = userMapper.selectByMap(map);
        users.forEach(System.out::println);
    }
    

    2. 分页查询

    要进行分页查询,我们需要先配置 MP 的分页插件,并将 MP 的分页插件注册到 Spring 容器中

    在我们之前写的 MyBatisPlus 配置类 MyBatisPlusConfig 类中增加如下代码:

    MyBatisPlusConfig

    @Bean
    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;
    }
    

    测试

    // 分页查询
    @Test
    void selectPage(){
        // 普通分页查询,Page构造器的第一个参数表示第几页,第二个参数表示每页最多多少条数据
        Page<User> page = new Page<>(2,5);
        // 进行分页查询
        Page<User> userPage = userMapper.selectPage(page, null);
        // 获取分页记录
        List<User> userList = userPage.getRecords();
        userList.forEach(System.out::println);
        // 获取总记录数
        System.out.println("总页数:"+page.getTotal());
        // 获取总页数
        long pages = page.getPages();
        System.out.println("总记录数:"+pages);
        // 每页展示最大记录条数
        System.out.println("每页展示最大记录调试:"+page.getSize());
    }
    

    3. 条件构造器查询

    MP 中提供了查询的条件构造器 QueryWrapper

    @Test
    void queryWrapper(){
        // 创建 QueryWrapper 对象
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // 构造条件,wrapper 是链式编程,所以可以构造很多的条件.这里的查询条件是 name=张三 且 age>3 
        wrapper.eq("name","张三").gt("age",3);
        // 查询结果
        List<User> userList = userMapper.selectList(wrapper);
        userList.forEach(System.out::println);
    }
    

    5. 自动填充

    根据阿里巴巴开发手册:所有的数据库表都应该有 gmt_create、gmt_modified 两个字段。且更新数据表记录时,必须同时更新记录对应的 gmt_modified 字段值为当前时间。

    MP 已经帮我们做好了自动填充的操作。

    5.1 修改表结构

    在我们原本的 User 表中增加 gmt_create 和 gmt_modified 两个字段

    image-20200730192842829

    5.2 同步实体类

    同步实体类,并在实体类上增加注解

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        @TableId(type=IdType.AUTO)
        private Long id;
        private String name;
        private Integer age;
        private String email;
        @TableField(value = "gmt_create",fill = FieldFill.INSERT)
        private Date createTime;
        @TableField(value = "gmt_modified",fill = FieldFill.INSERT_UPDATE)
        private Date modifiedTime;
    
    }
    

    上面的注解介绍中已经有写道,@TableField 这个注解中 fill 属性可以设置自动填充策略

    FieldFill 中有4个自动填充策略

    public enum FieldFill {
        DEFAULT,		// 默认不处理
        INSERT,			// 插入时自动填充填充
        UPDATE,			// 更新时自动填充
        INSERT_UPDATE;	// 插入或更新时自动填充
    
        private FieldFill() {
        }
    }
    

    5.3 编写填充策略

    我们自定义一个元数据处理器 MyMetaObjectHandler,实现 MetaObjectHandler 接口

    @Slf4j      // 使用日志
    @Component  // 将我们自定义的元数据处理器注册到 Spring 容器中
    public class MyMetaObjectHandler implements MetaObjectHandler {
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("start insert ");
            // 设置插入的填充 参数分别时:第一个为原数据对象,第二个为填充的字段,第三个为填充的内容的Class对象,第四个是填充的内容
            this.strictInsertFill(metaObject,"createTime",Date.class,new Date());
            this.strictInsertFill(metaObject,"modifiedTime",Date.class,new Date());
            // fillStrategy 这个也可以设置自动填充,但是有bug 需要升级到 3.3.1.8-SNAPSHOT版本后
            // this.fillStrategy(metaObject,"modifiedTime",new Date());
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("start update ");
            // 设置修改的填充
            this.strictUpdateFill(metaObject,"modifiedTime",Date.class,new Date());
       		// fillStrategy 这个也可以设置自动填充,但是有bug 需要升级到 3.3.1.8-SNAPSHOT版本后
            // this.fillStrategy(metaObject,"modifiedTime",Date.class,new Date());
        }
    }
    

    5.4 测试

    我们先执行增加操作,看下增加操作是否自动填充了

    @SpringBootTest
    public class ApplicationTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void test(){
            System.out.println(userMapper.insert(new User(null, "张三", 18, "xp@qq.com",null,null)));
        }
    
    }
    

    可以发现,我们自定义的元数据处理器已经生效了,并且在执行插入操作的时候,createTime 和 modifiedTime 两个字段都自动填充了现在的时间

    image-20200730200620866

    我们再执行修改操作

    @SpringBootTest
    public class ApplicationTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void test(){
            System.out.println(userMapper.updateById(new User(3L, "李四", 3, null,null,null))));
        }
    
    }
    

    可以发现我们自定义的元数据处理器已经生效,而且自动更新了 modifiedTime 字段的修改时间

    image-20200730200850496

    5.5 注意事项

    • 字段必须声明 @TableField 注解,属性 fill选择对应策略,该声明告知 MyBatis-Plus 需要预留注入 sql 字段
    • 填充处理器 MyMetaObjectHandler 在 SpringBoot 中需要声明 @Component@Bean 注入
    • 要想根据注解 FieldFill.xxx字段名 以及 字段类型来取分必须使用父类的 strictInsertFill 或者 strictUpdateFill 方法
    • 不需要根据任何来取分可以使用父类的 fillStrategy 方法
    • 使用 update(T entity, Wrapper<T> updateWrapper) 时,想要自动填充,则第一个 entity 不能为空,否则则无法自动填充

    6. 乐观锁

    在面试过程中,我们经常会被问到乐观锁,悲观锁。

    6.1 什么是乐观锁,什么是悲观锁

    什么是悲观锁(Pessimistic Lock)

    当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库所机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【又名“悲观锁”,Perssimistic ,Concurrency Control,缩写“PCC"】

    什么是乐观锁(Optimistic Locking)

    乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以再数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

    6.2 乐观锁插件

    MP 提供了乐观锁插件

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

    乐观锁的实现方式:

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

    比如: 更新时的 version=1

    update user set name='xp',version = version +1 where id=1 and version=1
    

    如果执行更新操作时,数据已经被修改,即 version 不再是原来的 version =1,则会更新失败

    6.2.1 插件配置

    1. 修改数据表

      将我们之前的 User 表添加 version 字段,并设置默认为1

      image-20200730203833588

    2. 将乐观锁插件注册到 Spring 容器中

      创建一个 MyBatisPlusConfig 配置类

      @Configuration
      public class MyBatisPlusConfig {
      
          @Bean
          OptimisticLockerInterceptor optimisticLockerInterceptor(){
              return new OptimisticLockerInterceptor();
          }
      
      }
      
    3. 注解字段(必须要)

      修改实体类,给 version 增加注解

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
      
          @TableId(type=IdType.AUTO)
          private Long id;
          private String name;
          private Integer age;
          private String email;
          @TableField(fill = FieldFill.INSERT)
          private Date createTime;
          @TableField(fill = FieldFill.INSERT_UPDATE)
          private Date modifiedTime;
          @Version
          private Integer version;
      
      }
      

      特别说明

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

      先模拟乐观锁更新成功,单线程时

      // 模拟乐观锁成功
      @Test
      void test1(){
          // 1.查询用户信息
          User user = userMapper.selectById(2);
          System.out.println(user);
          // 2.修改用户信息
          user.setName("xp01");
          user.setId(2L);
          user.setAge(3);
          // 3.执行更新操作
          userMapper.updateById(user);
      }
      

      运行后控制台输出如下:

      image-20200730224315699

      可以看到,我们原本的版本号是1,执行更新操作后,MP 帮我们将版本号进行 +1 的操作

      模拟乐观锁失败,多线程下

      // 模拟乐观锁失败,多线程下
      @Test
      void test2(){
          // 线程1
          User user = userMapper.selectById(2);
          user.setName("xp111");
          user.setId(2L);
          user.setAge(1);
          System.out.println(user);
      
          // 模拟另一个线程执行插队操作
          User user1 = userMapper.selectById(2);
          user1.setName("xp222");
          user1.setId(2L);
          user1.setAge(2);
          System.out.println(user1);
          userMapper.updateById(user1);
      
          // 乐观锁失败
          userMapper.updateById(user);
      }
      

      运行后控制台输出如下:

      image-20200730230236289

    ​ 我们可以发现,在线程2插队执行更新操作后,线程1 执行更新操作时,MP帮我们自动加上了乐观锁,所以线程1 更新数据失败了

    7. 性能分析插件

    我们平时的开发中,会遇到一些慢 SQL。

    MP 提供了性能分析插件,如果 sql 执行时间超过这个时间就停止运行!

    但在 MP 的 3.2 版本后移除了性能分析插件

    image-20200801092959474

    这里就基于 p6spy 插件来学习,想要是想要使用以前 MP 自带的插件的话得降版本使用

    1. 引入依赖

      <!-- p6spy 性能分析插件 -->
      <dependency>
          <groupId>p6spy</groupId>
          <artifactId>p6spy</artifactId>
          <version>3.9.1</version>
      </dependency>
      
    2. 配置 p6spy

      在 application.yml 配置中将数据源的 driver-class-name 改成 p6spy 提供的驱动

      spring:
        datasource:
          driver-class-name: com.p6spy.engine.spy.P6SpyDriver
          url: jdbc:p6spy:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
      
      
      

      在 resources 目录下创建 spy.properties 文件,并配置如下内容:

      #3.2.1以上使用
      modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
      #3.2.1以下使用或者不配置
      #modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
      # 自定义日志打印
      logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
      #日志输出到控制台
      appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
      # 使用日志系统记录 sql
      #appender=com.p6spy.engine.spy.appender.Slf4JLogger
      # 设置 p6spy driver 代理
      deregisterdrivers=true
      # 取消JDBC URL前缀
      useprefix=true
      # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
      excludecategories=info,debug,result,commit,resultset
      # 日期格式
      dateformat=yyyy-MM-dd HH:mm:ss
      # 实际驱动可多个
      #driverlist=org.h2.Driver
      # 是否开启慢SQL记录
      outagedetection=true
      # 慢SQL记录标准 2 秒
      outagedetectioninterval=2
      

      注意

      • driver-class-name 为 p6spy提供的驱动类
      • url 前缀为 jdbc:p6spy 跟着冒号为对应数据库连接地址
      • 打印出 sql 为null,在 excludecategories 增加 commit
      • 批量操作不打印 sql,去除 excludecategories 中的 batch
      • 批量操作打印重复的问题请使用 MybatisPlusLogFactory (3.2.1新增)
      • 该插件有性能损耗,不建议生产环境使用
    3. 简单使用

      执行之前查询语句,查看控制台日志输出

      控制台中输出了sql语句以及执行时间,查找资料好像没有找到慢 SQL 可以是小输,所以这里无法演示慢SQL 执行时的控制台输出。MP 以前自带的性能分析插件慢SQL标准是可以精确到毫秒级别的

      image-20200801095623972

    8. 条件构造器

    8.1 说明

    • 以下出现的第一个入参 boolean condition 表示该条件是否加入最后生成的 sql 中
    • 以下代码块内的那个方法均为从上往下补全个别 boolean 类型的入参,默认为 true
    • 以下出现的泛型 Param 均为 wrapper 的子类实例(均具有 AbstractWrapper的所有方法)
    • 以下方法在入参中出现的 R 为泛型,在普通 wrapper 中式 String ,在 LambdaWrapper 中是函数(例:Entity::getIdEntity为实体类,getId为字段 idget(Method)
    • 以下方法入参中 R column 均表示数据库字段。当 R 具体类型为 String 时则为数据库字段名(字段名是数据库关键字的自己用转义符包裹!)而不是具体实体类数据字段名!!!,另当 R 具体类型为 SFunction 时项目 runtime 不支持 eclipse 自家的编译器!!!
    • 以下举例为使用 普通 wrapper,入参为 maplist 的均以 json 形式表现!
    • 使用中如果入参 map 或者 list 为空 ,则不会加入最后生成的 sql 中!!!

    8.2 警告

    不支持以及不赞成在 RPC 调用中把 Wrapper 进行传输

    1. wrapper 很重
    2. 传输 wrapper 可以类比为你的 controller 用 map 接受值(开发一时爽,维护火葬场)
    3. 正确的 RPC 调用模式时写一个 DTO 进行传输,被调用方再根据 DTO 执行响应的操作
    4. 我们拒绝接收任何关于 RPC 传输 Wrapper 报错相关的 issue 甚至 pr

    8.3 AbstractWrapper

    说明

    QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapepr)的父类用于生成 sql 的 where 条件,entity 属性也用于生成 sql 的 where 条件

    注意:entity 生成的 where 条件与使用各个 api 生成的 where 条件没有任何关联行为

    allEq

    allEq(Map<R, V> params)
    allEq(Map<R, V> params, boolean null2IsNull)
    allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
    
    • 全部 eq(或个别isNull)

    个别参数说明

    paramskey为数据库字段名,value为字段值

    null2IsNull:为 true 则再 mapvaluenull 时调用 isNull 方法, 为 false 时则忽略valuenull

    • 例1: allEq({id:1,name:"老王",age:null})--->id = 1 and name = '老王' and age is null
    • 例2: allEq({id:1,name:"老王",age:null}, false)--->id = 1 and name = '老王'
    allEq(BiPredicate<R, V> filter, Map<R, V> params)
    allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
    allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull) 
    

    个别参数说明

    filter:过滤函数,是否允许字段传入比对条件中

    paramsnull2IsNull:同上

    • 例1: allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null})--->name = '老王' and age is null
    • 例2: allEq((k,v) -> k.indexOf("a") >= 0, {id:1,name:"老王",age:null}, false)--->name = '老王'

    eq

    eq(R column, Object val)
    eq(boolean condition, R column, Object val)
    
    • 等于 =
    • 例: eq("name","老王")--->name='老王'

    ne

    ne(R column, Object val)
    ne(boolean condition, R column, Object val)
    
    • 不等于 <>
    • 例: ne("name", "老王")--->name <> '老王'

    gt

    ge(R column, Object val)
    ge(boolean condition, R column, Object val)
    
    • 大于 >
    • 例: gt("age", 18)--->age > 18

    ge

    ge(R column, Object val)
    ge(boolean condition, R column, Object val)
    
    • 大于等于 >=
    • 例: ge("age", 18)--->age >= 18

    lt

    lt(R column, Object val)
    lt(boolean condition, R column, Object val)
    
    • 小于 <
    • 例: lt("age", 18)--->age < 18

    le

    le(R column, Object val)
    le(boolean condition, R column, Object val)
    
    • 小于等于 <=
    • 例: le("age", 18)--->age <= 18

    between

    between(R column, Object val1, Object val2)
    between(boolean condition, R column, Object val1, Object val2)
    
    • BETWEEN 值1 AND 值2
    • 例: between("age", 18, 30)--->age between 18 and 30

    noBetween

    notBetween(R column, Object val1, Object val2)
    notBetween(boolean condition, R column, Object val1, Object val2)
    
    • NOT BETWEEN 值1 AND 值2
    • 例: notBetween("age", 18, 30)--->age not between 18 and 30

    like

    like(R column, Object val)
    like(boolean condition, R column, Object val)
    
    • LIKE '%值%'
    • 例: like("name", "王")--->name like '%王%'

    notLike

    notLike(R column, Object val)
    notLike(boolean condition, R column, Object val)
    
    • NOT LIKE '%值%'
    • 例: notLike("name", "王")--->name not like '%王%'

    LikeLeft

    likeLeft(R column, Object val)
    likeLeft(boolean condition, R column, Object val)
    
    • LIKE '%值'
    • 例: likeLeft("name", "王")--->name like '%王'

    likeRight

    likeRight(R column, Object val)
    likeRight(boolean condition, R column, Object val)
    
    • LIKE '值%'
    • 例: likeRight("name", "王")--->name like '王%'

    isNull

    isNull(R column)
    isNull(boolean condition, R column)
    
    • 字段 IS NULL
    • 例: isNull("name")--->name is null

    isNotNull

    isNotNull(R column)
    isNotNull(boolean condition, R column)
    
    • 字段 IS NOT NULL
    • 例: isNotNull("name")--->name is not null

    in

    in(R column, Collection<?> value)
    in(boolean condition, R column, Collection<?> value)
    
    • 字段 IN (value.get(0), value.get(1), ...)
    • 例: in("age",{1,2,3})--->age in (1,2,3)
    in(R column, Object... values)
    in(boolean condition, R column, Object... values)
    
    • 字段 IN (v0, v1, ...)
    • 例: in("age", 1, 2, 3)--->age in (1,2,3)

    notIn

    notIn(R column, Collection<?> value)
    notIn(boolean condition, R column, Collection<?> value)
    
    • 字段 NOT IN (value.get(0), value.get(1), ...)
    • 例: notIn("age",{1,2,3})--->age not in (1,2,3)
    notIn(R column, Object... values)
    notIn(boolean condition, R column, Object... values)
    
    • 字段 NOT IN (v0, v1, ...)
    • 例: notIn("age", 1, 2, 3)--->age not in (1,2,3)

    inSql

    inSql(R column, String inValue)
    inSql(boolean condition, R column, String inValue)
    
    • 字段 IN ( sql语句 )
    • 例: inSql("age", "1,2,3,4,5,6")--->age in (1,2,3,4,5,6)
    • 例: inSql("id", "select id from table where id < 3")--->id in (select id from table where id < 3)

    notInSql

    notInSql(R column, String inValue)
    notInSql(boolean condition, R column, String inValue)
    
    • 字段 NOT IN ( sql语句 )
    • 例: notInSql("age", "1,2,3,4,5,6")--->age not in (1,2,3,4,5,6)
    • 例: notInSql("id", "select id from table where id < 3")--->id not in (select id from table where id < 3)

    groupBy

    groupBy(R... columns)
    groupBy(boolean condition, R... columns)
    
    • 分组:GROUP BY 字段, ...
    • 例: groupBy("id", "name")--->group by id,name

    orderByAsc

    orderByAsc(R... columns)
    orderByAsc(boolean condition, R... columns)
    
    • 排序:ORDER BY 字段, ... ASC
    • 例: orderByAsc("id", "name")--->order by id ASC,name ASC

    orderByDesc

    orderByDesc(R... columns)
    orderByDesc(boolean condition, R... columns)
    
    • 排序:ORDER BY 字段, ... DESC
    • 例: orderByDesc("id", "name")--->order by id DESC,name DESC

    orderBy

    orderBy(boolean condition, boolean isAsc, R... columns)
    
    • 排序:ORDER BY 字段, ...
    • 例: orderBy(true, true, "id", "name")--->order by id ASC,name ASC

    having

    having(String sqlHaving, Object... params)
    having(boolean condition, String sqlHaving, Object... params)
    
    • HAVING ( sql语句 )
    • 例: having("sum(age) > 10")--->having sum(age) > 10
    • 例: having("sum(age) > {0}", 11)--->having sum(age) > 11

    func

    func(Consumer<Children> consumer)
    func(boolean condition, Consumer<Children> consumer)
    
    • func 方法(主要方便在出现if...else下调用不同方法能不断链)
    • 例: func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})

    or

    or()
    or(boolean condition)
    

    拼接 OR

    注意事项

    主动调用 or 表示紧接着下一个方法不是用 and 连接!(不调用 or 则默认为使用 and 连接)

    • 例: eq("id",1).or().eq("name","老王")--->id = 1 or name = '老王'
    or(Consumer<Param> consumer)
    or(boolean condition, Consumer<Param> consumer)
    
    • OR 嵌套
    • 例: or(i -> i.eq("name", "李白").ne("status", "活着"))--->or (name = '李白' and status <> '活着')

    and

    and(Consumer<Param> consumer)
    and(boolean condition, Consumer<Param> consumer)
    
    • AND 嵌套
    • 例: and(i -> i.eq("name", "李白").ne("status", "活着"))--->and (name = '李白' and status <> '活着')

    nest

    nested(Consumer<Param> consumer)
    nested(boolean condition, Consumer<Param> consumer)
    
    • 正常嵌套 不带 AND 或者 OR
    • 例: nested(i -> i.eq("name", "李白").ne("status", "活着"))--->(name = '李白' and status <> '活着')

    apply

    apply(String applySql, Object... params)
    apply(boolean condition, String applySql, Object... params)
    
    • 拼接 sql

      注意事项

      该方法可用于数据库函数动态入参得 params 对应得前面 applySql 内部得(index)部分。这样是不会有 sql 注入风险的,反之会有!

    • 例: apply("id = 1")--->id = 1

    • 例: apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")

    • 例: apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08")--->date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")

    last

    last(String lastSql)
    last(boolean condition, String lastSql)
    
    • 无视优化规则直接拼接到 sql 的最后

      注意事项

      只能调用一次,多次调用以最后一次为准,有 sql 注入的风险,请谨慎使用

    • 例: last("limit 1")

    exists

    exists(String existsSql)
    exists(boolean condition, String existsSql)
    
    • 拼接 EXISTS ( sql语句 )
    • 例: exists("select id from table where age = 1")--->exists (select id from table where age = 1)

    notExists

    notExists(String notExistsSql)
    notExists(boolean condition, String notExistsSql)
    
    • 拼接 NOT EXISTS ( sql语句 )
    • 例: notExists("select id from table where age = 1")--->not exists (select id from table where age = 1)

    QueryWrapper

    说明

    继承自 AbstractWrapper,自身的内部属性 entity 也用于生成 where 条件及 LambdaQueryWrapper ,可以通过 new QueryWrapper().lambda() 方法获取

    select

    select(String... sqlSelect)
    select(Predicate<TableFieldInfo> predicate)
    select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
    

    设置查询字段

    说明

    以上方法为两类

    第二类方法为过滤器查询字段(主键除外),入参不包含 class 的调用前需要 wrapper 内的 entity 属性有值!这两类方法重复调用以最后一次为准

    • 例: select("id", "name", "age")
    • 例: select(i -> i.getProperty().startsWith("test"))

    UpdateWrapper

    说明

    继承自 AbstractWrapper,自身的内部属性 entity 也用于生成 where 条件及 LambdaUpdateWrapper,可以通过 new UpdateWrapper().lambda() 方法获取!

    set

    set(String column, Object val)
    set(boolean condition, String column, Object val)
    
    • SQL SET 字段
    • 例: set("name", "老李头")
    • 例: set("name", "")--->数据库字段值变为空字符串
    • 例: set("name", null)--->数据库字段值变为null

    setSql

    setSql(String sql)
    
    • 设置 SET 部分 SQL
    • 例: setSql("name = '老李头'")

    lambda

    获取 LambdaWrapper
    QueryWrapper中是获取LambdaQueryWrapper
    UpdateWrapper中是获取LambdaUpdateWrapper

    使用 Wrapper 自定义 SQL

    需求来源

    再使用了 mybatis-plus 之后,自定义 SQL 的同时也想使用 wrapper 的便利应该怎么办?在 mybatis-plus 版本 3.0.7得到了完美解决。版本需要大于或等于 3.0.7 ,以下两种方案取其一即可

    Service.java

    mysqlMapper.getAll(Wrappers.<MysqlData>lambdaQuery().eq(MysqlData::getGroup, 1));
    

    Mapper.xml

    <select id="getAll" resultType="MysqlData">
    	SELECT * FROM mysql_data ${ew.customSqlSegment}
    </select>
    

    简单示例

    条件构造器的例子上面 CRUD 中有部分了,下面再写几个例子来找点感觉

    @SpringBootTest
    public class WrapperTest {
    
        @Autowired
        UserMapper userMapper;
    
        // 查询 name 不为空的,并且邮箱不为空,年龄大于12的用户
        @Test
        void test1(){
            // 创建 QueryWrapper
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            // 构建条件,wrapper是链式编程,所以可以一直 .下去
            // isNotNull 有两个重载方法,下面调用的这个方法的第一个参数表示的是数据库表中的字段名
            // gt 用来拼接 > 条件,第一个参数为数据库表中的字段名,第二个是 > 号后的值
            wrapper
                    .isNotNull("name")
                    .isNotNull("email")
                    .gt("age",3);
            // 执行查询操作,输出查询结果
            userMapper.selectList(wrapper).forEach(System.out::println);
        }
    
        // 查询名字等于李四的,且年龄小于30的用户
        @Test
        void test2(){
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            // eq 拼接 = 条件,第一个参数为数据库表中的字段名,第二个是 = 后的值
            // lt 拼接 < 条件,第一个参数为数据库表中的字段名,第二个是 《 后的值
            wrapper
                    .eq("name","李四")
                    .lt("age",30);
            // selectOne 查询一个结果,如果返回多个结果,使用 selectMaps 或 selectList
            System.out.println(userMapper.selectOne(wrapper));
        }
    
        // 查询年龄再 20-30 岁之间的用户
        @Test
        void test3(){
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            // between 拼接 between and 条件,第一个参数表示数据库中的字段名,第二个参数为最小值,第三个参数为最大值
            wrapper.between("age",20,30);
            userMapper.selectList(wrapper).forEach(System.out::println);
        }
    
        // 模糊查询
        // 名字当中不包含 张 的,且是 李 开头的用户
        @Test
        void test4(){
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            // notLike 拼接 not like 条件,第一个参数为数据库表中的字段名,第二个是 not like 后的值,它会自动拼接 %  % 比如这里是 %张%
            // likeRight ,拼接 like 条件,第一个参数为数据库表中的字段名,第二个是 like后的值,它会再第二个参数后拼接 % 比如这里是 李%
            wrapper
                    .notLike("name","张")
                    .likeRight("name","李");
    
            List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
            maps.forEach(System.out::println);
        }
    
        // 查询用户 id 小于2 的用户
        @Test
        void test5(){
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            // inSql 在 in 条件后拼接 sql 语句,比如下面的这个执行后的语句为
            // SELECT id,name,age,email,create_time,modified_time,version,deleted FROM user WHERE deleted=0 AND (id IN (select id from user where id < 2))
            wrapper.inSql("id","select id from user where id < 2");
            userMapper.selectList(wrapper).forEach(System.out::println);
        }
    
        // 通过 id 进行排序
        @Test
        void test6(){
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            // orderByDesc sql 在末尾添加降序排序,参数是可变参数,参数都是数据库中字段名
            wrapper.orderByDesc("id");
            userMapper.selectList(wrapper).forEach(System.out::println);
        }
    
    }
    

    Wrapper 十分重要,我们需要执行复杂 sql 语句时需要使用到,可以多加练习,找到规律来使用 Wrapper ,这样会让你的开发更加快速高效

    9. 代码生成器

    AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。

    也就是说,MP 的代码生成器可以节省我们写很多重复繁琐的代码,快速搭建项目。

    1. 创建 user 表,或者使用我们刚刚的 user 表

      如果是用刚刚的 user 表,最好加上注释,这样我们等下自动生成 Swagger 注解时就可以看到很明显的效果

      DROP TABLE IF EXISTS `user`;
      CREATE TABLE `user`  (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
        `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
        `age` int(11) NULL DEFAULT NULL COMMENT '年龄',
        `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
        `modified_time` datetime(6) NULL DEFAULT NULL COMMENT '修改时间',
        `create_time` datetime(6) NULL DEFAULT NULL COMMENT '创建时间',
        `version` int(1) NULL DEFAULT 1 COMMENT '版本号(乐观锁)',
        `deleted` int(1) NULL DEFAULT 0,
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
      
      INSERT INTO `user` VALUES (1, '张三', 3, 'test1@baomidou.com', '2020-07-31 14:10:17.902000', '2020-07-31 14:01:47.161240', 1, 0);
      INSERT INTO `user` VALUES (2, '李四', 18, 'test2@baomidou.com', '2020-07-31 14:10:17.902000', '2020-07-31 14:01:47.161240', 4, 0);
      INSERT INTO `user` VALUES (8, '王五', 18, 'xp@qq.com', '2020-07-31 14:10:17.902000', '2020-07-31 14:01:47.161240', 1, 0);
      
    2. 创建一个新的 SpringBoot 项目

    3. 引入依赖

      引入的核心依赖是 mybatis-plus-boot-startermysql-connector-javamybatis-plus-generatorvelocity-engine-core。只要存在这四个依赖,则可以使用代码生成器。数据源可以使用 Spring默认的,这里使用 Druid。数据库连接驱动也不一定是要MySQL的。

      <dependencies>
          <!-- mysql -->
          <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <version>5.1.46</version>
          </dependency>
          <!-- druid -->
          <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>druid</artifactId>
              <version>1.1.23</version>
          </dependency>
          <!-- mybatis plus -->
          <dependency>
              <groupId>com.baomidou</groupId>
              <artifactId>mybatis-plus-boot-starter</artifactId>
              <version>3.3.2</version>
          </dependency>
          <!-- lombok -->
          <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
          </dependency>
          <!-- mybatis-plus-generator -->
          <dependency>
              <groupId>com.baomidou</groupId>
              <artifactId>mybatis-plus-generator</artifactId>
              <version>3.3.2</version>
          </dependency>
          <!-- swagger-ui -->
          <dependency>
              <groupId>io.springfox</groupId>
              <artifactId>springfox-swagger-ui</artifactId>
              <version>2.9.2</version>
          </dependency>
          <!-- swagger2 -->
          <dependency>
              <groupId>io.springfox</groupId>
              <artifactId>springfox-swagger2</artifactId>
              <version>2.9.2</version>
          </dependency>
          <!-- velocity 模板引擎 -->
          <dependency>
              <groupId>org.apache.velocity</groupId>
              <artifactId>velocity-engine-core</artifactId>
              <version>2.2</version>
          </dependency>
      
          <!-- SpringBoot 相关依赖 -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-test</artifactId>
              <scope>test</scope>
          </dependency>
      </dependencies>
      
    4. 配置 application.xml

      配置数据源

      spring:
        datasource:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
          password: root
          username: root
          # 切换成 Druid 数据源
          type: com.alibaba.druid.pool.DruidDataSource
          #Spring Boot 默认是不注入这些属性值的,需要自己绑定
          #druid 数据源专有配置
          initialSize: 5
          minIdle: 5
          maxActive: 20
          maxWait: 60000
          timeBetweenEvictionRunsMillis: 60000
          minEvictableIdleTimeMillis: 300000
          validationQuery: SELECT 1 FROM DUAL
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          poolPreparedStatements: true
      
          #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
          #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
          #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
          filters: stat,wall,log4j
          maxPoolPreparedStatementPerConnectionSize: 20
          useGlobalDataSourceStat: true
          connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      
    5. 编写代码生成器

      根据步骤,先创建代码生成器对象。然后 全局配置 => 配置数据源 => 包的配置 => 策略配置

       @Test
       void myBatisPlusGenerator(){
           // 创建 AutoGenerator 对象
           AutoGenerator autoGenerator = new AutoGenerator();
           // 1. 设置全局配置
           GlobalConfig globalConfig = new GlobalConfig();
           globalConfig
                   .setOutputDir(System.getProperty("user.dir")+"/src/main/java")  // 设置输出的路径
                   .setOpen(false)                     // 是否打开目录
                   .setIdType(IdType.AUTO)             // 设置主键策略
                   .setAuthor("xp")                    // 自动设置作者名
                   .setFileOverride(false)             // 是否覆盖
                   .setEntityName("")                  // 设置entity 实体类名字
                   .setControllerName("%sController")  // 设置Controller名字
                   .setServiceName("%sService")        // 设置Service 接口名字
                   .setServiceImplName("%sServiceImpl")// 设置Service 接口实现类名字
                   .setMapperName("%sMapper")          // 设置Mapper 接口名字
                   .setBaseResultMap(true)             // 是否创建resultMap
                   .setSwagger2(true);                 // 设置开启 swagger2 注解
           autoGenerator.setGlobalConfig(globalConfig);    // 将全局配置注入到代码生成器中
      
           // 2. 配置数据源
           DataSourceConfig sourceConfig = new DataSourceConfig();
           sourceConfig
                   .setDbType(DbType.MYSQL)
                   .setDriverName("com.mysql.jdbc.Driver")
                   .setUrl("jdbc:mysql://localhost:3306/ebook?useUnicode=true&useSSL=false&characterEncoding=utf8")
                   .setUsername("root")
                   .setPassword("root");
           autoGenerator.setDataSource(sourceConfig);  // 将数据源配置注入到代码生成器中
      
           // 3. 包配置
           PackageConfig packageConfig = new PackageConfig();
           packageConfig
                   .setParent("com.xp");               // 设置父包名
           autoGenerator.setPackageInfo(packageConfig);// 将包配置注入到代码生成器中
           // 配置 xml 自动输出路径
           String templatePath = "/templates/mapper.xml.vm";   // 配置模板引擎路径
           InjectionConfig cfg = new InjectionConfig() {
               @Override
               public void initMap() {
                   // to do nothing
               }
           };
           // 自定义输出配置
           List<FileOutConfig> focList = new ArrayList<>();
           // 自定义配置会被优先输出
           focList.add(new FileOutConfig(templatePath) {
               @Override
               public String outputFile(TableInfo tableInfo) {
                   // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                   return System.getProperty("user.dir") +"/src/main/resources/mapper/"
                           + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
               }
           });
           cfg.setFileOutConfigList(focList);
           autoGenerator.setCfg(cfg);
           // 配置模板
           TemplateConfig templateConfig = new TemplateConfig();
           // 配置自定义输出模板
           templateConfig.setXml(null);                // 取消默认的 xml 路径
           autoGenerator.setTemplate(templateConfig);  // 将模板注入到代码生成器中
           autoGenerator.setTemplateEngine(new VelocityTemplateEngine()); // 将模板引擎注入到代码生成器中
      
           // 4. 配置策略
           StrategyConfig strategyConfig = new StrategyConfig();
           TableFill createTime = new TableFill("create_time", FieldFill.INSERT);
           TableFill modifiedTime = new TableFill("modified_time", FieldFill.INSERT_UPDATE);
           strategyConfig
                 //.setTablePrefix("ebook_")                                   // 去掉表名前缀
                   .setInclude("user")                                         // 设置需要映射的表名
                   .setRestControllerStyle(true)                               // Controller 添加 RestController 注解
                   .setEntityLombokModel(true)                                 // 是否使用 lombok
                   .setEntityTableFieldAnnotationEnable(true)                  // 是否生成实体时,生成字段注解
                   .setVersionFieldName("version")                             // 设置乐观锁版本字段
                   .setTableFillList(Arrays.asList(createTime,modifiedTime))   // 设置自动填充规则
                   .setLogicDeleteFieldName("deleted")                         // 设置逻辑删除字段
                   .setNaming(NamingStrategy.underline_to_camel)               // 数据库映射到实体类的名字为驼峰命名
                   .setColumnNaming(NamingStrategy.underline_to_camel);        // 字段名映射驼峰命名
           autoGenerator.setStrategy(strategyConfig);  // 将配置策略注入到代码生成器中
      
           // 5. 执行代码生成器
           autoGenerator.execute();
       }
      
    6. 运行代码生成器

      运行代码生成器,然后我们会发现我们的项目多出了包和类,如下图:

      image-20200801155828518

      点进去类里,我们会发现它里面已经帮我们全部写好了

    7. 编写 Controller 接口

      在我们自动生成的 UserController 类中增加一个hello 的接口

      /**
       * <p>
       *  前端控制器
       * </p>
       *
       * @author xp
       * @since 2020-08-01
       */
      @RestController
      @RequestMapping("/user")
      public class UserController {
      
          @Autowired
          UserService userService;
      
          @RequestMapping("/hello")
          public String hello(){
              return userService.getById(2).toString();
          }
      
      }
      
    8. 测试访问接口

      我们通过浏览器访问我们刚刚编写好的接口,输出我们数据库中的信息则代表代码生成器已经自动生成成功了

      image-20200801160119193

  • 相关阅读:
    Zend Guard 7 , Zend Guard Loader处理PHP加密
    [转]pHP源码加密方法调查
    使用USB Key(加密狗)实现身份认证
    4*4行列式矩阵键盘
    HDU-2546-饭卡
    HDU-2073-无限的路
    HDU-2065-"红色病毒"问题
    HDU-2063-过山车
    HDU-2060-Snooker
    HDU-2056-Rectangles
  • 原文地址:https://www.cnblogs.com/windowsxpxp/p/13418314.html
Copyright © 2020-2023  润新知