关于mybatis-plus:mybatis-plus-看这篇就够了一发入魂

7次阅读

共计 40770 个字符,预计需要花费 102 分钟才能阅读完成。

mybatis-plus 是一款 Mybatis 加强工具,用于简化开发,提高效率。下文应用缩写 mp 来简化示意mybatis-plus,本文次要介绍 mp 搭配 SpringBoot 的应用。

注:本文应用的 mp 版本是以后最新的 3.4.2,晚期版本的差别请自行查阅文档

官方网站:baomidou.com/

疾速入门

  1. 创立一个 SpringBoot 我的项目
  2. 导入依赖
   <!-- pom.xml -->
   <?xml version="1.0" encoding="UTF-8"?>
   <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
       <modelVersion>4.0.0</modelVersion>
       <parent>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-parent</artifactId>
           <version>2.3.4.RELEASE</version>
           <relativePath/> <!-- lookup parent from repository -->
       </parent>
       <groupId>com.example</groupId>
       <artifactId>mybatis-plus</artifactId>
       <version>0.0.1-SNAPSHOT</version>
       <name>mybatis-plus</name>
       <properties>
           <java.version>1.8</java.version>
       </properties>
       <dependencies>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter</artifactId>
           </dependency>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-test</artifactId>
               <scope>test</scope>
           </dependency>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-configuration-processor</artifactId>
           </dependency>
           <dependency>
               <groupId>com.baomidou</groupId>
               <artifactId>mybatis-plus-boot-starter</artifactId>
               <version>3.4.2</version>
           </dependency>
           <dependency>
               <groupId>mysql</groupId>
               <artifactId>mysql-connector-java</artifactId>
               <scope>runtime</scope>
           </dependency>
           <dependency>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
           </dependency>
       </dependencies>
       <build>
           <plugins>
               <plugin>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-maven-plugin</artifactId>
               </plugin>
           </plugins>
       </build>
   </project>
  1. 配置数据库
   # application.yml
   spring:
     datasource:
       driver-class-name: com.mysql.cj.jdbc.Driver
       url: jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai
       username: root
       password: root
       
   mybatis-plus:
     configuration:
       log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启 SQL 语句打印
  1. 创立一个实体类
   package com.example.mp.po;
   import lombok.Data;
   import java.time.LocalDateTime;
   @Data
   public class User {
       private Long id;
       private String name;
       private Integer age;
       private String email;
       private Long managerId;
       private LocalDateTime createTime;
   }
  1. 创立一个 mapper 接口
   package com.example.mp.mappers;
   import com.baomidou.mybatisplus.core.mapper.BaseMapper;
   import com.example.mp.po.User;
   public interface UserMapper extends BaseMapper<User> {}
  1. 在 SpringBoot 启动类上配置 mapper 接口的扫描门路
   package com.example.mp;
   import org.mybatis.spring.annotation.MapperScan;
   import org.springframework.boot.SpringApplication;
   import org.springframework.boot.autoconfigure.SpringBootApplication;
   @SpringBootApplication
   @MapperScan("com.example.mp.mappers")
   public class MybatisPlusApplication {public static void main(String[] args) {SpringApplication.run(MybatisPlusApplication.class, args);
       }
   }
  1. 在数据库中创立表
   DROP TABLE IF EXISTS user;
   CREATE TABLE user (id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键',
   name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
   age INT(11) DEFAULT NULL COMMENT '年龄',
   email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
   manager_id BIGINT(20) DEFAULT NULL COMMENT '直属下级 id',
   create_time DATETIME DEFAULT NULL COMMENT '创立工夫',
   CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user (id)
   ) ENGINE=INNODB CHARSET=UTF8;
   
   INSERT INTO user (id, name, age ,email, manager_id, create_time) VALUES
   (1, '大 BOSS', 40, 'boss@baomidou.com', NULL, '2021-03-22 09:48:00'),
   (2, '李经理', 40, 'boss@baomidou.com', 1, '2021-01-22 09:48:00'),
   (3, '黄主管', 40, 'boss@baomidou.com', 2, '2021-01-22 09:48:00'),
   (4, '吴组长', 40, 'boss@baomidou.com', 2, '2021-02-22 09:48:00'),
   (5, '小菜', 40, 'boss@baomidou.com', 2, '2021-02-22 09:48:00')
  1. 编写一个 SpringBoot 测试类
   package com.example.mp;
   import com.example.mp.mappers.UserMapper;
   import com.example.mp.po.User;
   import org.junit.Test;
   import org.junit.runner.RunWith;
   import org.springframework.beans.factory.annotation.Autowired;
   import org.springframework.boot.test.context.SpringBootTest;
   import org.springframework.test.context.junit4.SpringRunner;
   import java.util.List;
   import static org.junit.Assert.*;
   @RunWith(SpringRunner.class)
   @SpringBootTest
   public class SampleTest {
       @Autowired
       private UserMapper mapper;
       @Test
       public void testSelect() {List<User> list = mapper.selectList(null);
           assertEquals(5, list.size());
           list.forEach(System.out::println);
       }
   }

筹备工作实现

数据库状况如下

我的项目目录如下

运行测试类

能够看到,针对单表的根本 CRUD 操作,只须要创立好实体类,并创立一个继承自 BaseMapper 的接口即可,堪称十分简洁。并且,咱们留神到,User类中的 managerIdcreateTime 属性,主动和数据库表中的 manager_idcreate_time 对应了起来,这是因为 mp 主动做了数据库下划线命名,到 Java 类的驼峰命名之间的转化。

外围性能

注解

mp 一共提供了 8 个注解,这些注解是用在 Java 的实体类下面的。

  • @TableName

    注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名雷同时,能够不指定该注解。

  • @TableId

    注解在实体类的某一字段上,示意这个字段对应数据库表的主键 。当主键名为 id 时(表中列名为 id,实体类中字段名为 id),无需应用该注解显式指定主键,mp 会主动关联。若类的字段名和表的列名不统一,可用value 属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略。

  • @TableField

    注解在某一字段上,指定 Java 实体类的字段和数据库表的列的映射关系。这个注解有如下几个利用场景。

    • 排除非表字段

      若 Java 实体类中某个字段,不对应表中的任何列,它只是用于保留一些额定的,或组装后的数据,则能够设置 exist 属性为 false,这样在对实体对象进行插入时,会疏忽这个字段。排除非表字段也能够通过其余形式实现,如应用statictransient关键字,但集体感觉不是很正当,不做赘述

    • 字段验证策略

      通过 insertStrategyupdateStrategywhereStrategy 属性进行配置,能够管制在实体对象进行插入,更新,或作为 WHERE 条件时,对象中的字段要如何组装到 SQL 语句中。

    • 字段填充策略

      通过 fill 属性指定,字段为空时会进行主动填充

  • @Version

    乐观锁注解

  • @EnumValue

    注解在枚举字段上

  • @TableLogic

    逻辑删除

  • KeySequence

    序列主键策略(oracle

  • InterceptorIgnore

    插件过滤规定

CRUD 接口

mp 封装了一些最根底的 CRUD 办法,只须要间接继承 mp 提供的接口,无需编写任何 SQL,即可食用。mp 提供了两套接口,别离是 Mapper CRUD 接口和 Service CRUD 接口。并且 mp 还提供了条件结构器Wrapper,能够不便地组装 SQL 语句中的 WHERE 条件。

Mapper CRUD 接口

只需定义好实体类,而后创立一个接口,继承 mp 提供的 BaseMapper,即可食用。mp 会在 mybatis 启动时,主动解析实体类和表的映射关系,并注入带有通用 CRUD 办法的 mapper。BaseMapper 里提供的办法,局部列举如下:

  • insert(T entity) 插入一条记录
  • deleteById(Serializable id) 依据主键 id 删除一条记录
  • delete(Wrapper<T> wrapper) 依据条件结构器 wrapper 进行删除
  • selectById(Serializable id) 依据主键 id 进行查找
  • selectBatchIds(Collection idList) 依据主键 id 进行批量查找
  • selectByMap(Map<String,Object> map) 依据 map 中指定的列名和列值进行 等值匹配 查找
  • selectMaps(Wrapper<T> wrapper) 依据 wrapper 条件,查问记录,将查问后果封装为一个 Map,Map 的 key 为后果的列,value 为值
  • selectList(Wrapper<T> wrapper) 依据条件结构器 wrapper 进行查问
  • update(T entity, Wrapper<T> wrapper) 依据条件结构器 wrapper 进行更新
  • updateById(T entity)

上面解说几个比拟特地的办法

selectMaps

BaseMapper接口还提供了一个 selectMaps 办法,这个办法会将查问后果封装为一个 Map,Map 的 key 为后果的列,value 为值

该办法的应用场景如下:

  • 只查局部列

    当某个表的列特地多,而 SELECT 的时候只须要选取个别列,查问出的后果也没必要封装成 Java 实体类对象时(只查局部列时,封装成实体后,实体对象中的很多属性会是 null),则能够用selectMaps,获取到指定的列后,再自行进行解决即可

    比方

      @Test
      public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
          wrapper.select("id","name","email").likeRight("name","黄");
          List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
          maps.forEach(System.out::println);
      }

  • 进行数据统计

    比方

  // 依照直属下级进行分组,查问每组的平均年龄,最大年龄,最小年龄
  /**
  select avg(age) avg_age ,min(age) min_age, max(age) max_age from user group by manager_id having sum(age) < 500;
  **/
  
  @Test
  public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
      wrapper.select("manager_id", "avg(age) avg_age", "min(age) min_age", "max(age) max_age")
              .groupBy("manager_id").having("sum(age) < {0}", 500);
      List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
      maps.forEach(System.out::println);
  }

selectObjs

只会返回第一个字段(第一列)的值,其余字段会被舍弃

比方

    @Test
    public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.select("id", "name").like("name", "黄");
        List<Object> objects = userMapper.selectObjs(wrapper);
        objects.forEach(System.out::println);
    }

失去的后果,只封装了第一列的 id

selectCount

查问满足条件的总数,留神,应用这个办法,不能调用 QueryWrapperselect办法设置要查问的列了。这个办法会主动增加select count(1)

比方

    @Test
    public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.like("name", "黄");

        Integer count = userMapper.selectCount(wrapper);
        System.out.println(count);
    }
复制代码

Service CRUD 接口

另外一套 CRUD 是 Service 层的,只须要编写一个接口,继承 IService,并创立一个接口实现类,即可食用。(这个接口提供的 CRUD 办法,和 Mapper 接口提供的性能大同小异, 比拟显著的区别在于 IService 反对了更多的批量化操作 ,如saveBatchsaveOrUpdateBatch 等办法。

食用示例如下

  1. 首先,新建一个接口,继承IService
   package com.example.mp.service;
   
   import com.baomidou.mybatisplus.extension.service.IService;
   import com.example.mp.po.User;
   
   public interface UserService extends IService<User> {}
  1. 创立这个接口的实现类,并继承 ServiceImpl,最初打上@Service 注解,注册到 Spring 容器中,即可食用
   package com.example.mp.service.impl;
   
   import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
   import com.example.mp.mappers.UserMapper;
   import com.example.mp.po.User;
   import com.example.mp.service.UserService;
   import org.springframework.stereotype.Service;
   
   @Service
   public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}
  1. 测试代码
   package com.example.mp;
   
   import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
   import com.baomidou.mybatisplus.core.toolkit.Wrappers;
   import com.example.mp.po.User;
   import com.example.mp.service.UserService;
   import org.junit.Test;
   import org.junit.runner.RunWith;
   import org.springframework.beans.factory.annotation.Autowired;
   import org.springframework.boot.test.context.SpringBootTest;
   import org.springframework.test.context.junit4.SpringRunner;
   @RunWith(SpringRunner.class)
   @SpringBootTest
   public class ServiceTest {
       @Autowired
       private UserService userService;
       @Test
       public void testGetOne() {LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery();
           wrapper.gt(User::getAge, 28);
           User one = userService.getOne(wrapper, false); // 第二参数指定为 false, 使得在查到了多行记录时, 不抛出异样, 而返回第一条记录
           System.out.println(one);
       }
   }
  1. 后果

另,IService也反对链式调用,代码写起来十分简洁,查问示例如下

    @Test
    public void testChain() {List<User> list = userService.lambdaQuery()
                .gt(User::getAge, 39)
                .likeRight(User::getName, "王")
                .list();
        list.forEach(System.out::println);
    }

更新示例如下

    @Test
    public void testChain() {userService.lambdaUpdate()
                .gt(User::getAge, 39)
                .likeRight(User::getName, "王")
                .set(User::getEmail, "w39@baomidou.com")
                .update();}

删除示例如下

    @Test
    public void testChain() {userService.lambdaUpdate()
                .like(User::getName, "青蛙")
                .remove();}

条件结构器

mp 让我感觉极其不便的一点在于其提供了弱小的条件结构器Wrapper,能够十分不便的结构 WHERE 条件。条件结构器次要波及到 3 个类,AbstractWrapperQueryWrapperUpdateWrapper,它们的类关系如下

AbstractWrapper 中提供了十分多的办法用于构建 WHERE 条件,而 QueryWrapper 针对 SELECT 语句,提供了 select() 办法,可自定义须要查问的列,而 UpdateWrapper 针对 UPDATE 语句,提供了 set() 办法,用于结构 set 语句。条件结构器也反对 lambda 表达式,写起来十分舒爽。

上面对 AbstractWrapper 中用于构建 SQL 语句中的 WHERE 条件的办法进行局部列举

  • eq:equals,等于
  • allEq:all equals,全等于
  • ne:not equals,不等于
  • gt:greater than,大于 >
  • ge:greater than or equals,大于等于
  • lt:less than,小于<
  • le:less than or equals,小于等于
  • between:相当于 SQL 中的 BETWEEN
  • notBetween
  • like:含糊匹配。like("name","黄"),相当于 SQL 的name like '% 黄 %'
  • likeRight:含糊匹配右半边。likeRight("name","黄"),相当于 SQL 的name like '黄 %'
  • likeLeft:含糊匹配左半边。likeLeft("name","黄"),相当于 SQL 的name like '% 黄'
  • notLikenotLike("name","黄"),相当于 SQL 的name not like '% 黄 %'
  • isNull
  • isNotNull
  • in
  • and:SQL 连接符 AND
  • or:SQL 连接符 OR
  • apply:用于拼接 SQL,该办法可用于数据库函数,并能够动静传参
  • …….

应用示例

上面通过一些具体的案例来练习条件结构器的应用。(应用前文创立的 user 表)

// 案例先展现须要实现的 SQL 语句,后展现 Wrapper 的写法

// 1. 名字中蕴含佳,且年龄小于 25
// SELECT * FROM user WHERE name like '% 佳 %' AND age < 25
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "佳").lt("age", 25);
List<User> users = userMapper.selectList(wrapper);
// 上面展现 SQL 时,仅展现 WHERE 条件;展现代码时, 仅展现 Wrapper 构建局部

// 2. 姓名为黄姓,且年龄大于等于 20,小于等于 40,且 email 字段不为空
// name like '黄 %' AND age BETWEEN 20 AND 40 AND email is not null
wrapper.likeRight("name","黄").between("age", 20, 40).isNotNull("email");

// 3. 姓名为黄姓,或者年龄大于等于 40,依照年龄降序排列,年龄雷同则依照 id 升序排列
// name like '黄 %' OR age >= 40 order by age desc, id asc
wrapper.likeRight("name","黄").or().ge("age",40).orderByDesc("age").orderByAsc("id");

// 4. 创立日期为 2021 年 3 月 22 日,并且直属下级的名字为李姓
// date_format(create_time,'%Y-%m-%d') = '2021-03-22' AND manager_id IN (SELECT id FROM user WHERE name like '李 %')
wrapper.apply("date_format(create_time,'%Y-%m-%d') = {0}", "2021-03-22")  // 倡议采纳 {index} 这种形式动静传参, 可避免 SQL 注入
                .inSql("manager_id", "SELECT id FROM user WHERE name like' 李 %'");
// 下面的 apply, 也能够间接应用上面这种形式做字符串拼接,但当这个日期是一个内部参数时,这种形式有 SQL 注入的危险
wrapper.apply("date_format(create_time,'%Y-%m-%d') ='2021-03-22'");

// 5. 名字为王姓,并且(年龄小于 40,或者邮箱不为空)// name like '王 %' AND (age < 40 OR email is not null)
wrapper.likeRight("name", "王").and(q -> q.lt("age", 40).or().isNotNull("email"));

// 6. 名字为王姓,或者(年龄小于 40 并且年龄大于 20 并且邮箱不为空)// name like '王 %' OR (age < 40 AND age > 20 AND email is not null)
wrapper.likeRight("name", "王").or(q -> q.lt("age",40)
                        .gt("age",20)
                        .isNotNull("email")
        );

// 7. (年龄小于 40 或者邮箱不为空) 并且名字为王姓
// (age < 40 OR email is not null) AND name like '王 %'
wrapper.nested(q -> q.lt("age", 40).or().isNotNull("email"))
                .likeRight("name", "王");

// 8. 年龄为 30,31,34,35
// age IN (30,31,34,35)
wrapper.in("age", Arrays.asList(30,31,34,35));
// 或
wrapper.inSql("age","30,31,34,35");

// 9. 年龄为 30,31,34,35, 返回满足条件的第一条记录
// age IN (30,31,34,35) LIMIT 1
wrapper.in("age", Arrays.asList(30,31,34,35)).last("LIMIT 1");

// 10. 只选出 id, name 列 (QueryWrapper 特有)
// SELECT id, name FROM user;
wrapper.select("id", "name");

// 11. 选出 id, name, age, email, 等同于排除 manager_id 和 create_time
// 当列特地多, 而只须要排除个别列时, 采纳下面的形式可能须要写很多个列, 能够采纳重载的 select 办法,指定须要排除的列
wrapper.select(User.class, info -> {String columnName = info.getColumn();
            return !"create_time".equals(columnName) && !"manager_id".equals(columnName);
        });

Condition

条件结构器的诸多办法中,均能够指定一个 boolean 类型的参数condition,用来决定该条件是否退出最初生成的 WHERE 语句中,比方

String name = "黄"; // 假如 name 变量是一个内部传入的参数
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个 like 语句到 WHERE 中
// 其实就是对上面代码的简化
if (StringUtils.hasText(name)) {wrapper.like("name", name);
}

实体对象作为条件

调用构造函数创立一个 Wrapper 对象时,能够传入一个实体对象。后续应用这个 Wrapper 时,会以实体对象中的非空属性,构建 WHERE 条件(默认构建 等值匹配 的 WHERE 条件,这个行为能够通过实体类里各个字段上的 @TableField 注解中的 condition 属性进行扭转)

示例如下

    @Test
    public void test3() {User user = new User();
        user.setName("黄主管");
        user.setAge(28);
        QueryWrapper<User> wrapper = new QueryWrapper<>(user);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }
复制代码

执行后果如下。能够看到,是依据实体对象中的非空属性,进行了 等值匹配查问

若心愿针对某些属性,扭转 等值匹配 的行为,则能够在实体类中用 @TableField 注解进行配置,示例如下

package com.example.mp.po;
import com.baomidou.mybatisplus.annotation.SqlCondition;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
    private Long id;
    @TableField(condition = SqlCondition.LIKE)   // 配置该字段应用 like 进行拼接
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    private LocalDateTime createTime;
}

运行上面的测试代码

    @Test
    public void test3() {User user = new User();
        user.setName("黄");
        QueryWrapper<User> wrapper = new QueryWrapper<>(user);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

从下图失去的后果来看,对于实体对象中的 name 字段,采纳了 like 进行拼接

@TableField中配置的 condition 属性实则是一个字符串,SqlCondition类中预约义了一些字符串以供选择

package com.baomidou.mybatisplus.annotation;

public class SqlCondition {
    // 上面的字符串中, %s 是占位符, 第一个 %s 是列名, 第二个 %s 是列的值
    public static final String EQUAL = "%s=#{%s}";
    public static final String NOT_EQUAL = "%s&lt;&gt;#{%s}";
    public static final String LIKE = "%s LIKE CONCAT('%%',#{%s},'%%')";
    public static final String LIKE_LEFT = "%s LIKE CONCAT('%%',#{%s})";
    public static final String LIKE_RIGHT = "%s LIKE CONCAT(#{%s},'%%')";
}

SqlCondition中提供的配置比拟无限,当咱们须要 <>等拼接形式,则须要本人定义。比方

package com.example.mp.po;
import com.baomidou.mybatisplus.annotation.SqlCondition;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
    private Long id;
    @TableField(condition = SqlCondition.LIKE)
    private String name;
    @TableField(condition = "%s &gt; #{%s}")   // 这里相当于大于, 其中 &gt; 是字符实体
    private Integer age;
    private String email;
    private Long managerId;
    private LocalDateTime createTime;
}

测试如下

    @Test
    public void test3() {User user = new User();
        user.setName("黄");
        user.setAge(30);
        QueryWrapper<User> wrapper = new QueryWrapper<>(user);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

从下图失去的后果,能够看出,name属性是用 like 拼接的,而 age 属性是用 > 拼接的

allEq 办法

allEq 办法传入一个map,用来做等值匹配

    @Test
    public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
        Map<String, Object> param = new HashMap<>();
        param.put("age", 40);
        param.put("name", "黄飞飞");
        wrapper.allEq(param);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

当 allEq 办法传入的 Map 中有 value 为 null 的元素时,默认会设置为is null

    @Test
    public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
        Map<String, Object> param = new HashMap<>();
        param.put("age", 40);
        param.put("name", null);
        wrapper.allEq(param);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

若想疏忽 map 中 value 为 null 的元素,能够在调用 allEq 时,设置参数 boolean null2IsNullfalse

    @Test
    public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
        Map<String, Object> param = new HashMap<>();
        param.put("age", 40);
        param.put("name", null);
        wrapper.allEq(param, false);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

若想要在执行 allEq 时,过滤掉 Map 中的某些元素,能够调用 allEq 的重载办法allEq(BiPredicate<R, V> filter, Map<R, V> params)

    @Test
    public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
        Map<String, Object> param = new HashMap<>();
        param.put("age", 40);
        param.put("name", "黄飞飞");
        wrapper.allEq((k,v) -> !"name".equals(k), param); // 过滤掉 map 中 key 为 name 的元素
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

lambda 条件结构器

lambda 条件结构器,反对 lambda 表达式,能够不用像一般条件结构器一样,以字符串模式指定列名,它能够间接以实体类的 办法援用 来指定列。示例如下

    @Test
    public void testLambda() {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(User::getName, "黄").lt(User::getAge, 30);
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

像一般的条件结构器,列名是用字符串的模式指定,无奈在编译期进行列名合法性的查看,这就不如 lambda 条件结构器来的优雅。

另外,还有个 链式 lambda 条件结构器,应用示例如下

    @Test
    public void testLambda() {LambdaQueryChainWrapper<User> chainWrapper = new LambdaQueryChainWrapper<>(userMapper);
        List<User> users = chainWrapper.like(User::getName, "黄").gt(User::getAge, 30).list();
        users.forEach(System.out::println);
    }

更新操作

下面介绍的都是查问操作, 当初来讲更新和删除操作。

BaseMapper中提供了 2 个更新办法

  • updateById(T entity)

    依据入参 entityid(主键)进行更新,对于 entity 中非空的属性,会呈现在 UPDATE 语句的 SET 前面,即 entity 中非空的属性,会被更新到数据库,示例如下

  @RunWith(SpringRunner.class)
  @SpringBootTest
  public class UpdateTest {
      @Autowired
      private UserMapper userMapper;
      @Test
      public void testUpdate() {User user = new User();
          user.setId(2L);
          user.setAge(18);
          userMapper.updateById(user);
      }
  }

  • update(T entity, Wrapper<T> wrapper)

    依据实体 entity 和条件结构器 wrapper 进行更新,示例如下

        @Test
        public void testUpdate2() {User user = new User();
            user.setName("王三蛋");
            LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
            wrapper.between(User::getAge, 26,31).likeRight(User::getName,"吴");
            userMapper.update(user, wrapper);
        }

    额定演示一下,把实体对象传入Wrapper,即用实体对象结构 WHERE 条件的案例

      @Test
      public void testUpdate3() {User whereUser = new User();
          whereUser.setAge(40);
          whereUser.setName("王");
  
          LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(whereUser);
          User user = new User();
          user.setEmail("share@baomidou.com");
          user.setManagerId(10L);
  
          userMapper.update(user, wrapper);
      }

留神到咱们的 User 类中,对 name 属性和 age 属性进行了如下的设置

@Data
public class User {
    private Long id;
    @TableField(condition = SqlCondition.LIKE)
    private String name;
    @TableField(condition = "%s &gt; #{%s}")
    private Integer age;
    private String email;
    private Long managerId;
    private LocalDateTime createTime;
}

执行后果

再额定演示一下,链式 lambda 条件结构器的应用

    @Test
    public void testUpdate5() {LambdaUpdateChainWrapper<User> wrapper = new LambdaUpdateChainWrapper<>(userMapper);
        wrapper.likeRight(User::getEmail, "share")
                .like(User::getName, "飞飞")
                .set(User::getEmail, "ff@baomidou.com")
                .update();}

反思

因为 BaseMapper 提供的 2 个更新办法都是传入一个实体对象去执行更新,这 在须要更新的列比拟多时还好 ,若想要更新的只有那么一列,或者两列,则创立一个实体对象就显得有点麻烦。针对这种状况,UpdateWrapper 提供有 set 办法,能够手动拼接 SQL 中的 SET 语句,此时能够不用传入实体对象,示例如下

    @Test
    public void testUpdate4() {LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
        wrapper.likeRight(User::getEmail, "share").set(User::getManagerId, 9L);
        userMapper.update(null, wrapper);
    }

删除操作

BaseMapper一共提供了如下几个用于删除的办法

  • deleteById 依据主键 id 进行删除
  • deleteBatchIds 依据主键 id 进行批量删除
  • deleteByMap 依据 Map 进行删除(Map 中的 key 为列名,value 为值,依据列和值进行等值匹配)
  • delete(Wrapper<T> wrapper) 依据条件结构器 Wrapper 进行删除

与后面查问和更新的操作大同小异,不做赘述

自定义 SQL

当 mp 提供的办法还不能满足需要时,则能够自定义 SQL。

原生 mybatis

示例如下

  • 注解形式
package com.example.mp.mappers;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mp.po.User;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * @Author yogurtzzz
 * @Date 2021/3/18 11:21
 **/
public interface UserMapper extends BaseMapper<User> {@Select("select * from user")
    List<User> selectRaw();}
  • xml 形式
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mp.mappers.UserMapper">
    <select id="selectRaw" resultType="com.example.mp.po.User">
        SELECT * FROM user
    </select>
</mapper>
package com.example.mp.mappers;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mp.po.User;
import org.apache.ibatis.annotations.Select;
import java.util.List;

public interface UserMapper extends BaseMapper<User> {List<User> selectRaw();
}

应用 xml 时,若 xml 文件与 mapper 接口文件不在同一目录下 ,则须要在application.yml 中配置 mapper.xml 的寄存门路

mybatis-plus:
  mapper-locations: /mappers/*

若有多个中央寄存 mapper,则用数组模式进行配置

mybatis-plus:
  mapper-locations: 
  - /mappers/*
  - /com/example/mp/*

测试代码如下

    @Test
    public void testCustomRawSql() {List<User> users = userMapper.selectRaw();
        users.forEach(System.out::println);
    }

后果

mybatis-plus

也能够应用 mp 提供的 Wrapper 条件结构器,来自定义 SQL

示例如下

  • 注解形式
package com.example.mp.mappers;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.example.mp.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;

public interface UserMapper extends BaseMapper<User> {// SQL 中不写 WHERE 关键字,且固定应用 ${ew.customSqlSegment}
    @Select("select * from user ${ew.customSqlSegment}")
    List<User> findAll(@Param(Constants.WRAPPER)Wrapper<User> wrapper);
}
  • xml 形式
package com.example.mp.mappers;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mp.po.User;
import java.util.List;

public interface UserMapper extends BaseMapper<User> {List<User> findAll(Wrapper<User> wrapper);
}
复制代码
<!-- UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mp.mappers.UserMapper">

    <select id="findAll" resultType="com.example.mp.po.User">
        SELECT * FROM user ${ew.customSqlSegment}
    </select>
</mapper>

分页查问

BaseMapper中提供了 2 个办法进行分页查问,别离是 selectPageselectMapsPage,前者会将查问的后果封装成 Java 实体对象,后者会封装成Map<String,Object>。分页查问的食用示例如下

  1. 创立 mp 的分页拦截器,注册到 Spring 容器中
   package com.example.mp.config;
   import com.baomidou.mybatisplus.annotation.DbType;
   import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
   import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
   import org.springframework.context.annotation.Bean;
   import org.springframework.context.annotation.Configuration;
   
   @Configuration
   public class MybatisPlusConfig {
   
       /** 新版 mp **/
       @Bean
       public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
           interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
           return interceptor;
       }
       /** 旧版 mp 用 PaginationInterceptor **/
   }
  1. 执行分页查问
       @Test
       public void testPage() {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
           wrapper.ge(User::getAge, 28);
           // 设置分页信息, 查第 3 页, 每页 2 条数据
           Page<User> page = new Page<>(3, 2);
           // 执行分页查问
           Page<User> userPage = userMapper.selectPage(page, wrapper);
           System.out.println("总记录数 =" + userPage.getTotal());
           System.out.println("总页数 =" + userPage.getPages());
           System.out.println("以后页码 =" + userPage.getCurrent());
           // 获取分页查问后果
           List<User> records = userPage.getRecords();
           records.forEach(System.out::println);
       }
  1. 后果

  2. 其余

    • 留神到,分页查问总共收回了 2 次 SQL,一次查总记录数,一次查具体数据。若心愿不查总记录数,仅查分页后果 。能够通过Page 的重载构造函数,指定 isSearchCountfalse即可
     public Page(long current, long size, boolean isSearchCount)
  • 在理论开发中,可能遇到 多表联查 的场景,此时 BaseMapper 中提供的单表分页查问的办法无奈满足需要,须要 自定义 SQL,示例如下(应用单表查问的 SQL 进行演示,理论进行多表联查时,批改 SQL 语句即可)

    1. 在 mapper 接口中定义一个函数,接管一个 Page 对象为参数,并编写自定义 SQL
        // 这里采纳纯注解形式。当然,若 SQL 比较复杂,倡议还是采纳 XML 的形式
        @Select("SELECT * FROM user ${ew.customSqlSegment}")
        Page<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
 2. 执行查问
            @Test
            public void testPage2() {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
                wrapper.ge(User::getAge, 28).likeRight(User::getName, "王");
                Page<User> page = new Page<>(3,2);
                Page<User> userPage = userMapper.selectUserPage(page, wrapper);
                System.out.println("总记录数 =" + userPage.getTotal());
                System.out.println("总页数 =" + userPage.getPages());
                userPage.getRecords().forEach(System.out::println);
            }
  1. 后果

AR 模式

ActiveRecord 模式,通过操作实体对象,间接操作数据库表。与 ORM 有点相似。

示例如下

  1. 让实体类 User 继承自Model
   package com.example.mp.po;
   
   import com.baomidou.mybatisplus.annotation.SqlCondition;
   import com.baomidou.mybatisplus.annotation.TableField;
   import com.baomidou.mybatisplus.extension.activerecord.Model;
   import lombok.Data;
   import lombok.EqualsAndHashCode;
   import java.time.LocalDateTime;
   
   @EqualsAndHashCode(callSuper = false)
   @Data
   public class User extends Model<User> {
       private Long id;
       @TableField(condition = SqlCondition.LIKE)
       private String name;
       @TableField(condition = "%s &gt; #{%s}")
       private Integer age;
       private String email;
       private Long managerId;
       private LocalDateTime createTime;
   }
  1. 间接调用实体对象上的办法
       @Test
       public void insertAr() {User user = new User();
           user.setId(15L);
           user.setName("我是 AR 猪");
           user.setAge(1);
           user.setEmail("ar@baomidou.com");
           user.setManagerId(1L);
           boolean success = user.insert(); // 插入
           System.out.println(success);
       }
  1. 后果

其余示例

    // 查问
    @Test
    public void selectAr() {User user = new User();
        user.setId(15L);
        User result = user.selectById();
        System.out.println(result);
    }
    // 更新
    @Test
    public void updateAr() {User user = new User();
        user.setId(15L);
        user.setName("王全蛋");
        user.updateById();}
    // 删除
    @Test
    public void deleteAr() {User user = new User();
        user.setId(15L);
        user.deleteById();}

主键策略

在定义实体类时,用 @TableId 指定主键,而其 type 属性,能够指定主键策略。

mp 反对多种主键策略,默认的策略是基于雪花算法的自增 id。全副主键策略定义在了枚举类 IdType 中,IdType有如下的取值

  • AUTO

    数据库 ID 自增,依赖于数据库。在插入操作生成 SQL 语句时,不会插入主键这一列

  • NONE

    未设置主键类型。若在代码中没有手动设置主键,则会依据 主键的全局策略 主动生成(默认的主键全局策略是基于雪花算法的自增 ID)

  • INPUT

    须要手动设置主键,若不设置。插入操作生成 SQL 语句时,主键这一列的值会是null。oracle 的序列主键须要应用这种形式

  • ASSIGN_ID

    当没有手动设置主键,即实体类中的主键属性为空时,才会主动填充,应用雪花算法

  • ASSIGN_UUID

    当实体类的主键属性为空时,才会主动填充,应用 UUID

  • ….(还有几种是已过期的,就不再列举)

能够针对每个实体类,应用 @TableId 注解指定该实体类的主键策略,这能够了解为 部分策略 。若心愿对所有的实体类,都采纳同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时能够用主键的 全局策略 。只须要在application.yml 进行配置即可。比方,配置了全局采纳自增主键策略

# application.yml
mybatis-plus:
  global-config:
    db-config:
      id-type: auto

上面对不同主键策略的行为进行演示

  • AUTO

    User 上对 id 属性加上注解,而后将 MYSQL 的 user 表批改其主键为自增。

  @EqualsAndHashCode(callSuper = false)
  @Data
  public class User extends Model<User> {@TableId(type = IdType.AUTO)
      private Long id;
      @TableField(condition = SqlCondition.LIKE)
      private String name;
      @TableField(condition = "%s &gt; #{%s}")
      private Integer age;
      private String email;
      private Long managerId;
      private LocalDateTime createTime;
  }

测试

      @Test
      public void testAuto() {User user = new User();
          user.setName("我是青蛙呱呱");
          user.setAge(99);
          user.setEmail("frog@baomidou.com");
          user.setCreateTime(LocalDateTime.now());
          userMapper.insert(user);
          System.out.println(user.getId());
      }

后果

能够看到,代码中没有设置主键 ID,收回的 SQL 语句中也没有设置主键 ID,并且插入完结后,主键 ID 会被写回到实体对象。

  • NONE

    在 MYSQL 的 user 表中,去掉主键自增。而后批改 User 类(若不配置 @TableId 注解,默认主键策略也是NONE

  @TableId(type = IdType.NONE)
  private Long id;

插入时,若实体类的主键 ID 有值,则应用之;若主键 ID 为空,则应用主键全局策略,来生成一个 ID。

  • 其余的策略相似,不赘述

小结

AUTO依赖于数据库的自增主键,插入时,实体对象无需设置主键,插入胜利后,主键会被写回实体对象。

INPUT` 齐全依赖于用户输出。实体对象中主键 ID 是什么,插入到数据库时就设置什么。若有值便设置值,若为 `null` 则设置 `null

其余的几个策略,都是在实体对象中主键 ID 为空时,才会主动生成。

NONE会追随全局策略,ASSIGN_ID采纳雪花算法,ASSIGN_UUID采纳 UUID

全局配置,在 application.yml 中进行即可;针对单个实体类的部分配置,应用 @TableId 即可。对于某个实体类,若它有部分主键策略,则采纳之,否则,追随全局策略。

配置

mybatis plus 有许多可配置项,可在 application.yml 中进行配置,如下面的全局主键策略。上面列举局部配置项

根本配置

  • configLocation:若有独自的 mybatis 配置,用这个注解指定 mybatis 的配置文件(mybatis 的全局配置文件)
  • mapperLocations:mybatis mapper 所对应的 xml 文件的地位
  • typeAliasesPackage:mybatis 的别名包扫描门路
  • …..

进阶配置

  • mapUnderscoreToCamelCase:是否开启主动驼峰命名规定映射。(默认开启)
  • dbTpe:数据库类型。个别不必配,会依据数据库连贯 url 自动识别
  • fieldStrategy:(已过期)字段验证策略。该配置项在最新版的 mp 文档中曾经找不到了,被细分成了insertStrategyupdateStrategyselectStrategy。默认值是NOT_NULL,即对于实体对象中非空的字段,才会组装到最终的 SQL 语句中。

    有如下几种可选配置

    • IGNORED:疏忽校验。即,不做校验。实体对象中的全副字段,无论值是什么,都如实地被组装到 SQL 语句中(为 NULL 的字段在 SQL 语句中就组装为NULL)。
    • NOT_NULL:非 NULL 校验。只会将非 NULL 的字段组装到 SQL 语句中
    • NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其余类型的字段,等同于NOT_NULL
    • NEVER:不退出 SQL。所有字段不退出到 SQL 语句

    这个配置项,可在 application.yml 中进行 全局配置 ,也能够在某一实体类中,对某一字段用@TableField 注解进行 部分配置

    这个字段验证策略有什么用呢?在 UPDATE 操作中可能体现进去,若用一个 User 对象执行 UPDATE 操作,咱们心愿只对 User 对象中非空的属性,更新到数据库中,其余属性不做更新,则 NOT_NULL 能够满足需要。而若 updateStrategy 配置为IGNORED,则不会进行非空判断,会将实体对象中的全副属性如实组装到 SQL 中,这样,执行 UPDATE 时,可能就将一些不想更新的字段,设置为了NULL

  • tablePrefix:增加表名前缀

    比方

  mybatis-plus:
    global-config:
      db-config:
        table-prefix: xx_

而后将 MYSQL 中的表做一下批改。但 Java 实体类放弃不变(依然为User)。

测试

      @Test
      public void test3() {QueryWrapper<User> wrapper = new QueryWrapper<>();
          wrapper.like("name", "黄");
          Integer count = userMapper.selectCount(wrapper);
          System.out.println(count);
      }

能够看到拼接进去的 SQL,在表名后面增加了前缀

代码生成器

mp 提供一个生成器,可疾速生成 Entity 实体类,Mapper 接口,Service,Controller 等全套代码。

示例如下

public class GeneratorTest {
    @Test
    public void generate() {AutoGenerator generator = new AutoGenerator();

        // 全局配置
        GlobalConfig config = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        // 设置输入到的目录
        config.setOutputDir(projectPath + "/src/main/java");
        config.setAuthor("yogurt");
        // 生成完结后是否关上文件夹
        config.setOpen(false);

        // 全局配置增加到 generator 上
        generator.setGlobalConfig(config);

        // 数据源配置
        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai");
        dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
        dataSourceConfig.setUsername("root");
        dataSourceConfig.setPassword("root");

        // 数据源配置增加到 generator
        generator.setDataSource(dataSourceConfig);

        // 包配置, 生成的代码放在哪个包下
        PackageConfig packageConfig = new PackageConfig();
        packageConfig.setParent("com.example.mp.generator");

        // 包配置增加到 generator
        generator.setPackageInfo(packageConfig);

        // 策略配置
        StrategyConfig strategyConfig = new StrategyConfig();
        // 下划线驼峰命名转换
        strategyConfig.setNaming(NamingStrategy.underline_to_camel);
        strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);
        // 开启 lombok
        strategyConfig.setEntityLombokModel(true);
        // 开启 RestController
        strategyConfig.setRestControllerStyle(true);
        generator.setStrategy(strategyConfig);
        generator.setTemplateEngine(new FreemarkerTemplateEngine());

        // 开始生成
        generator.execute();}
}

运行后,能够看到生成了如下图所示的全套代码

高级性能

高级性能的演示须要用到一张新的表user2

DROP TABLE IF EXISTS user2;
CREATE TABLE user2 (id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键 id',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直属下级 id',
create_time DATETIME DEFAULT NULL COMMENT '创立工夫',
update_time DATETIME DEFAULT NULL COMMENT '批改工夫',
version INT(11) DEFAULT '1' COMMENT '版本',
deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识,0- 未删除,1- 已删除',
CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user2(id)
) ENGINE = INNODB CHARSET=UTF8;

INSERT INTO user2(id, name, age, email, manager_id, create_time)
VALUES
(1, '老板', 40 ,'boss@baomidou.com' ,NULL, '2021-03-28 13:12:40'),
(2, '王狗蛋', 40 ,'gd@baomidou.com' ,1, '2021-03-28 13:12:40'),
(3, '王鸡蛋', 40 ,'jd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(4, '王鸭蛋', 40 ,'yd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(5, '王猪蛋', 40 ,'zd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(6, '王软蛋', 40 ,'rd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(7, '王铁蛋', 40 ,'td@baomidou.com' ,2, '2021-03-28 13:12:40')
复制代码

并创立对应的实体类User2

package com.example.mp.po;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User2 {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Integer version;
    private Integer deleted;
}

以及 Mapper 接口

package com.example.mp.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mp.po.User2;
public interface User2Mapper extends BaseMapper<User2> {}

逻辑删除

首先,为什么要有逻辑删除呢?间接删掉不行吗?当然能够,但日后若想要复原,或者须要查看这些数据,就做不到了。逻辑删除是为了不便数据恢复,和爱护数据自身价值的一种计划

日常中,咱们在电脑中删除一个文件后,也仅仅是把该文件放入了回收站,日后若有须要还能进行查看或复原。当咱们确定不再须要某个文件,能够将其从回收站中彻底删除。这也是相似的情理。

mp 提供的逻辑删除实现起来非常简单

只须要在 application.yml 中进行逻辑删除的相干配置即可

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted  # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
      # 若逻辑已删除和未删除的值和默认值一样,则能够不配置这 2 项

测试代码

package com.example.mp;
import com.example.mp.mappers.User2Mapper;
import com.example.mp.po.User2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class LogicDeleteTest {
    @Autowired
    private User2Mapper mapper;
    @Test
    public void testLogicDel() {int i = mapper.deleteById(6);
        System.out.println("rowAffected =" + i);
    }
}

后果

能够看到,收回的 SQL 不再是DELETE,而是UPDATE

此时咱们再执行一次SELECT

    @Test
    public void testSelect() {List<User2> users = mapper.selectList(null);
    }

能够看到,收回的 SQL 语句,会主动在 WHERE 前面拼接逻辑未删除的条件。查问进去的后果中,没有了 id 为 6 的王软蛋。

若想要 SELECT 的列,不包含逻辑删除的那一列,则能够在实体类中通过 @TableField 进行配置

@TableField(select = false)
private Integer deleted;
复制代码

能够看到下图的执行后果中,SELECT 中曾经不蕴含 deleted 这一列了

后面在 application.yml 中做的配置,是全局的。通常来说,对于多个表,咱们也会对立逻辑删除字段的名称,对立逻辑已删除和未删除的值,所以全局配置即可。当然,若要对某些表进行独自配置,在实体类的对应字段上应用 @TableLogic 即可

@TableLogic(value = "0", delval = "1")
private Integer deleted;

小结

开启 mp 的逻辑删除后,会对 SQL 产生如下的影响

  • INSERT 语句:没有影响
  • SELECT 语句:追加 WHERE 条件,过滤掉已删除的数据
  • UPDATE 语句:追加 WHERE 条件,避免更新到已删除的数据
  • DELETE 语句:转变为 UPDATE 语句

留神,上述的影响,只针对 mp 主动注入的 SQL 失效。如果是本人手动增加的自定义 SQL,则不会失效。比方

public interface User2Mapper extends BaseMapper<User2> {@Select("select * from user2")
    List<User2> selectRaw();}

调用这个selectRaw,则 mp 的逻辑删除不会失效。

另,逻辑删除可在 application.yml 中进行全局配置,也可在实体类中用 @TableLogic 进行部分配置。

主动填充

表中经常会有“新增工夫”,“批改工夫”,“操作人”等字段。比拟原始的形式,是每次插入或更新时,手动进行设置。mp 能够通过配置,对某些字段进行主动填充,食用示例如下

  1. 在实体类中的某些字段上,通过 @TableField 设置主动填充
   public class User2 {
       private Long id;
       private String name;
       private Integer age;
       private String email;
       private Long managerId;
       @TableField(fill = FieldFill.INSERT) // 插入时主动填充
       private LocalDateTime createTime;
       @TableField(fill = FieldFill.UPDATE) // 更新时主动填充
       private LocalDateTime updateTime;
       private Integer version;
       private Integer deleted;
   }
  1. 实现主动填充处理器
   package com.example.mp.component;
   import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
   import org.apache.ibatis.reflection.MetaObject;
   import org.springframework.stereotype.Component;
   import java.time.LocalDateTime;
   
   @Component // 须要注册到 Spring 容器中
   public class MyMetaObjectHandler implements MetaObjectHandler {
   
       @Override
       public void insertFill(MetaObject metaObject) {
           // 插入时主动填充
           // 留神第二个参数要填写实体类中的字段名称,而不是表的列名称
           strictFillStrategy(metaObject, "createTime", LocalDateTime::now);
       }
   
       @Override
       public void updateFill(MetaObject metaObject) {
           // 更新时主动填充
           strictFillStrategy(metaObject, "updateTime", LocalDateTime::now);
       }
   }

测试

    @Test
    public void test() {User2 user = new User2();
        user.setId(8L);
        user.setName("王一蛋");
        user.setAge(29);
        user.setEmail("yd@baomidou.com");
        user.setManagerId(2L);
        mapper.insert(user);
    }

依据下图后果,能够看到对 createTime 进行了主动填充

留神,主动填充仅在该字段为空时会失效,若该字段不为空,则间接应用已有的值。如下

    @Test
    public void test() {User2 user = new User2();
        user.setId(8L);
        user.setName("王一蛋");
        user.setAge(29);
        user.setEmail("yd@baomidou.com");
        user.setManagerId(2L);
        user.setCreateTime(LocalDateTime.of(2000,1,1,8,0,0));
        mapper.insert(user);
    }

更新时的主动填充,测试如下

    @Test
    public void test() {User2 user = new User2();
        user.setId(8L);
        user.setName("王一蛋");
        user.setAge(99);
        mapper.updateById(user);
    }

乐观锁插件

当呈现并发操作时,须要确保各个用户对数据的操作不产生抵触,此时须要一种并发管制伎俩。乐观锁的办法是,在对数据库的一条记录进行批改时,先间接加锁(数据库的锁机制),锁定这条数据,而后再进行操作;而乐观锁,正如其名,它先假如不存在抵触状况,而在理论进行数据操作时,再查看是否抵触。乐观锁的一种通常实现是 版本号,在 MySQL 中也有名为 MVCC 的基于版本号的并发事务管制。

在读多写少的场景下,乐观锁比拟实用,可能缩小加锁操作导致的性能开销,进步零碎吞吐量。

在写多读少的场景下,乐观锁比拟应用,否则会因为乐观锁一直失败重试,反而导致性能降落。

乐观锁的实现如下:

  1. 取出记录时,获取以后 version
  2. 更新时,带上这个 version
  3. 执行更新时,set version = newVersion where version = oldVersion
  4. 如果 oldVersion 与数据库中的 version 不统一,就更新失败

这种思维和 CAS(Compare And Swap)十分类似。

乐观锁的实现步骤如下

  1. 配置乐观锁插件
   package com.example.mp.config;
   
   import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
   import org.springframework.context.annotation.Bean;
   import org.springframework.context.annotation.Configuration;
   
   @Configuration
   public class MybatisPlusConfig {
       /** 3.4.0 当前的 mp 版本,举荐用如下的配置形式 **/
       @Bean
       public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
           interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
           return interceptor;
       }
       /** 旧版 mp 能够采纳如下形式。留神新旧版本中,新版的类,名称带有 Inner, 旧版的不带, 不要配错了 **/
       /*
       @Bean
       public OptimisticLockerInterceptor opLocker() {return new OptimisticLockerInterceptor();
       }
       */
   }
  1. 在实体类中示意版本的字段上增加注解@Version
   @Data
   public class User2 {
       private Long id;
       private String name;
       private Integer age;
       private String email;
       private Long managerId;
       private LocalDateTime createTime;
       private LocalDateTime updateTime;
       @Version
       private Integer version;
       private Integer deleted;
   }

测试代码

    @Test
    public void testOpLocker() {
        int version = 1; // 假如这个 version 是先前查问时取得的
        User2 user = new User2();
        user.setId(8L);
        user.setEmail("version@baomidou.com");
        user.setVersion(version);
        int i = mapper.updateById(user);
    }

执行之前先看一下数据库的状况

依据下图执行后果,能够看到 SQL 语句中增加了 version 相干的操作

当 UPDATE 返回了 1,示意影响行数为 1,则更新胜利。反之,因为 WHERE 前面的 version 与数据库中的不统一,匹配不到任何记录,则影响行数为 0,示意更新失败。更新胜利后,新的 version 会被封装回实体对象中。

实体类中 version 字段,类型只反对 int,long,Date,Timestamp,LocalDateTime

留神,乐观锁插件仅反对 updateById(id)update(entity, wrapper)办法

留神:如果应用 wrapper,则wrapper 不能复用!示例如下

    @Test
    public void testOpLocker() {User2 user = new User2();
        user.setId(8L);
        user.setVersion(1);
        user.setAge(2);

        // 第一次应用
        LambdaQueryWrapper<User2> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User2::getName, "王一蛋");
        mapper.update(user, wrapper);

        // 第二次复用
        user.setAge(3);
        mapper.update(user, wrapper);
    }

能够看到在第二次复用 wrapper 时,拼接出的 SQL 中,前面 WHERE 语句中呈现了 2 次 version,是有问题的。

性能剖析插件

该插件会输入 SQL 语句的执行工夫,以便做 SQL 语句的性能剖析和调优。

注:3.2.0 版本之后,mp 自带的性能剖析插件被官网移除了,而举荐食用第三方性能剖析插件

食用步骤

  1. 引入 maven 依赖
   <dependency>
       <groupId>p6spy</groupId>
       <artifactId>p6spy</artifactId>
       <version>3.9.1</version>
   </dependency>
  1. 批改application.yml
   spring:
     datasource:
       driver-class-name: com.p6spy.engine.spy.P6SpyDriver #换成 p6spy 的驱动
       url: jdbc:p6spy:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai #url 批改
       username: root
       password: root
  1. src/main/resources 资源目录下增加spy.properties
   #spy.properties
   #3.2.1 以上应用
   modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
   # 实在 JDBC driver , 多个以逗号宰割, 默认为空。因为下面设置了 modulelist, 这里能够不必设置 driverlist
   #driverlist=com.mysql.cj.jdbc.Driver
   # 自定义日志打印
   logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
   #日志输入到控制台
   appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
   #若要日志输入到文件, 把下面的 appnder 正文掉, 或者采纳上面的 appender, 再增加 logfile 配置
   #不配置 appender 时, 默认是往文件进行输入的
   #appender=com.p6spy.engine.spy.appender.FileLogger
   #logfile=log.log
   # 设置 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
   # 是否开启慢 SQL 记录
   outagedetection=true
   # 慢 SQL 记录规范 2 秒
   outagedetectioninterval=2
   # 执行工夫设置, 只有超过这个执行工夫的才进行记录, 默认值 0, 单位毫秒
   executionThreshold=10

轻易运行一个测试用例,能够看到该 SQL 的执行时长被记录了下来

多租户 SQL 解析器

多租户的概念:多个用户共用一套零碎,但他们的数据有须要绝对的独立,放弃肯定的隔离性。

多租户的数据隔离个别有如下的形式:

  • 不同租户应用不同的数据库服务器

    长处是:不同租户有不同的独立数据库,有助于扩大,以及对不同租户提供更好的个性化,呈现故障时复原数据较为简单。

    毛病是:减少了数据库数量,购买老本,保护老本更高

  • 不同租户应用雷同的数据库服务器,但应用不同的数据库(不同的 schema)

    长处是购买和保护成本低了一些,毛病是数据恢复较为艰难,因为不同租户的数据都放在了一起

  • 不同租户应用雷同的数据库服务器,应用雷同的数据库,共享数据表,在表中减少租户 id 来做辨别

    长处是,购买和保护老本最低,反对用户最多,毛病是隔离性最低,安全性最低

食用实例如下

增加多租户拦截器配置。增加配置后,在执行 CRUD 的时候,会主动在 SQL 语句最初拼接租户 id 的条件

package com.example.mp.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                // 返回租户 id 的值, 这里固定写死为 1
                // 个别是从以后上下文中取出一个 租户 id
                return new LongValue(1);
            }

            /**
            ** 通常会将示意租户 id 的列名,须要排除租户 id 的表等信息,封装到一个配置类中(如 TenantConfig)**/
            @Override
            public String getTenantIdColumn() {
                // 返回表中的示意租户 id 的列名
                return "manager_id";
            }

            @Override
            public boolean ignoreTable(String tableName) {
                // 表名不为 user2 的表, 不拼接多租户条件
                return !"user2".equals(tableName);
            }
        }));
        
        // 如果用了分页插件留神先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
        return interceptor;
    }

}

测试代码

    @Test
    public void testTenant() {LambdaQueryWrapper<User2> wrapper = new LambdaQueryWrapper<>();
        wrapper.likeRight(User2::getName, "王")
                .select(User2::getName, User2::getAge, User2::getEmail, User2::getManagerId);
        user2Mapper.selectList(wrapper);
    }

动静表名 SQL 解析器

当数据量特地大的时候,咱们通常会采纳分库分表。这时,可能就会有多张表,其表构造雷同,但表名不同。例如order_1order_2order_3,查问时,咱们可能须要动静设置要查的表名。mp 提供了动静表名 SQL 解析器,食用示例如下

先在 mysql 中拷贝一下 user2

配置动静表名拦截器

package com.example.mp.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Random;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        HashMap<String, TableNameHandler> map = new HashMap<>();
        // 对于 user2 表,进行动静表名设置
        map.put("user2", (sql, tableName) -> {
            String _ = "_";
            int random = new Random().nextInt(2) + 1;
            return tableName + _ + random; // 若返回 null, 则不会进行动静表名替换, 还是会应用 user2
        });
        dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        return interceptor;
    }

}

测试

    @Test
    public void testDynamicTable() {user2Mapper.selectList(null);
    }

总结

  • 条件结构器 AbstractWrapper 中提供了多个办法用于结构 SQL 语句中的 WHERE 条件,而其子类 QueryWrapper 额定提供了 select 办法,能够只选取特定的列,子类 UpdateWrapper 额定提供了 set 办法,用于设置 SQL 中的 SET 语句。除了一般的 Wrapper,还有基于 lambda 表达式的Wrapper,如LambdaQueryWrapperLambdaUpdateWrapper,它们在结构 WHERE 条件时,间接以 办法援用 来指定 WHERE 条件中的列,比一般 Wrapper 通过字符串来指定要更加优雅。另,还有 链式 Wrapper,如LambdaQueryChainWrapper,它封装了BaseMapper,能够更不便地获取后果。
  • 条件结构器采纳 链式调用 来拼接多个条件,条件之间默认以 AND 连贯
  • ANDOR前面的条件须要被括号包裹时,将括号中的条件以 lambda 表达式模式,作为参数传入 and()or()

    特地的,当 () 须要放在 WHERE 语句的最结尾时,能够应用 nested() 办法

  • 条件表达式时当须要传入自定义的 SQL 语句,或者须要调用数据库函数时,可用 apply() 办法进行 SQL 拼接
  • 条件结构器中的各个办法能够通过一个 boolean 类型的变量 condition,来依据须要灵便拼接 WHERE 条件(仅当conditiontrue时会拼接 SQL 语句)
  • 应用 lambda 条件结构器,能够通过 lambda 表达式,间接应用实体类中的属性进行条件结构,比一般的条件结构器更加优雅
  • 若 mp 提供的办法不够用,能够通过 自定义 SQL(原生 mybatis)的模式进行扩大开发
  • 应用 mp 进行分页查问时,须要创立一个分页拦截器(Interceptor),注册到 Spring 容器中,随后查问时,通过传入一个分页对象(Page 对象)进行查问即可。单表查问时,能够应用 BaseMapper 提供的 selectPageselectMapsPage办法。简单场景下(如多表联查),应用自定义 SQL。
  • AR 模式能够间接通过操作实体类来操作数据库。让实体类继承自 Model 即可

作者:yogurtzzz
链接:https://juejin.cn/post/696172…

正文完
 0