Mybatis-plus-note

简介: Mybatis-plus-note


目录

目录

1. 人生苦短

2. Mybatis-Plus简介

3. 适合人群

5. 核心功能

6. CRUD接口

6.1 Mapper CRUD接口

6.1.1 Insert方法

6.1.2 Delete方法

6.1.3 Update方法

6.1.4 Select方法

6.2 Service接口

6.2.1 使用方略示例

6.2.2 Save方法

6.2.3 SaveOrUpdate

6.2.4 Remove方法

6.2.5 Update

6.2.6 Get

6.2.7 List

6.2.8 Page

6.2.9 Count

6.3.0 Chain

7. 条件构造器

7.1 常用方法

7.2 常用方法示例

7.3 Condition

7.4 实体对象作为条件

8. lambda条件构造器

8.1 补充

8.2 反思

9. 自定义SQL

9.1 原生mybatis

9.2 mybatis-plus

9.2.1 注解配置方式

9.2.1 xml配置方式

10. 分页查询

10.1 selectPage方式

10.2 多表联查

11.1 局部策略和全局策略

12. 配置

12.1 基本配置

12.2 进阶配置

12.2.1 updateStrategy

13. 高级功能

13.1. 逻辑删除

13.2 自动填充

14. 乐观锁插件实现原理实现步骤注意点

15. 性能分析插件实现步骤

16. 多租户SQL解析器

17. 防止全表更新和删除实现步骤

18.Mybatis-plus逆向工程具体步骤

1. 人生苦短

2. Mybatis-Plus简介

  1. MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
  2. 官网指南 - https://baomidou.com/guide/

3. 适合人群

  1. 拥有 Java 开发环境以及相应 IDE
  2. 熟悉 Spring Boot
  3. 熟悉 Maven
  4. 熟悉mybatis框架

#4. 快速开始

  1. 创建SpringBoot工程
  2. 导入依赖
<?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.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>mybatis-plus-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybatis-plus-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');
  1. 实体类
package tech.aistar.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
    * 本类用来演示: 用户实体类
 *
    * @author: success
    * @date: 2021/5/31 9:51 上午
 */
@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    private Date createTime;
}
  1. application.yml文件配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  1. 主程序类
package tech.aistar;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("tech.aistar.mapper")
@SpringBootApplication
public class MybatisPlusDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusDemoApplication.class, args);
    }
}
  1. 单元测试
package tech.aistar.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
    * 本类用来演示:
 *
    * @author: success
    * @date: 2021/5/31 11:14 上午
 */
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;
    /**
        * 测试 - 查询所有
     */
    @Test
    public void testFindAll(){
        userMapper.selectList(null).forEach(System.out::println);
    }
}

总结:可以看到,针对单表的基本CRUD操作,只需要创建好实体类,并创建一个继承自BaseMapper的接口即可,可谓非常简洁。并且,我们注意到,User类中的managerId,createTime属性,自动和数据库表中的manager_id,create_time对应了起来,这是因为mp自动做了数据库下划线命名,到Java类的驼峰命名之间的转化。

5. 核心功能

注解

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

  1. @TableName注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。
  2. @TableId注解在实体类的某一字段上,表示这个字段对应数据库表的主键。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略。
  3. @TableField注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。
  • 排除非表字段
  1. 若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist属性为false,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用statictransient关键字,但个人觉得不是很合理,不做赘述
  • 字段验证策略
  1. 通过insertStrategyupdateStrategywhereStrategy属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。
  • 字段填充策略
  1. 通过fill属性指定,字段为空时会进行自动填充
  2. @Version乐观锁注解
  3. @EnumValue注解在枚举字段上
  4. @TableLogic逻辑删除
  5. @KeySequence序列主键策略(oracle
  6. @InterceptorIgnore插件过滤规则

6. CRUD接口

  1. mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。
  2. mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。
  3. 并且mp还提供了条件构造器Wrapper,可以方便地组装SQL语句中的WHERE条件

6.1 Mapper CRUD接口

Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器

  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器

学习目标 - BaseMapper里提供的方法

6.1.1 Insert方法

// 插入一条记录
int insert(T entity);

参数说明

类型

参数名

描述

T

entity

实体对象

测试代码

/**
  * 测试Insert方法
  */
@Test
public void testInsert(){
  User user = new User();
  user.setName("success");
  user.setAge(18);
  user.setEmail("849962874@qq.com");
  user.setCreateTime(new Date());
  user.setManagerId(2L);
  userMapper.insert(user);
}

6.1.2 Delete方法

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

参数说明

类型

参数名

描述

Wrapper<T>

wrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

测试代码

@Test
public void testDelete(){
  //根据 ID 删除
  //userMapper.deleteById(1399249825152729090L);
  //删除(根据ID 批量删除)
  //        List<Long> ids = new ArrayList<>();
  //        ids.add(5L);
  //        ids.add(3L);
  //        userMapper.deleteBatchIds(ids);
  // 根据 entity 条件,删除记录
  //DELETE FROM user WHERE (manager_id = ?)
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.eq("manager_id",2L);
  //        userMapper.delete(queryWrapper);
  //根据 columnMap 条件,删除记录
  //DELETE FROM user WHERE name = ? 
  Map<String,Object> maps = new HashMap<>();
  maps.put("name","吴组长");
  userMapper.deleteByMap(maps);
}

6.1.3 Update方法

// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

参数说明

类型

参数名

描述

T

entity

实体对象 (set 条件值,可为 null)

Wrapper<T>

updateWrapper

实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

测试代码

@Test
public void testUpdate(){
  //        User user = userMapper.selectById(1L);
  //        user.setName("success");
  //        userMapper.updateById(user);
  //第一种写法 - 使用set()方法单独更新某个列
  //        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  //        updateWrapper.ge("age",40).set("age",100);
  //
  //        userMapper.update(null,updateWrapper);
  //第二种写法 - 使用对象
  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",23).le("age",30);
  User user = new User();
  user.setAge(18);
  userMapper.update(user,updateWrapper);
}

6.1.4 Select方法

// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Map<String, Object>

columnMap

表字段 map 对象

IPage<T>

page

分页查询条件(可以为 RowBounds.DEFAULT)

部分测试代码

select Maps

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

该方法的使用场景如下:

  1. 只查部分列

当某个表的列特别多,而SELECT的时候只需要选取个别列,查询出的结果也没必要封装成Java实体类对象时(只查部分列时,封装成实体后,实体对象中的很多属性会是null),则可以用selectMaps,获取到指定的列后,再自行进行处理即可

测试代码

//SELECT id,name FROM user WHERE (name LIKE ?)
//==>  Preparing: SELECT id,name FROM user WHERE (name LIKE ?)
//==> Parameters: 黄%(String)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id","name");
queryWrapper.likeRight("name","黄");
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);

进行数据统计

测试代码

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("manager_id","avg(age)")
  .groupBy("manager_id")
  .having("avg(age)>={0}",59);
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
  1. selectObjs

只会返回第一个字段(第一列)的值,其他字段会被舍弃 - 得到的结果,只封装了第一列的id

QueryWrapper<User> wrapper = new QueryWrapper<>();  
wrapper.select("id", "name").like("name", "黄");  
List<Object> objects = userMapper.selectObjs(wrapper);  
objects.forEach(System.out::println);
  1. selectCount

查询满足条件的总数,注意,使用这个方法,不能调用QueryWrapperselect方法设置要查询的列了。这个方法会自动添加select count(1)

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("age",18);
Integer size = userMapper.selectCount(queryWrapper);
System.out.println(size);

6.2 Service接口

说明:

  • 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行remove 删除list 查询集合page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper 为 条件构造器

6.2.1 使用方略

另外一套CRUD是Service层的,只需要编写一个接口,继承IService,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,比较明显的区别在于IService支持了更多的批量化操作,如saveBatchsaveOrUpdateBatch等方法。

示例

  1. 首先,新建一个接口,继承IService
public interface UserService extends IService<User2> {
}
  1. 创建这个接口的实现类,并继承ServiceImpl,最后打上@Service注解,注册到Spring容器中,即可使用
@Service
public class UserServiceImpl extends ServiceImpl<User2Mapper, User2> implements UserService{
}

6.2.2 Save方法

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)- batchSize - 表示一次批量插入的数据量,默认为 1000
boolean saveBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

单元测试

@Test
public void testSave(){
  List<User2> list = new ArrayList<>();
  for (int i = 0; i < 5; i++) {
    User2 user2 = new User2();
    user2.setAge(22+i);
    user2.setEmail("99999"+i+"@qq.com");
    user2.setName("tom"+i);
    user2.setManagerId(2L);
    list.add(user2);
  }
  //批量插入
  userService.saveBatch(list);
}

6.2.3 SaveOrUpdate

// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

测试01-根据主键值id进行更新操作

/**
  * 测试根据主键值id进行更新
  * 如果实体类的id属性名和表的列主键值id列名保持一致,那么可以不使用@TableId
  * 否则必须使用
  */
@Test
public void testUpdateByPrimaryKey(){
  //        User2 user2 = userService.getById(1);
  //        user2.setAge(33);
  //        //测试对已经存在的记录进行更新
  //        userService.saveOrUpdate(user2);
  User2 user01 = new User2();
  user01.setName("亲爱的管");
  user01.setAge(100);
  //测试对不存在的记录进行插入
  userService.saveOrUpdate(user01);
}

测试02-根据Wrapper条件进行更新

/**
   * 测试根据条件进行更新
   * 如果存在,则更新,否则执行插入
   */
@Test
public void testUpdateByWrapper(){
  UpdateWrapper<User2> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",300);
  User2 u = new User2();
  u.setName("ping");
  userService.saveOrUpdate(u,updateWrapper);
}

6.2.4 Remove方法

// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体包装类 QueryWrapper

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

Collection<? extends Serializable>

idList

主键ID列表

测试代码

@Test
public void testRemove(){
  //本人更加推荐lambda写法
  userService.lambdaUpdate().eq(User2::getId,3).remove();
}

6.2.5 Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

更新批次数量

6.2.6 Get

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
//getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

boolean

throwEx

有多个 result 是否抛出异常

T

entity

实体对象

Function<? super Object, V>

mapper

转换函数

测试

@Test
public void testGet(){
  //        LambdaQueryWrapper<User2> queryWrapper = new LambdaQueryWrapper<>();
  //        queryWrapper.eq(User2::getAge,40);
  //本人更喜欢如下方式
  //这个是方法返回结果不止一条则会抛出异常,如果想默认取第一条结果,可以给这方法传第二个参数为false。
  //User2 user2 = userService.getOne(Wrappers.<User2>lambdaQuery().eq(User2::getAge,40),false);
  //System.out.println(user2);
  //getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
   Integer id = userService.getObj(Wrappers.<User2>lambdaQuery().eq(User2::getId,1),o->{return    Integer.parseInt(o.toString());});
        System.out.println(id);
}

6.2.7 List

// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

Collection<? extends Serializable>

idList

主键ID列表

Map<?String, Object>

columnMap

表字段 map 对象

Function<? super Object, V>

mapper

转换函数

测试代码

@Test
public void testList(){
  userService.listObjs(o->{
    return "user_"+o;
  }).forEach(e-> System.out.println(e));
}

6.2.8 Page

// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

IPage<T>

page

翻页对象

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testPage(){
  Page<User2> page = new Page<>(3,2);
  //        userService.page(page).getRecords().forEach(e-> System.out.println(e));
  userService.page(page,Wrappers.<User2>lambdaQuery().eq(User2::getAge,40))
    .getRecords().forEach(e-> System.out.println(e));
}

6.2.9 Count

// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testCount(){
  System.out.println(userService.count(Wrappers.<User2>lambdaQuery().eq(User2::getAge, 40)));
}

6.3.0 Chain

  1. query
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery(); 
// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();

测试

// Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (age = ? OR (id = ?))
userService.lambdaQuery()
  .eq(User2::getAge,40)
  .or(e1->e1.eq(User2::getId,30))
  .list().forEach(e-> System.out.println(e));
SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (id = ?)
System.out.println(userService.lambdaQuery().eq(User2::getId, 1).one());
  1. update
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin 
LambdaUpdateChainWrapper<T> lambdaUpdate();
// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

测试

@Test
public void testUpdate(){
  //直接更新具体的某列,不需要实体对象
  //userService.lambdaUpdate().eq(User2::getId,1).set(User2::getName,"ss").update();
  //更新一个对象
  User2 user2 = new User2();
  user2.setName("xx");
  user2.setAge(100);
  userService.lambdaUpdate().eq(User2::getId,1).update(user2);
}

7. 条件构造器

mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器主要涉及到3个类,AbstractWrapperQueryWrapperUpdateWrapper,它们的类关系如下:

AbstractWrapper中提供了非常多的方法用于构建WHERE条件,而QueryWrapper针对SELECT语句,提供了select()方法,可自定义需要查询的列,而UpdateWrapper针对UPDATE语句,提供了set()方法,用于构造set语句。条件构造器也支持lambda表达式,写起来非常舒爽。

7.1 常用方法

查询方式

说明

setSqlSelect

设置 SELECT 查询字段

where

WHERE 语句,拼接 +?WHERE 条件

and

AND 语句,拼接 +?AND 字段=值

andNew

AND 语句,拼接 +?AND (字段=值)

or

OR 语句,拼接 +?OR 字段=值

orNew

OR 语句,拼接 +?OR (字段=值)

eq

等于=

allEq

基于 map 内容等于=

ne

不等于<>

gt

大于>

ge

大于等于>=

lt

小于<

le

小于等于<=

like

模糊查询 LIKE

notLike

模糊查询 NOT LIKE

in

IN 查询

notIn

NOT IN 查询

isNull

NULL 值查询

isNotNull

IS NOT NULL

groupBy

分组 GROUP BY

having

HAVING 关键词

orderBy

排序 ORDER BY

orderAsc

ASC 排序 ORDER BY

orderDesc

DESC 排序 ORDER BY

exists

EXISTS 条件语句

notExists

NOT EXISTS 条件语句

between

BETWEEN 条件语句

notBetween

NOT BETWEEN 条件语句

addFilter

自由拼接 SQL

last

拼接在最后,例如:last(“LIMIT 1”)

下面对AbstractWrapper中用于构建SQL语句中的WHERE条件的方法进行部分列举:

  1. allEq - 全部eq
@Test
public void test条件构造器(){
  //SELECT id,name,age,email,manager_id,create_time FROM user WHERE (name = ? AND age = ?)
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> conditional = new HashMap<>();
  conditional.put("age",18);
  conditional.put("name","小菜");
  queryWrapper.allEq(conditional);
  List<User> users = userMapper.selectList(queryWrapper);
  users.forEach(e-> System.out.println(e));
}

细节01-当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);  
}

细节02 - 若想忽略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);  
}

细节03 - 忽略某个key的查询 - SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age = ?)

查询语句中只会出现age列,不会出现name列

QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 18);
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);

7.2 常用方法示例

@Test
public void test条件构造器02(){
  //1. 名字中包含三,且年龄小于25
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.like("name","三")
  //                    .lt("age",25);
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //2.姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.likeRight("name","黄").and(
  //                (w->w.gt("age",20)
  //                        .lt("age",40).isNotNull("email"))
  //        );
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //3.或者年龄大于等于23,按照年龄降序排列,年龄相同则按照id升序排列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.ge("age",23)
  //                    .orderByDesc("age")
  //                    .orderByAsc("id");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //4.创建日期为2021年2月22日,并且直属上级的名字为小姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.apply("date_format(create_time,'%Y-%m-%d')={0}","2021-02-22")
  //                     .inSql("manager_id","select id from user where name like '小%'");
  //
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //5. 正常嵌套
  //(年龄小于40或者邮箱不为空) 并且名字为黄姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.nested(c->c.lt("age",40).or().isNotNull("email"))
  //                .likeRight("name","黄")
  //
  //        //6.last
  //        //.只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
  //                .last("limit 1");
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //7. 筛选部分列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.select("id","name");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //8. 选出id, name, age, email, 等同于排除 manager_id 和 create_time
  // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列,
  // 可以采用重载的select方法,指定需要排除的列
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.select(User.class,info->{
    String colName = info.getColumn();
    return !colName.equals("manager_id") && !colName.equals("create_time");
  });
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}

7.3 Condition

条件构造器的诸多方法中,均可以指定一个boolean类型的参数condition,用来决定该条件是否加入最后生成的WHERE语句中,比如

/**
  * 测试boolean参数
*/
@Test
public void testBooleanParam(){
String name = "";
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中
// 其实就是对下面代码的简化
//如果字符串里面的值为null, "", "   ",那么返回值为false;否则为true
//        if (StringUtils.hasText(name)) {
//            wrapper.like("name", name);
//        }
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(e-> System.out.println(e));
}

7.4 实体对象作为条件

调用构造函数创建一个Wrapper对象时,可以传入一个实体对象。

后续使用这个Wrapper时,会以实体对象中的非空属性,构建WHERE条件(默认构建等值匹配的WHERE条件,

这个行为可以通过实体类里各个字段上的@TableField注解中的condition属性进行改变)

  1. 示例01 - 会以实体对象中的非空属性,构建WHERE条件

执行结果可以看到,是根据实体对象中的非空属性,进行了等值匹配查询

@Test
public void testObjParams(){
  User user = new User();
  user.setName("小菜");
  QueryWrapper<User> queryWrapper = new QueryWrapper<>(user);
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}
  1. 示例02 - 若希望针对某些属性,改变等值匹配的行为,则可以在实体类中用@TableField注解进行配置,示例如下
@TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择
SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如
@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;  
}

8. lambda条件构造器

lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的方法引用来指定列。示例如下

@Test
public void testLambda(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(User::getName,"三");
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(System.out::println);
}

8.1 补充

  1. 还有个链式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);  
}
  1. 更新操作
@Test
public void testLambdaUpdate(){
  User user = new User();
  user.setName("james");
  LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
  lambdaUpdateWrapper.eq(User::getId,1);
  userMapper.update(user,lambdaUpdateWrapper);
}
  1. 再额外演示一下,链式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();  
}

8.2 反思

由于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);  
 }

9. 自定义SQL

当mp提供的方法还不能满足需求时,则可以自定义SQL。

9.1 原生mybatis

public interface UserMapper extends BaseMapper<User> {
    @Select("select * from user")
    List<User> findAll();
}

9.2 mybatis-plus

也可以使用mp提供的Wrapper条件构造器,来自定义SQL

在UserMapper.java接口添加

9.2.1 注解配置方式

//自定义sql - mybatis-plus方式
@Select("select * from user ${ew.customSqlSegment}")
List<User> findAllWhere(@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

/**
  * 自定义SQL
  * 2. 采用原生的mybatis
  * 也可以使用mp提供的Wrapper条件构造器,来自定义SQL
  */
@Test
public void testFindAllWhere(){
  LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  wrapper.like(User::getName,"三");
  List<User> userList = userMapper.findAllWhere(wrapper);
  userList.forEach(user -> System.out.println(user));
}

9.2.1 xml配置方式

List<User> findAll(Wrapper<User> wrapper);
<mapper namespace="tech.aistar.mapper.UserMapper">  
  <select id="findAll" resultType="tech.aistar.pojo.User">  
    SELECT * FROM user ${ew.customSqlSegment}  
  </select>  
</mapper>

10. 分页查询

BaseMapper中提供了2个方法进行分页查询,分别是

  1. selectPage:将查询的结果封装成Java实体对象
  2. selectMapsPage:将查询的结果封装成封装成Map<String,Object>

10.1 selectPage方式

  1. 创建mp的分页拦截器,注册到Spring容器中

config/

@Configuration
public class MybatisplusConfig {
 @Bean
 public MybatisPlusInterceptor mybatisPlusInterceptor(){
     MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
     PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
     paginationInnerInterceptor.setDbType(DbType.MYSQL);
     paginationInnerInterceptor.setOverflow(true);
     interceptor.addInnerInterceptor(paginationInnerInterceptor);
     return interceptor;
 }
}
@Test
public void testSelectPage(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.ge(User::getAge,20);
//设置分页信息,查询第3页,每页显示2条
Page<User> page = new Page<>(3,2);
//执行分页查询
Page<User> pageInfo = userMapper.selectPage(page,lambdaQueryWrapper);
//展示分页的信息
System.out.println("总的记录数[条数rows]:"+page.getTotal());
System.out.println("总的页数:"+page.getPages());
System.out.println("每页显示条数:"+page.getSize());
//展示分页具体返回的内容
page.getRecords().forEach(System.out::println);
}

控制台sql分析

==>  Preparing: SELECT COUNT(*) FROM user WHERE (age >= ?)
==> Parameters: 20(Integer)
<==    Columns: COUNT(*)
<==        Row: 6
<==      Total: 1
==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2

注意到,分页查询总共发出了2次SQL,一次查总记录数,一次查具体数据。若希望不查总记录数,仅查分页结果。可以通过Page的重载构造函数,指定isSearchCountfalse即可

public Page(long current, long size, boolean isSearchCount);
Page<User> page = new Page<>(3,2,false);

设置成第三个参数为false,之后那么只会出现一条sql,与此同时总的记录数和总的页数都将返回默认值0.

==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@57545c3f]
总的记录数[条数rows]:0
总的页数:0
每页显示条数:2

10.2 多表联查

在实际开发中,可能遇到多表联查的场景,此时BaseMapper中提供的单表分页查询的方法无法满足需求,需要自定义SQL,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,修改SQL语句即可)

/**
  *在实际开发中,可能遇到**多表联查**的场景,此时`BaseMapper`中提供的单表分页查询的方法无法满足需求,
  * 需要**自定义SQL**,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,
  * 修改SQL语句即可)
  *
  * 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式
  * @param page
  * @param wrapper
  * @return
  */
@Select("select * from user ${ew.customSqlSegment}")
Page<User> selectUserPage(Page<User> page,@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

@Test
public void testSelectUserPage(){
  LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
  lambdaQueryWrapper.ge(User::getAge,20);
  //设置分页信息,查询第3页,每页显示2条
  //第三个参数设置成false,不会查询总的记录数
  //Page<User> page = new Page<>(3,2,false);
  Page<User> page = new Page<>(3,2);
  //执行分页查询
  Page<User> pageInfo = userMapper.selectUserPage(page,lambdaQueryWrapper);
  //展示分页的信息
  System.out.println("总的记录数[条数rows]:"+page.getTotal());
  System.out.println("总的页数:"+page.getPages());
  System.out.println("每页显示条数:"+page.getSize());
  //展示分页具体返回的内容
  page.getRecords().forEach(System.out::println);
}

#11. Sequence主键生成策略

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

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

  • AUTO

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

并且插入成功之后,主键会被写回实体对象。

  • NONE

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

  • INPUT

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

  • ASSIGN_ID

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

  • ASSIGN_UUID

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

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

11.1 局部策略和全局策略

可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略

mybatis-plus:
  global-config:
    db-config:
      id-type: auto

注意一旦设置成了auto,需要修改主键为自增长,修改脚本文件-添加auto_increment否则会抛出id does not default value的异常

DROP TABLE IF EXISTS user;  
   CREATE TABLE user (  
     id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');

12. 配置

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

12.1 基本配置

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

12.2 进阶配置

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

12.2.1 updateStrategy

有如下几种可选配置这个配置项,可在application.yml中进行全局配置,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置

这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。

而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL

  • IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。
  • NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中
  • NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL
  • NEVER:不加入SQL。所有字段不加入到SQL语句
  • tablePrefix:添加表名前缀
mybatis-plus:  
   global-config:  
     db-config:  
       table-prefix: xx_
@Test  
public void test3() {  
 QueryWrapper<User> wrapper = new QueryWrapper<>();  
 wrapper.like("name", "黄");  
 Integer count = userMapper.selectCount(wrapper);  
 System.out.println(count);  
}

可以看到拼接出来的SQL,在表名前面添加了前缀

13. 高级功能

高级功能的演示需要用到一张新的表user2

  • sql脚本
DROP TABLE IF EXISTS user2;  
CREATE TABLE user2 (  
id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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_user2 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实体类
@Data  
public class User2 {  
private Long id;  
private String name;  
private Integer age;  
private String email;  
private Long managerId;  
private Date createTime;  
private Date updateTime;  
private Integer version;  
private Integer deleted;  
}
  • User2Mapper接口
public interface User2Mapper extends BaseMapper<User2> {
}

13.1. 逻辑删除

首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案

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

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项

测试代码

user2Mapper.deleteById(1);

分析控制台SQL

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

可以看到,发出的SQL不再是DELETE,而是UPDATE

此时我们再执行一次SELECT

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

分析控制台SQL

==>  Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user2 WHERE deleted=0
==> Parameters: 
<==    Columns: id, name, age, email, manager_id, create_time, update_time, version, deleted
<==        Row: 2, 王狗蛋, 40, gd@baomidou.com, 1, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 3, 王鸡蛋, 40, jd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 4, 王鸭蛋, 40, yd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 5, 王猪蛋, 40, zd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 6, 王软蛋, 40, rd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 7, 王铁蛋, 40, td@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==      Total: 6

可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为1的老板

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

@TableField(select = false)
private Integer deleted;

分析控制台SQL

SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE deleted=0

查询的列中已经不包括逻辑删除的那一列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进行局部配置。

13.2 自动填充

表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。

比较原始的方式,是每次插入或更新时,手动进行设置。

mp可以通过配置,对某些字段进行自动填充,实用实例如下

原理

  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
  • 注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置

在实体类中的某些字段上,通过@TableField设置自动填充

@Data
public class User2 implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    //自动填充
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.UPDATE)
    private Date updateTime;
    private Integer version;
    //逻辑删除 - '逻辑删除标识,0-未删除,1-已删除'
    @TableField(select = false)
//    @TableLogic(delval = "1",value = "0")
    private Integer deleted;
}

实现自动填充处理器

package tech.aistar.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Date;
import java.util.function.Supplier;
/**
 * 本类用来演示: 实现自动填充处理器
 *
 * @author: success
 * @date: 2021/6/3 9:59 上午
 */
@Component //需要注册到Spring容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
//        this.strictFillStrategy(metaObject, "createTime", new Supplier<Object>() {
//            @Override
//            public Object get() {
//                return new Date();
//            }
//        });
        this.strictFillStrategy(metaObject, "createTime",Date::new);
    }
    @Override
    public void updateFill(MetaObject metaObject) {
         this.strictFillStrategy(metaObject,"updateTime",Date::new);
    }
}

单元测试-insert

@SpringBootTest
public class TestMyMetaObjectHandler {
    @Autowired
    private User2Mapper user2Mapper;
    /**
     * 测试自动填充 - createTime字段
     */
    @Test
    public void testInsert(){
        User2 user = new User2();
        user.setName("强老师");
        user.setAge(29);
        user.setEmail("yt@baomidou.com");
        user.setManagerId(2L);
        user2Mapper.insert(user);
    }
}

可以看到对createTime进行了自动填充,注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值.

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

@Test
public void testUpdate(){
  User2 user2 = new User2();
  user2.setId(8L);
  user2.setName("平老师");
  user2Mapper.updateById(user2);
}

14. 乐观锁插件

当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。

在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。

在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。

实现原理

乐观锁的实现如下:

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

这种思想和CAS(Compare And Swap)非常相似。

实现步骤

  1. 配置乐观锁插件
/**
     * 乐观锁插件 - 新版
  */
@Bean
public MybatisPlusInterceptor oPtimisticLockerInterceptor() {
  MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
  mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
  return mybatisPlusInterceptor;
}
  1. 在实体类中表示版本的字段上添加注解@Version
//乐观锁- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
@Version
private Integer version;
  1. 测试代码
@Test
public void testLock(){
  // 假设这个version是先前查询时获得的
  int version=1;
  User2 user2 = new User2();
  user2.setId(1L);
  user2.setName("管管");
  user2.setVersion(version);
  user2Mapper.updateById(user2);
}

SQL打印如下,可以发现version从一开始的值1变成2

==>  Preparing: UPDATE user2 SET name=?, update_time=?, version=? WHERE id=? AND version=?
==> Parameters: 管管(String), 2021-06-08 09:10:44.678(Timestamp), 2(Integer), 1(Long), 1(Integer)
<==    Updates: 1

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

注意点

注意,乐观锁插件仅支持updateById(id)update(entity, 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,是有问题的。

15. 性能分析插件

该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。

注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐使第三方性能分析插件

该插件有性能损耗,不建议生产环境使用

实现步骤

  1. 引入第三方依赖
<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
    url: jdbc:p6spy:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    # url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
#    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  1. src/main/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
  1. 随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来
Consume Time:8 ms 2021-06-08 09:30:35
 Execute SQL:select * from user

16. 多租户SQL解析器

多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。

此处内容作为课外了解 - 想要进一步学习,可参考 - https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/105002089

17. 防止全表更新和删除

在实际项目开发中,可能由于不小心或者有人恶意将整个表格的数据修改、或删除。这对项目/产品的影响是非常大的,可能会导致项目/产品失败。

MyBatis Plus 提供了 BlockAttackInnerInterceptor 插件,该插件可以阻止全表更新和删除操作。在一定程度上,保证了数据库数据的安全。下面将通过示例介绍怎样使用该插件:

实现步骤

  1. 添加配置
@Bean
public MybatisPlusInterceptor blockAttackInterceptor() {
  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  // 防止全表更新与删除
  // 针对 update 和 delete 语句
  interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
  return interceptor;
}
  1. 测试
@Test
public void testUpdateAll(){
  User2 user2 = new User2();
  user2.setAge(100);
  //更新全表
  user2Mapper.update(user2,null);
}

控制台报错

### Error updating database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation

上面的错误信息中,“Prohibition of table update operation” 禁止全表更新操作,这样就能阻止全表更新和删除操作

18.Mybatis-plus逆向工程

官网指南 - https://baomidou.com/guide/

这个具体的代码实现不在此工程中,如果想要更加清晰的知道工程的结构,

请移步到https://gitee.com/guancg/success-yeb

具体步骤

  1. 依赖
<?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>tech.aistar</groupId>
        <artifactId>yeb-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>yeb-generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>yeb-generator</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--freemarker-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 配置类
package tech.aistar.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
    /**
        * <p>
        * 读取控制台内容
        * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }
    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/yeb-generator/src/main/java");
        gc.setAuthor("亲爱的小管");
        // 打开输出目录
        gc.setOpen(false);
        // xml开启BaseResultMap
        gc.setBaseResultMap(true);
        // xml开启BaseColumnList
        gc.setBaseColumnList(true);
        //实体属性 Swagger2 注解
        gc.setSwagger2(true);
        mpg.setGlobalConfig(gc);
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);
        // 包配置
        PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("模块名"));
        pc.setParent("tech.aistar")
                .setEntity("pojo")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);
        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };
        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";
        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/yeb-generator/src/main/resources/mapper/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);
        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        // 数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.no_change);
//        strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        // lombok
        strategy.setEntityLombokModel(true);
        // 生产restController
        strategy.setRestControllerStyle(true);
        // 公共父类
//        strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
//        strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

目录

目录1. 人生苦短2. Mybatis-Plus简介3. 适合人群5. 核心功能6. CRUD接口6.1 Mapper CRUD接口6.1.1 Insert方法6.1.2 Delete方法6.1.3 Update方法6.1.4 Select方法6.2 Service接口6.2.1 使用方略示例6.2.2 Save方法6.2.3 SaveOrUpdate6.2.4 Remove方法6.2.5 Update6.2.6 Get6.2.7 List6.2.8 Page6.2.9 Count6.3.0 Chain7. 条件构造器7.1 常用方法7.2 常用方法示例7.3 Condition7.4 实体对象作为条件8. lambda条件构造器8.1 补充8.2 反思9. 自定义SQL9.1 原生mybatis9.2 mybatis-plus9.2.1 注解配置方式9.2.1 xml配置方式10. 分页查询10.1 selectPage方式10.2 多表联查11.1 局部策略和全局策略12. 配置12.1 基本配置12.2 进阶配置12.2.1 updateStrategy13. 高级功能13.1. 逻辑删除13.2 自动填充14. 乐观锁插件实现原理实现步骤注意点15. 性能分析插件实现步骤16. 多租户SQL解析器17. 防止全表更新和删除实现步骤18.Mybatis-plus逆向工程具体步骤

1. 人生苦短

2. Mybatis-Plus简介

  1. MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
  2. 官网指南 - https://baomidou.com/guide/

3. 适合人群

  1. 拥有 Java 开发环境以及相应 IDE
  2. 熟悉 Spring Boot
  3. 熟悉 Maven
  4. 熟悉mybatis框架

#4. 快速开始

  1. 创建SpringBoot工程
  2. 导入依赖
<?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.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>mybatis-plus-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybatis-plus-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');
  1. 实体类
package tech.aistar.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
    * 本类用来演示: 用户实体类
 *
    * @author: success
    * @date: 2021/5/31 9:51 上午
 */
@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    private Date createTime;
}
  1. application.yml文件配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  1. 主程序类
package tech.aistar;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("tech.aistar.mapper")
@SpringBootApplication
public class MybatisPlusDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusDemoApplication.class, args);
    }
}
  1. 单元测试
package tech.aistar.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
    * 本类用来演示:
 *
    * @author: success
    * @date: 2021/5/31 11:14 上午
 */
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;
    /**
        * 测试 - 查询所有
     */
    @Test
    public void testFindAll(){
        userMapper.selectList(null).forEach(System.out::println);
    }
}

总结:可以看到,针对单表的基本CRUD操作,只需要创建好实体类,并创建一个继承自BaseMapper的接口即可,可谓非常简洁。并且,我们注意到,User类中的managerId,createTime属性,自动和数据库表中的manager_id,create_time对应了起来,这是因为mp自动做了数据库下划线命名,到Java类的驼峰命名之间的转化。

5. 核心功能

注解

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

  1. @TableName注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。
  2. @TableId注解在实体类的某一字段上,表示这个字段对应数据库表的主键。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略。
  3. @TableField注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。
  • 排除非表字段
  1. 若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist属性为false,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用statictransient关键字,但个人觉得不是很合理,不做赘述
  • 字段验证策略
  1. 通过insertStrategyupdateStrategywhereStrategy属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。
  • 字段填充策略
  1. 通过fill属性指定,字段为空时会进行自动填充
  2. @Version乐观锁注解
  3. @EnumValue注解在枚举字段上
  4. @TableLogic逻辑删除
  5. @KeySequence序列主键策略(oracle
  6. @InterceptorIgnore插件过滤规则

6. CRUD接口

  1. mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。
  2. mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。
  3. 并且mp还提供了条件构造器Wrapper,可以方便地组装SQL语句中的WHERE条件

6.1 Mapper CRUD接口

Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器

  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器

学习目标 - BaseMapper里提供的方法

6.1.1 Insert方法

// 插入一条记录
int insert(T entity);

参数说明

类型

参数名

描述

T

entity

实体对象

测试代码

/**
  * 测试Insert方法
  */
@Test
public void testInsert(){
  User user = new User();
  user.setName("success");
  user.setAge(18);
  user.setEmail("849962874@qq.com");
  user.setCreateTime(new Date());
  user.setManagerId(2L);
  userMapper.insert(user);
}

6.1.2 Delete方法

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

参数说明

类型

参数名

描述

Wrapper<T>

wrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

测试代码

@Test
public void testDelete(){
  //根据 ID 删除
  //userMapper.deleteById(1399249825152729090L);
  //删除(根据ID 批量删除)
  //        List<Long> ids = new ArrayList<>();
  //        ids.add(5L);
  //        ids.add(3L);
  //        userMapper.deleteBatchIds(ids);
  // 根据 entity 条件,删除记录
  //DELETE FROM user WHERE (manager_id = ?)
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.eq("manager_id",2L);
  //        userMapper.delete(queryWrapper);
  //根据 columnMap 条件,删除记录
  //DELETE FROM user WHERE name = ? 
  Map<String,Object> maps = new HashMap<>();
  maps.put("name","吴组长");
  userMapper.deleteByMap(maps);
}

6.1.3 Update方法

// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

参数说明

类型

参数名

描述

T

entity

实体对象 (set 条件值,可为 null)

Wrapper<T>

updateWrapper

实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

测试代码

@Test
public void testUpdate(){
  //        User user = userMapper.selectById(1L);
  //        user.setName("success");
  //        userMapper.updateById(user);
  //第一种写法 - 使用set()方法单独更新某个列
  //        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  //        updateWrapper.ge("age",40).set("age",100);
  //
  //        userMapper.update(null,updateWrapper);
  //第二种写法 - 使用对象
  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",23).le("age",30);
  User user = new User();
  user.setAge(18);
  userMapper.update(user,updateWrapper);
}

6.1.4 Select方法

// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Map<String, Object>

columnMap

表字段 map 对象

IPage<T>

page

分页查询条件(可以为 RowBounds.DEFAULT)

部分测试代码

select Maps

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

该方法的使用场景如下:

  1. 只查部分列

当某个表的列特别多,而SELECT的时候只需要选取个别列,查询出的结果也没必要封装成Java实体类对象时(只查部分列时,封装成实体后,实体对象中的很多属性会是null),则可以用selectMaps,获取到指定的列后,再自行进行处理即可

测试代码

//SELECT id,name FROM user WHERE (name LIKE ?)
//==>  Preparing: SELECT id,name FROM user WHERE (name LIKE ?)
//==> Parameters: 黄%(String)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id","name");
queryWrapper.likeRight("name","黄");
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);

进行数据统计

测试代码

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("manager_id","avg(age)")
  .groupBy("manager_id")
  .having("avg(age)>={0}",59);
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
  1. selectObjs

只会返回第一个字段(第一列)的值,其他字段会被舍弃 - 得到的结果,只封装了第一列的id

QueryWrapper<User> wrapper = new QueryWrapper<>();  
wrapper.select("id", "name").like("name", "黄");  
List<Object> objects = userMapper.selectObjs(wrapper);  
objects.forEach(System.out::println);
  1. selectCount

查询满足条件的总数,注意,使用这个方法,不能调用QueryWrapperselect方法设置要查询的列了。这个方法会自动添加select count(1)

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("age",18);
Integer size = userMapper.selectCount(queryWrapper);
System.out.println(size);

6.2 Service接口

说明:

  • 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行remove 删除list 查询集合page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper 为 条件构造器

6.2.1 使用方略

另外一套CRUD是Service层的,只需要编写一个接口,继承IService,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,比较明显的区别在于IService支持了更多的批量化操作,如saveBatchsaveOrUpdateBatch等方法。

示例

  1. 首先,新建一个接口,继承IService
public interface UserService extends IService<User2> {
}
  1. 创建这个接口的实现类,并继承ServiceImpl,最后打上@Service注解,注册到Spring容器中,即可使用
@Service
public class UserServiceImpl extends ServiceImpl<User2Mapper, User2> implements UserService{
}

6.2.2 Save方法

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)- batchSize - 表示一次批量插入的数据量,默认为 1000
boolean saveBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

单元测试

@Test
public void testSave(){
  List<User2> list = new ArrayList<>();
  for (int i = 0; i < 5; i++) {
    User2 user2 = new User2();
    user2.setAge(22+i);
    user2.setEmail("99999"+i+"@qq.com");
    user2.setName("tom"+i);
    user2.setManagerId(2L);
    list.add(user2);
  }
  //批量插入
  userService.saveBatch(list);
}

6.2.3 SaveOrUpdate

// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

测试01-根据主键值id进行更新操作

/**
  * 测试根据主键值id进行更新
  * 如果实体类的id属性名和表的列主键值id列名保持一致,那么可以不使用@TableId
  * 否则必须使用
  */
@Test
public void testUpdateByPrimaryKey(){
  //        User2 user2 = userService.getById(1);
  //        user2.setAge(33);
  //        //测试对已经存在的记录进行更新
  //        userService.saveOrUpdate(user2);
  User2 user01 = new User2();
  user01.setName("亲爱的管");
  user01.setAge(100);
  //测试对不存在的记录进行插入
  userService.saveOrUpdate(user01);
}

测试02-根据Wrapper条件进行更新

/**
   * 测试根据条件进行更新
   * 如果存在,则更新,否则执行插入
   */
@Test
public void testUpdateByWrapper(){
  UpdateWrapper<User2> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",300);
  User2 u = new User2();
  u.setName("ping");
  userService.saveOrUpdate(u,updateWrapper);
}

6.2.4 Remove方法

// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体包装类 QueryWrapper

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

Collection<? extends Serializable>

idList

主键ID列表

测试代码

@Test
public void testRemove(){
  //本人更加推荐lambda写法
  userService.lambdaUpdate().eq(User2::getId,3).remove();
}

6.2.5 Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

更新批次数量

6.2.6 Get

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
//getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

boolean

throwEx

有多个 result 是否抛出异常

T

entity

实体对象

Function<? super Object, V>

mapper

转换函数

测试

@Test
public void testGet(){
  //        LambdaQueryWrapper<User2> queryWrapper = new LambdaQueryWrapper<>();
  //        queryWrapper.eq(User2::getAge,40);
  //本人更喜欢如下方式
  //这个是方法返回结果不止一条则会抛出异常,如果想默认取第一条结果,可以给这方法传第二个参数为false。
  //User2 user2 = userService.getOne(Wrappers.<User2>lambdaQuery().eq(User2::getAge,40),false);
  //System.out.println(user2);
  //getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
   Integer id = userService.getObj(Wrappers.<User2>lambdaQuery().eq(User2::getId,1),o->{return    Integer.parseInt(o.toString());});
        System.out.println(id);
}

6.2.7 List

// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

Collection<? extends Serializable>

idList

主键ID列表

Map<?String, Object>

columnMap

表字段 map 对象

Function<? super Object, V>

mapper

转换函数

测试代码

@Test
public void testList(){
  userService.listObjs(o->{
    return "user_"+o;
  }).forEach(e-> System.out.println(e));
}

6.2.8 Page

// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

IPage<T>

page

翻页对象

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testPage(){
  Page<User2> page = new Page<>(3,2);
  //        userService.page(page).getRecords().forEach(e-> System.out.println(e));
  userService.page(page,Wrappers.<User2>lambdaQuery().eq(User2::getAge,40))
    .getRecords().forEach(e-> System.out.println(e));
}

6.2.9 Count

// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testCount(){
  System.out.println(userService.count(Wrappers.<User2>lambdaQuery().eq(User2::getAge, 40)));
}

6.3.0 Chain

  1. query
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery(); 
// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();

测试

// Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (age = ? OR (id = ?))
userService.lambdaQuery()
  .eq(User2::getAge,40)
  .or(e1->e1.eq(User2::getId,30))
  .list().forEach(e-> System.out.println(e));
SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (id = ?)
System.out.println(userService.lambdaQuery().eq(User2::getId, 1).one());
  1. update
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin 
LambdaUpdateChainWrapper<T> lambdaUpdate();
// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

测试

@Test
public void testUpdate(){
  //直接更新具体的某列,不需要实体对象
  //userService.lambdaUpdate().eq(User2::getId,1).set(User2::getName,"ss").update();
  //更新一个对象
  User2 user2 = new User2();
  user2.setName("xx");
  user2.setAge(100);
  userService.lambdaUpdate().eq(User2::getId,1).update(user2);
}

7. 条件构造器

mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器主要涉及到3个类,AbstractWrapperQueryWrapperUpdateWrapper,它们的类关系如下:

AbstractWrapper中提供了非常多的方法用于构建WHERE条件,而QueryWrapper针对SELECT语句,提供了select()方法,可自定义需要查询的列,而UpdateWrapper针对UPDATE语句,提供了set()方法,用于构造set语句。条件构造器也支持lambda表达式,写起来非常舒爽。

7.1 常用方法

查询方式

说明

setSqlSelect

设置 SELECT 查询字段

where

WHERE 语句,拼接 +?WHERE 条件

and

AND 语句,拼接 +?AND 字段=值

andNew

AND 语句,拼接 +?AND (字段=值)

or

OR 语句,拼接 +?OR 字段=值

orNew

OR 语句,拼接 +?OR (字段=值)

eq

等于=

allEq

基于 map 内容等于=

ne

不等于<>

gt

大于>

ge

大于等于>=

lt

小于<

le

小于等于<=

like

模糊查询 LIKE

notLike

模糊查询 NOT LIKE

in

IN 查询

notIn

NOT IN 查询

isNull

NULL 值查询

isNotNull

IS NOT NULL

groupBy

分组 GROUP BY

having

HAVING 关键词

orderBy

排序 ORDER BY

orderAsc

ASC 排序 ORDER BY

orderDesc

DESC 排序 ORDER BY

exists

EXISTS 条件语句

notExists

NOT EXISTS 条件语句

between

BETWEEN 条件语句

notBetween

NOT BETWEEN 条件语句

addFilter

自由拼接 SQL

last

拼接在最后,例如:last(“LIMIT 1”)

下面对AbstractWrapper中用于构建SQL语句中的WHERE条件的方法进行部分列举:

  1. allEq - 全部eq
@Test
public void test条件构造器(){
  //SELECT id,name,age,email,manager_id,create_time FROM user WHERE (name = ? AND age = ?)
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> conditional = new HashMap<>();
  conditional.put("age",18);
  conditional.put("name","小菜");
  queryWrapper.allEq(conditional);
  List<User> users = userMapper.selectList(queryWrapper);
  users.forEach(e-> System.out.println(e));
}

细节01-当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);  
}

细节02 - 若想忽略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);  
}

细节03 - 忽略某个key的查询 - SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age = ?)

查询语句中只会出现age列,不会出现name列

QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 18);
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);

7.2 常用方法示例

@Test
public void test条件构造器02(){
  //1. 名字中包含三,且年龄小于25
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.like("name","三")
  //                    .lt("age",25);
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //2.姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.likeRight("name","黄").and(
  //                (w->w.gt("age",20)
  //                        .lt("age",40).isNotNull("email"))
  //        );
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //3.或者年龄大于等于23,按照年龄降序排列,年龄相同则按照id升序排列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.ge("age",23)
  //                    .orderByDesc("age")
  //                    .orderByAsc("id");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //4.创建日期为2021年2月22日,并且直属上级的名字为小姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.apply("date_format(create_time,'%Y-%m-%d')={0}","2021-02-22")
  //                     .inSql("manager_id","select id from user where name like '小%'");
  //
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //5. 正常嵌套
  //(年龄小于40或者邮箱不为空) 并且名字为黄姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.nested(c->c.lt("age",40).or().isNotNull("email"))
  //                .likeRight("name","黄")
  //
  //        //6.last
  //        //.只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
  //                .last("limit 1");
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //7. 筛选部分列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.select("id","name");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //8. 选出id, name, age, email, 等同于排除 manager_id 和 create_time
  // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列,
  // 可以采用重载的select方法,指定需要排除的列
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.select(User.class,info->{
    String colName = info.getColumn();
    return !colName.equals("manager_id") && !colName.equals("create_time");
  });
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}

7.3 Condition

条件构造器的诸多方法中,均可以指定一个boolean类型的参数condition,用来决定该条件是否加入最后生成的WHERE语句中,比如

/**
  * 测试boolean参数
*/
@Test
public void testBooleanParam(){
String name = "";
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中
// 其实就是对下面代码的简化
//如果字符串里面的值为null, "", "   ",那么返回值为false;否则为true
//        if (StringUtils.hasText(name)) {
//            wrapper.like("name", name);
//        }
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(e-> System.out.println(e));
}

7.4 实体对象作为条件

调用构造函数创建一个Wrapper对象时,可以传入一个实体对象。

后续使用这个Wrapper时,会以实体对象中的非空属性,构建WHERE条件(默认构建等值匹配的WHERE条件,

这个行为可以通过实体类里各个字段上的@TableField注解中的condition属性进行改变)

  1. 示例01 - 会以实体对象中的非空属性,构建WHERE条件

执行结果可以看到,是根据实体对象中的非空属性,进行了等值匹配查询

@Test
public void testObjParams(){
  User user = new User();
  user.setName("小菜");
  QueryWrapper<User> queryWrapper = new QueryWrapper<>(user);
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}
  1. 示例02 - 若希望针对某些属性,改变等值匹配的行为,则可以在实体类中用@TableField注解进行配置,示例如下
@TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择
SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如
@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;  
}

8. lambda条件构造器

lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的方法引用来指定列。示例如下

@Test
public void testLambda(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(User::getName,"三");
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(System.out::println);
}

8.1 补充

  1. 还有个链式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);  
}
  1. 更新操作
@Test
public void testLambdaUpdate(){
  User user = new User();
  user.setName("james");
  LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
  lambdaUpdateWrapper.eq(User::getId,1);
  userMapper.update(user,lambdaUpdateWrapper);
}
  1. 再额外演示一下,链式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();  
}

8.2 反思

由于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);  
 }

9. 自定义SQL

当mp提供的方法还不能满足需求时,则可以自定义SQL。

9.1 原生mybatis

public interface UserMapper extends BaseMapper<User> {
    @Select("select * from user")
    List<User> findAll();
}

9.2 mybatis-plus

也可以使用mp提供的Wrapper条件构造器,来自定义SQL

在UserMapper.java接口添加

9.2.1 注解配置方式

//自定义sql - mybatis-plus方式
@Select("select * from user ${ew.customSqlSegment}")
List<User> findAllWhere(@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

/**
  * 自定义SQL
  * 2. 采用原生的mybatis
  * 也可以使用mp提供的Wrapper条件构造器,来自定义SQL
  */
@Test
public void testFindAllWhere(){
  LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  wrapper.like(User::getName,"三");
  List<User> userList = userMapper.findAllWhere(wrapper);
  userList.forEach(user -> System.out.println(user));
}

9.2.1 xml配置方式

List<User> findAll(Wrapper<User> wrapper);
<mapper namespace="tech.aistar.mapper.UserMapper">  
  <select id="findAll" resultType="tech.aistar.pojo.User">  
    SELECT * FROM user ${ew.customSqlSegment}  
  </select>  
</mapper>

10. 分页查询

BaseMapper中提供了2个方法进行分页查询,分别是

  1. selectPage:将查询的结果封装成Java实体对象
  2. selectMapsPage:将查询的结果封装成封装成Map<String,Object>

10.1 selectPage方式

  1. 创建mp的分页拦截器,注册到Spring容器中

config/

@Configuration
public class MybatisplusConfig {
 @Bean
 public MybatisPlusInterceptor mybatisPlusInterceptor(){
     MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
     PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
     paginationInnerInterceptor.setDbType(DbType.MYSQL);
     paginationInnerInterceptor.setOverflow(true);
     interceptor.addInnerInterceptor(paginationInnerInterceptor);
     return interceptor;
 }
}
@Test
public void testSelectPage(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.ge(User::getAge,20);
//设置分页信息,查询第3页,每页显示2条
Page<User> page = new Page<>(3,2);
//执行分页查询
Page<User> pageInfo = userMapper.selectPage(page,lambdaQueryWrapper);
//展示分页的信息
System.out.println("总的记录数[条数rows]:"+page.getTotal());
System.out.println("总的页数:"+page.getPages());
System.out.println("每页显示条数:"+page.getSize());
//展示分页具体返回的内容
page.getRecords().forEach(System.out::println);
}

控制台sql分析

==>  Preparing: SELECT COUNT(*) FROM user WHERE (age >= ?)
==> Parameters: 20(Integer)
<==    Columns: COUNT(*)
<==        Row: 6
<==      Total: 1
==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2

注意到,分页查询总共发出了2次SQL,一次查总记录数,一次查具体数据。若希望不查总记录数,仅查分页结果。可以通过Page的重载构造函数,指定isSearchCountfalse即可

public Page(long current, long size, boolean isSearchCount);
Page<User> page = new Page<>(3,2,false);

设置成第三个参数为false,之后那么只会出现一条sql,与此同时总的记录数和总的页数都将返回默认值0.

==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@57545c3f]
总的记录数[条数rows]:0
总的页数:0
每页显示条数:2

10.2 多表联查

在实际开发中,可能遇到多表联查的场景,此时BaseMapper中提供的单表分页查询的方法无法满足需求,需要自定义SQL,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,修改SQL语句即可)

/**
  *在实际开发中,可能遇到**多表联查**的场景,此时`BaseMapper`中提供的单表分页查询的方法无法满足需求,
  * 需要**自定义SQL**,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,
  * 修改SQL语句即可)
  *
  * 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式
  * @param page
  * @param wrapper
  * @return
  */
@Select("select * from user ${ew.customSqlSegment}")
Page<User> selectUserPage(Page<User> page,@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

@Test
public void testSelectUserPage(){
  LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
  lambdaQueryWrapper.ge(User::getAge,20);
  //设置分页信息,查询第3页,每页显示2条
  //第三个参数设置成false,不会查询总的记录数
  //Page<User> page = new Page<>(3,2,false);
  Page<User> page = new Page<>(3,2);
  //执行分页查询
  Page<User> pageInfo = userMapper.selectUserPage(page,lambdaQueryWrapper);
  //展示分页的信息
  System.out.println("总的记录数[条数rows]:"+page.getTotal());
  System.out.println("总的页数:"+page.getPages());
  System.out.println("每页显示条数:"+page.getSize());
  //展示分页具体返回的内容
  page.getRecords().forEach(System.out::println);
}

#11. Sequence主键生成策略

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

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

  • AUTO

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

并且插入成功之后,主键会被写回实体对象。

  • NONE

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

  • INPUT

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

  • ASSIGN_ID

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

  • ASSIGN_UUID

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

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

11.1 局部策略和全局策略

可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略

mybatis-plus:
  global-config:
    db-config:
      id-type: auto

注意一旦设置成了auto,需要修改主键为自增长,修改脚本文件-添加auto_increment否则会抛出id does not default value的异常

DROP TABLE IF EXISTS user;  
   CREATE TABLE user (  
     id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');

12. 配置

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

12.1 基本配置

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

12.2 进阶配置

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

12.2.1 updateStrategy

有如下几种可选配置这个配置项,可在application.yml中进行全局配置,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置

这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。

而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL

  • IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。
  • NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中
  • NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL
  • NEVER:不加入SQL。所有字段不加入到SQL语句
  • tablePrefix:添加表名前缀
mybatis-plus:  
   global-config:  
     db-config:  
       table-prefix: xx_
@Test  
public void test3() {  
 QueryWrapper<User> wrapper = new QueryWrapper<>();  
 wrapper.like("name", "黄");  
 Integer count = userMapper.selectCount(wrapper);  
 System.out.println(count);  
}

可以看到拼接出来的SQL,在表名前面添加了前缀

13. 高级功能

高级功能的演示需要用到一张新的表user2

  • sql脚本
DROP TABLE IF EXISTS user2;  
CREATE TABLE user2 (  
id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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_user2 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实体类
@Data  
public class User2 {  
private Long id;  
private String name;  
private Integer age;  
private String email;  
private Long managerId;  
private Date createTime;  
private Date updateTime;  
private Integer version;  
private Integer deleted;  
}
  • User2Mapper接口
public interface User2Mapper extends BaseMapper<User2> {
}

13.1. 逻辑删除

首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案

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

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项

测试代码

user2Mapper.deleteById(1);

分析控制台SQL

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

可以看到,发出的SQL不再是DELETE,而是UPDATE

此时我们再执行一次SELECT

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

分析控制台SQL

==>  Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user2 WHERE deleted=0
==> Parameters: 
<==    Columns: id, name, age, email, manager_id, create_time, update_time, version, deleted
<==        Row: 2, 王狗蛋, 40, gd@baomidou.com, 1, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 3, 王鸡蛋, 40, jd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 4, 王鸭蛋, 40, yd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 5, 王猪蛋, 40, zd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 6, 王软蛋, 40, rd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 7, 王铁蛋, 40, td@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==      Total: 6

可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为1的老板

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

@TableField(select = false)
private Integer deleted;

分析控制台SQL

SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE deleted=0

查询的列中已经不包括逻辑删除的那一列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进行局部配置。

13.2 自动填充

表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。

比较原始的方式,是每次插入或更新时,手动进行设置。

mp可以通过配置,对某些字段进行自动填充,实用实例如下

原理

  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
  • 注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置

在实体类中的某些字段上,通过@TableField设置自动填充

@Data
public class User2 implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    //自动填充
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.UPDATE)
    private Date updateTime;
    private Integer version;
    //逻辑删除 - '逻辑删除标识,0-未删除,1-已删除'
    @TableField(select = false)
//    @TableLogic(delval = "1",value = "0")
    private Integer deleted;
}

实现自动填充处理器

package tech.aistar.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Date;
import java.util.function.Supplier;
/**
 * 本类用来演示: 实现自动填充处理器
 *
 * @author: success
 * @date: 2021/6/3 9:59 上午
 */
@Component //需要注册到Spring容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
//        this.strictFillStrategy(metaObject, "createTime", new Supplier<Object>() {
//            @Override
//            public Object get() {
//                return new Date();
//            }
//        });
        this.strictFillStrategy(metaObject, "createTime",Date::new);
    }
    @Override
    public void updateFill(MetaObject metaObject) {
         this.strictFillStrategy(metaObject,"updateTime",Date::new);
    }
}

单元测试-insert

@SpringBootTest
public class TestMyMetaObjectHandler {
    @Autowired
    private User2Mapper user2Mapper;
    /**
     * 测试自动填充 - createTime字段
     */
    @Test
    public void testInsert(){
        User2 user = new User2();
        user.setName("强老师");
        user.setAge(29);
        user.setEmail("yt@baomidou.com");
        user.setManagerId(2L);
        user2Mapper.insert(user);
    }
}

可以看到对createTime进行了自动填充,注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值.

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

@Test
public void testUpdate(){
  User2 user2 = new User2();
  user2.setId(8L);
  user2.setName("平老师");
  user2Mapper.updateById(user2);
}

14. 乐观锁插件

当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。

在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。

在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。

实现原理

乐观锁的实现如下:

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

这种思想和CAS(Compare And Swap)非常相似。

实现步骤

  1. 配置乐观锁插件
/**
     * 乐观锁插件 - 新版
  */
@Bean
public MybatisPlusInterceptor oPtimisticLockerInterceptor() {
  MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
  mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
  return mybatisPlusInterceptor;
}
  1. 在实体类中表示版本的字段上添加注解@Version
//乐观锁- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
@Version
private Integer version;
  1. 测试代码
@Test
public void testLock(){
  // 假设这个version是先前查询时获得的
  int version=1;
  User2 user2 = new User2();
  user2.setId(1L);
  user2.setName("管管");
  user2.setVersion(version);
  user2Mapper.updateById(user2);
}

SQL打印如下,可以发现version从一开始的值1变成2

==>  Preparing: UPDATE user2 SET name=?, update_time=?, version=? WHERE id=? AND version=?
==> Parameters: 管管(String), 2021-06-08 09:10:44.678(Timestamp), 2(Integer), 1(Long), 1(Integer)
<==    Updates: 1

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

注意点

注意,乐观锁插件仅支持updateById(id)update(entity, 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,是有问题的。

15. 性能分析插件

该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。

注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐使第三方性能分析插件

该插件有性能损耗,不建议生产环境使用

实现步骤

  1. 引入第三方依赖
<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
    url: jdbc:p6spy:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    # url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
#    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  1. src/main/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
  1. 随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来
Consume Time:8 ms 2021-06-08 09:30:35
 Execute SQL:select * from user

16. 多租户SQL解析器

多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。

此处内容作为课外了解 - 想要进一步学习,可参考 - https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/105002089

17. 防止全表更新和删除

在实际项目开发中,可能由于不小心或者有人恶意将整个表格的数据修改、或删除。这对项目/产品的影响是非常大的,可能会导致项目/产品失败。

MyBatis Plus 提供了 BlockAttackInnerInterceptor 插件,该插件可以阻止全表更新和删除操作。在一定程度上,保证了数据库数据的安全。下面将通过示例介绍怎样使用该插件:

实现步骤

  1. 添加配置
@Bean
public MybatisPlusInterceptor blockAttackInterceptor() {
  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  // 防止全表更新与删除
  // 针对 update 和 delete 语句
  interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
  return interceptor;
}
  1. 测试
@Test
public void testUpdateAll(){
  User2 user2 = new User2();
  user2.setAge(100);
  //更新全表
  user2Mapper.update(user2,null);
}

控制台报错

### Error updating database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation

上面的错误信息中,“Prohibition of table update operation” 禁止全表更新操作,这样就能阻止全表更新和删除操作

18.Mybatis-plus逆向工程

官网指南 - https://baomidou.com/guide/

这个具体的代码实现不在此工程中,如果想要更加清晰的知道工程的结构,

请移步到https://gitee.com/guancg/success-yeb

具体步骤

  1. 依赖
<?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>tech.aistar</groupId>
        <artifactId>yeb-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>yeb-generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>yeb-generator</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--freemarker-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 配置类
package tech.aistar.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
    /**
        * <p>
        * 读取控制台内容
        * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }
    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/yeb-generator/src/main/java");
        gc.setAuthor("亲爱的小管");
        // 打开输出目录
        gc.setOpen(false);
        // xml开启BaseResultMap
        gc.setBaseResultMap(true);
        // xml开启BaseColumnList
        gc.setBaseColumnList(true);
        //实体属性 Swagger2 注解
        gc.setSwagger2(true);
        mpg.setGlobalConfig(gc);
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);
        // 包配置
        PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("模块名"));
        pc.setParent("tech.aistar")
                .setEntity("pojo")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);
        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };
        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";
        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/yeb-generator/src/main/resources/mapper/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);
        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        // 数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.no_change);
//        strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        // lombok
        strategy.setEntityLombokModel(true);
        // 生产restController
        strategy.setRestControllerStyle(true);
        // 公共父类
//        strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
//        strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

目录

目录1. 人生苦短2. Mybatis-Plus简介3. 适合人群5. 核心功能6. CRUD接口6.1 Mapper CRUD接口6.1.1 Insert方法6.1.2 Delete方法6.1.3 Update方法6.1.4 Select方法6.2 Service接口6.2.1 使用方略示例6.2.2 Save方法6.2.3 SaveOrUpdate6.2.4 Remove方法6.2.5 Update6.2.6 Get6.2.7 List6.2.8 Page6.2.9 Count6.3.0 Chain7. 条件构造器7.1 常用方法7.2 常用方法示例7.3 Condition7.4 实体对象作为条件8. lambda条件构造器8.1 补充8.2 反思9. 自定义SQL9.1 原生mybatis9.2 mybatis-plus9.2.1 注解配置方式9.2.1 xml配置方式10. 分页查询10.1 selectPage方式10.2 多表联查11.1 局部策略和全局策略12. 配置12.1 基本配置12.2 进阶配置12.2.1 updateStrategy13. 高级功能13.1. 逻辑删除13.2 自动填充14. 乐观锁插件实现原理实现步骤注意点15. 性能分析插件实现步骤16. 多租户SQL解析器17. 防止全表更新和删除实现步骤18.Mybatis-plus逆向工程具体步骤

1. 人生苦短

2. Mybatis-Plus简介

  1. MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
  2. 官网指南 - https://baomidou.com/guide/

3. 适合人群

  1. 拥有 Java 开发环境以及相应 IDE
  2. 熟悉 Spring Boot
  3. 熟悉 Maven
  4. 熟悉mybatis框架

#4. 快速开始

  1. 创建SpringBoot工程
  2. 导入依赖
<?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.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>mybatis-plus-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybatis-plus-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');
  1. 实体类
package tech.aistar.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
    * 本类用来演示: 用户实体类
 *
    * @author: success
    * @date: 2021/5/31 9:51 上午
 */
@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    private Date createTime;
}
  1. application.yml文件配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  1. 主程序类
package tech.aistar;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("tech.aistar.mapper")
@SpringBootApplication
public class MybatisPlusDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusDemoApplication.class, args);
    }
}
  1. 单元测试
package tech.aistar.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
    * 本类用来演示:
 *
    * @author: success
    * @date: 2021/5/31 11:14 上午
 */
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;
    /**
        * 测试 - 查询所有
     */
    @Test
    public void testFindAll(){
        userMapper.selectList(null).forEach(System.out::println);
    }
}

总结:可以看到,针对单表的基本CRUD操作,只需要创建好实体类,并创建一个继承自BaseMapper的接口即可,可谓非常简洁。并且,我们注意到,User类中的managerId,createTime属性,自动和数据库表中的manager_id,create_time对应了起来,这是因为mp自动做了数据库下划线命名,到Java类的驼峰命名之间的转化。

5. 核心功能

注解

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

  1. @TableName注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。
  2. @TableId注解在实体类的某一字段上,表示这个字段对应数据库表的主键。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略。
  3. @TableField注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。
  • 排除非表字段
  1. 若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist属性为false,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用statictransient关键字,但个人觉得不是很合理,不做赘述
  • 字段验证策略
  1. 通过insertStrategyupdateStrategywhereStrategy属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。
  • 字段填充策略
  1. 通过fill属性指定,字段为空时会进行自动填充
  2. @Version乐观锁注解
  3. @EnumValue注解在枚举字段上
  4. @TableLogic逻辑删除
  5. @KeySequence序列主键策略(oracle
  6. @InterceptorIgnore插件过滤规则

6. CRUD接口

  1. mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。
  2. mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。
  3. 并且mp还提供了条件构造器Wrapper,可以方便地组装SQL语句中的WHERE条件

6.1 Mapper CRUD接口

Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器

  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器

学习目标 - BaseMapper里提供的方法

6.1.1 Insert方法

// 插入一条记录
int insert(T entity);

参数说明

类型

参数名

描述

T

entity

实体对象

测试代码

/**
  * 测试Insert方法
  */
@Test
public void testInsert(){
  User user = new User();
  user.setName("success");
  user.setAge(18);
  user.setEmail("849962874@qq.com");
  user.setCreateTime(new Date());
  user.setManagerId(2L);
  userMapper.insert(user);
}

6.1.2 Delete方法

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

参数说明

类型

参数名

描述

Wrapper<T>

wrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

测试代码

@Test
public void testDelete(){
  //根据 ID 删除
  //userMapper.deleteById(1399249825152729090L);
  //删除(根据ID 批量删除)
  //        List<Long> ids = new ArrayList<>();
  //        ids.add(5L);
  //        ids.add(3L);
  //        userMapper.deleteBatchIds(ids);
  // 根据 entity 条件,删除记录
  //DELETE FROM user WHERE (manager_id = ?)
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.eq("manager_id",2L);
  //        userMapper.delete(queryWrapper);
  //根据 columnMap 条件,删除记录
  //DELETE FROM user WHERE name = ? 
  Map<String,Object> maps = new HashMap<>();
  maps.put("name","吴组长");
  userMapper.deleteByMap(maps);
}

6.1.3 Update方法

// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

参数说明

类型

参数名

描述

T

entity

实体对象 (set 条件值,可为 null)

Wrapper<T>

updateWrapper

实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

测试代码

@Test
public void testUpdate(){
  //        User user = userMapper.selectById(1L);
  //        user.setName("success");
  //        userMapper.updateById(user);
  //第一种写法 - 使用set()方法单独更新某个列
  //        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  //        updateWrapper.ge("age",40).set("age",100);
  //
  //        userMapper.update(null,updateWrapper);
  //第二种写法 - 使用对象
  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",23).le("age",30);
  User user = new User();
  user.setAge(18);
  userMapper.update(user,updateWrapper);
}

6.1.4 Select方法

// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Map<String, Object>

columnMap

表字段 map 对象

IPage<T>

page

分页查询条件(可以为 RowBounds.DEFAULT)

部分测试代码

select Maps

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

该方法的使用场景如下:

  1. 只查部分列

当某个表的列特别多,而SELECT的时候只需要选取个别列,查询出的结果也没必要封装成Java实体类对象时(只查部分列时,封装成实体后,实体对象中的很多属性会是null),则可以用selectMaps,获取到指定的列后,再自行进行处理即可

测试代码

//SELECT id,name FROM user WHERE (name LIKE ?)
//==>  Preparing: SELECT id,name FROM user WHERE (name LIKE ?)
//==> Parameters: 黄%(String)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id","name");
queryWrapper.likeRight("name","黄");
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);

进行数据统计

测试代码

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("manager_id","avg(age)")
  .groupBy("manager_id")
  .having("avg(age)>={0}",59);
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
  1. selectObjs

只会返回第一个字段(第一列)的值,其他字段会被舍弃 - 得到的结果,只封装了第一列的id

QueryWrapper<User> wrapper = new QueryWrapper<>();  
wrapper.select("id", "name").like("name", "黄");  
List<Object> objects = userMapper.selectObjs(wrapper);  
objects.forEach(System.out::println);
  1. selectCount

查询满足条件的总数,注意,使用这个方法,不能调用QueryWrapperselect方法设置要查询的列了。这个方法会自动添加select count(1)

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("age",18);
Integer size = userMapper.selectCount(queryWrapper);
System.out.println(size);

6.2 Service接口

说明:

  • 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行remove 删除list 查询集合page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper 为 条件构造器

6.2.1 使用方略

另外一套CRUD是Service层的,只需要编写一个接口,继承IService,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,比较明显的区别在于IService支持了更多的批量化操作,如saveBatchsaveOrUpdateBatch等方法。

示例

  1. 首先,新建一个接口,继承IService
public interface UserService extends IService<User2> {
}
  1. 创建这个接口的实现类,并继承ServiceImpl,最后打上@Service注解,注册到Spring容器中,即可使用
@Service
public class UserServiceImpl extends ServiceImpl<User2Mapper, User2> implements UserService{
}

6.2.2 Save方法

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)- batchSize - 表示一次批量插入的数据量,默认为 1000
boolean saveBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

单元测试

@Test
public void testSave(){
  List<User2> list = new ArrayList<>();
  for (int i = 0; i < 5; i++) {
    User2 user2 = new User2();
    user2.setAge(22+i);
    user2.setEmail("99999"+i+"@qq.com");
    user2.setName("tom"+i);
    user2.setManagerId(2L);
    list.add(user2);
  }
  //批量插入
  userService.saveBatch(list);
}

6.2.3 SaveOrUpdate

// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

测试01-根据主键值id进行更新操作

/**
  * 测试根据主键值id进行更新
  * 如果实体类的id属性名和表的列主键值id列名保持一致,那么可以不使用@TableId
  * 否则必须使用
  */
@Test
public void testUpdateByPrimaryKey(){
  //        User2 user2 = userService.getById(1);
  //        user2.setAge(33);
  //        //测试对已经存在的记录进行更新
  //        userService.saveOrUpdate(user2);
  User2 user01 = new User2();
  user01.setName("亲爱的管");
  user01.setAge(100);
  //测试对不存在的记录进行插入
  userService.saveOrUpdate(user01);
}

测试02-根据Wrapper条件进行更新

/**
   * 测试根据条件进行更新
   * 如果存在,则更新,否则执行插入
   */
@Test
public void testUpdateByWrapper(){
  UpdateWrapper<User2> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",300);
  User2 u = new User2();
  u.setName("ping");
  userService.saveOrUpdate(u,updateWrapper);
}

6.2.4 Remove方法

// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体包装类 QueryWrapper

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

Collection<? extends Serializable>

idList

主键ID列表

测试代码

@Test
public void testRemove(){
  //本人更加推荐lambda写法
  userService.lambdaUpdate().eq(User2::getId,3).remove();
}

6.2.5 Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

更新批次数量

6.2.6 Get

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
//getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

boolean

throwEx

有多个 result 是否抛出异常

T

entity

实体对象

Function<? super Object, V>

mapper

转换函数

测试

@Test
public void testGet(){
  //        LambdaQueryWrapper<User2> queryWrapper = new LambdaQueryWrapper<>();
  //        queryWrapper.eq(User2::getAge,40);
  //本人更喜欢如下方式
  //这个是方法返回结果不止一条则会抛出异常,如果想默认取第一条结果,可以给这方法传第二个参数为false。
  //User2 user2 = userService.getOne(Wrappers.<User2>lambdaQuery().eq(User2::getAge,40),false);
  //System.out.println(user2);
  //getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
   Integer id = userService.getObj(Wrappers.<User2>lambdaQuery().eq(User2::getId,1),o->{return    Integer.parseInt(o.toString());});
        System.out.println(id);
}

6.2.7 List

// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

Collection<? extends Serializable>

idList

主键ID列表

Map<?String, Object>

columnMap

表字段 map 对象

Function<? super Object, V>

mapper

转换函数

测试代码

@Test
public void testList(){
  userService.listObjs(o->{
    return "user_"+o;
  }).forEach(e-> System.out.println(e));
}

6.2.8 Page

// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

IPage<T>

page

翻页对象

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testPage(){
  Page<User2> page = new Page<>(3,2);
  //        userService.page(page).getRecords().forEach(e-> System.out.println(e));
  userService.page(page,Wrappers.<User2>lambdaQuery().eq(User2::getAge,40))
    .getRecords().forEach(e-> System.out.println(e));
}

6.2.9 Count

// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testCount(){
  System.out.println(userService.count(Wrappers.<User2>lambdaQuery().eq(User2::getAge, 40)));
}

6.3.0 Chain

  1. query
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery(); 
// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();

测试

// Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (age = ? OR (id = ?))
userService.lambdaQuery()
  .eq(User2::getAge,40)
  .or(e1->e1.eq(User2::getId,30))
  .list().forEach(e-> System.out.println(e));
SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (id = ?)
System.out.println(userService.lambdaQuery().eq(User2::getId, 1).one());
  1. update
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin 
LambdaUpdateChainWrapper<T> lambdaUpdate();
// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

测试

@Test
public void testUpdate(){
  //直接更新具体的某列,不需要实体对象
  //userService.lambdaUpdate().eq(User2::getId,1).set(User2::getName,"ss").update();
  //更新一个对象
  User2 user2 = new User2();
  user2.setName("xx");
  user2.setAge(100);
  userService.lambdaUpdate().eq(User2::getId,1).update(user2);
}

7. 条件构造器

mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器主要涉及到3个类,AbstractWrapperQueryWrapperUpdateWrapper,它们的类关系如下:

AbstractWrapper中提供了非常多的方法用于构建WHERE条件,而QueryWrapper针对SELECT语句,提供了select()方法,可自定义需要查询的列,而UpdateWrapper针对UPDATE语句,提供了set()方法,用于构造set语句。条件构造器也支持lambda表达式,写起来非常舒爽。

7.1 常用方法

查询方式

说明

setSqlSelect

设置 SELECT 查询字段

where

WHERE 语句,拼接 +?WHERE 条件

and

AND 语句,拼接 +?AND 字段=值

andNew

AND 语句,拼接 +?AND (字段=值)

or

OR 语句,拼接 +?OR 字段=值

orNew

OR 语句,拼接 +?OR (字段=值)

eq

等于=

allEq

基于 map 内容等于=

ne

不等于<>

gt

大于>

ge

大于等于>=

lt

小于<

le

小于等于<=

like

模糊查询 LIKE

notLike

模糊查询 NOT LIKE

in

IN 查询

notIn

NOT IN 查询

isNull

NULL 值查询

isNotNull

IS NOT NULL

groupBy

分组 GROUP BY

having

HAVING 关键词

orderBy

排序 ORDER BY

orderAsc

ASC 排序 ORDER BY

orderDesc

DESC 排序 ORDER BY

exists

EXISTS 条件语句

notExists

NOT EXISTS 条件语句

between

BETWEEN 条件语句

notBetween

NOT BETWEEN 条件语句

addFilter

自由拼接 SQL

last

拼接在最后,例如:last(“LIMIT 1”)

下面对AbstractWrapper中用于构建SQL语句中的WHERE条件的方法进行部分列举:

  1. allEq - 全部eq
@Test
public void test条件构造器(){
  //SELECT id,name,age,email,manager_id,create_time FROM user WHERE (name = ? AND age = ?)
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> conditional = new HashMap<>();
  conditional.put("age",18);
  conditional.put("name","小菜");
  queryWrapper.allEq(conditional);
  List<User> users = userMapper.selectList(queryWrapper);
  users.forEach(e-> System.out.println(e));
}

细节01-当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);  
}

细节02 - 若想忽略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);  
}

细节03 - 忽略某个key的查询 - SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age = ?)

查询语句中只会出现age列,不会出现name列

QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 18);
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);

7.2 常用方法示例

@Test
public void test条件构造器02(){
  //1. 名字中包含三,且年龄小于25
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.like("name","三")
  //                    .lt("age",25);
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //2.姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.likeRight("name","黄").and(
  //                (w->w.gt("age",20)
  //                        .lt("age",40).isNotNull("email"))
  //        );
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //3.或者年龄大于等于23,按照年龄降序排列,年龄相同则按照id升序排列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.ge("age",23)
  //                    .orderByDesc("age")
  //                    .orderByAsc("id");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //4.创建日期为2021年2月22日,并且直属上级的名字为小姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.apply("date_format(create_time,'%Y-%m-%d')={0}","2021-02-22")
  //                     .inSql("manager_id","select id from user where name like '小%'");
  //
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //5. 正常嵌套
  //(年龄小于40或者邮箱不为空) 并且名字为黄姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.nested(c->c.lt("age",40).or().isNotNull("email"))
  //                .likeRight("name","黄")
  //
  //        //6.last
  //        //.只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
  //                .last("limit 1");
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //7. 筛选部分列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.select("id","name");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //8. 选出id, name, age, email, 等同于排除 manager_id 和 create_time
  // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列,
  // 可以采用重载的select方法,指定需要排除的列
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.select(User.class,info->{
    String colName = info.getColumn();
    return !colName.equals("manager_id") && !colName.equals("create_time");
  });
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}

7.3 Condition

条件构造器的诸多方法中,均可以指定一个boolean类型的参数condition,用来决定该条件是否加入最后生成的WHERE语句中,比如

/**
  * 测试boolean参数
*/
@Test
public void testBooleanParam(){
String name = "";
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中
// 其实就是对下面代码的简化
//如果字符串里面的值为null, "", "   ",那么返回值为false;否则为true
//        if (StringUtils.hasText(name)) {
//            wrapper.like("name", name);
//        }
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(e-> System.out.println(e));
}

7.4 实体对象作为条件

调用构造函数创建一个Wrapper对象时,可以传入一个实体对象。

后续使用这个Wrapper时,会以实体对象中的非空属性,构建WHERE条件(默认构建等值匹配的WHERE条件,

这个行为可以通过实体类里各个字段上的@TableField注解中的condition属性进行改变)

  1. 示例01 - 会以实体对象中的非空属性,构建WHERE条件

执行结果可以看到,是根据实体对象中的非空属性,进行了等值匹配查询

@Test
public void testObjParams(){
  User user = new User();
  user.setName("小菜");
  QueryWrapper<User> queryWrapper = new QueryWrapper<>(user);
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}
  1. 示例02 - 若希望针对某些属性,改变等值匹配的行为,则可以在实体类中用@TableField注解进行配置,示例如下
@TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择
SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如
@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;  
}

8. lambda条件构造器

lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的方法引用来指定列。示例如下

@Test
public void testLambda(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(User::getName,"三");
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(System.out::println);
}

8.1 补充

  1. 还有个链式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);  
}
  1. 更新操作
@Test
public void testLambdaUpdate(){
  User user = new User();
  user.setName("james");
  LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
  lambdaUpdateWrapper.eq(User::getId,1);
  userMapper.update(user,lambdaUpdateWrapper);
}
  1. 再额外演示一下,链式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();  
}

8.2 反思

由于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);  
 }

9. 自定义SQL

当mp提供的方法还不能满足需求时,则可以自定义SQL。

9.1 原生mybatis

public interface UserMapper extends BaseMapper<User> {
    @Select("select * from user")
    List<User> findAll();
}

9.2 mybatis-plus

也可以使用mp提供的Wrapper条件构造器,来自定义SQL

在UserMapper.java接口添加

9.2.1 注解配置方式

//自定义sql - mybatis-plus方式
@Select("select * from user ${ew.customSqlSegment}")
List<User> findAllWhere(@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

/**
  * 自定义SQL
  * 2. 采用原生的mybatis
  * 也可以使用mp提供的Wrapper条件构造器,来自定义SQL
  */
@Test
public void testFindAllWhere(){
  LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  wrapper.like(User::getName,"三");
  List<User> userList = userMapper.findAllWhere(wrapper);
  userList.forEach(user -> System.out.println(user));
}

9.2.1 xml配置方式

List<User> findAll(Wrapper<User> wrapper);
<mapper namespace="tech.aistar.mapper.UserMapper">  
  <select id="findAll" resultType="tech.aistar.pojo.User">  
    SELECT * FROM user ${ew.customSqlSegment}  
  </select>  
</mapper>

10. 分页查询

BaseMapper中提供了2个方法进行分页查询,分别是

  1. selectPage:将查询的结果封装成Java实体对象
  2. selectMapsPage:将查询的结果封装成封装成Map<String,Object>

10.1 selectPage方式

  1. 创建mp的分页拦截器,注册到Spring容器中

config/

@Configuration
public class MybatisplusConfig {
 @Bean
 public MybatisPlusInterceptor mybatisPlusInterceptor(){
     MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
     PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
     paginationInnerInterceptor.setDbType(DbType.MYSQL);
     paginationInnerInterceptor.setOverflow(true);
     interceptor.addInnerInterceptor(paginationInnerInterceptor);
     return interceptor;
 }
}
@Test
public void testSelectPage(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.ge(User::getAge,20);
//设置分页信息,查询第3页,每页显示2条
Page<User> page = new Page<>(3,2);
//执行分页查询
Page<User> pageInfo = userMapper.selectPage(page,lambdaQueryWrapper);
//展示分页的信息
System.out.println("总的记录数[条数rows]:"+page.getTotal());
System.out.println("总的页数:"+page.getPages());
System.out.println("每页显示条数:"+page.getSize());
//展示分页具体返回的内容
page.getRecords().forEach(System.out::println);
}

控制台sql分析

==>  Preparing: SELECT COUNT(*) FROM user WHERE (age >= ?)
==> Parameters: 20(Integer)
<==    Columns: COUNT(*)
<==        Row: 6
<==      Total: 1
==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2

注意到,分页查询总共发出了2次SQL,一次查总记录数,一次查具体数据。若希望不查总记录数,仅查分页结果。可以通过Page的重载构造函数,指定isSearchCountfalse即可

public Page(long current, long size, boolean isSearchCount);
Page<User> page = new Page<>(3,2,false);

设置成第三个参数为false,之后那么只会出现一条sql,与此同时总的记录数和总的页数都将返回默认值0.

==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@57545c3f]
总的记录数[条数rows]:0
总的页数:0
每页显示条数:2

10.2 多表联查

在实际开发中,可能遇到多表联查的场景,此时BaseMapper中提供的单表分页查询的方法无法满足需求,需要自定义SQL,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,修改SQL语句即可)

/**
  *在实际开发中,可能遇到**多表联查**的场景,此时`BaseMapper`中提供的单表分页查询的方法无法满足需求,
  * 需要**自定义SQL**,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,
  * 修改SQL语句即可)
  *
  * 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式
  * @param page
  * @param wrapper
  * @return
  */
@Select("select * from user ${ew.customSqlSegment}")
Page<User> selectUserPage(Page<User> page,@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

@Test
public void testSelectUserPage(){
  LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
  lambdaQueryWrapper.ge(User::getAge,20);
  //设置分页信息,查询第3页,每页显示2条
  //第三个参数设置成false,不会查询总的记录数
  //Page<User> page = new Page<>(3,2,false);
  Page<User> page = new Page<>(3,2);
  //执行分页查询
  Page<User> pageInfo = userMapper.selectUserPage(page,lambdaQueryWrapper);
  //展示分页的信息
  System.out.println("总的记录数[条数rows]:"+page.getTotal());
  System.out.println("总的页数:"+page.getPages());
  System.out.println("每页显示条数:"+page.getSize());
  //展示分页具体返回的内容
  page.getRecords().forEach(System.out::println);
}

#11. Sequence主键生成策略

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

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

  • AUTO

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

并且插入成功之后,主键会被写回实体对象。

  • NONE

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

  • INPUT

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

  • ASSIGN_ID

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

  • ASSIGN_UUID

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

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

11.1 局部策略和全局策略

可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略

mybatis-plus:
  global-config:
    db-config:
      id-type: auto

注意一旦设置成了auto,需要修改主键为自增长,修改脚本文件-添加auto_increment否则会抛出id does not default value的异常

DROP TABLE IF EXISTS user;  
   CREATE TABLE user (  
     id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');

12. 配置

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

12.1 基本配置

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

12.2 进阶配置

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

12.2.1 updateStrategy

有如下几种可选配置这个配置项,可在application.yml中进行全局配置,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置

这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。

而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL

  • IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。
  • NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中
  • NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL
  • NEVER:不加入SQL。所有字段不加入到SQL语句
  • tablePrefix:添加表名前缀
mybatis-plus:  
   global-config:  
     db-config:  
       table-prefix: xx_
@Test  
public void test3() {  
 QueryWrapper<User> wrapper = new QueryWrapper<>();  
 wrapper.like("name", "黄");  
 Integer count = userMapper.selectCount(wrapper);  
 System.out.println(count);  
}

可以看到拼接出来的SQL,在表名前面添加了前缀

13. 高级功能

高级功能的演示需要用到一张新的表user2

  • sql脚本
DROP TABLE IF EXISTS user2;  
CREATE TABLE user2 (  
id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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_user2 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实体类
@Data  
public class User2 {  
private Long id;  
private String name;  
private Integer age;  
private String email;  
private Long managerId;  
private Date createTime;  
private Date updateTime;  
private Integer version;  
private Integer deleted;  
}
  • User2Mapper接口
public interface User2Mapper extends BaseMapper<User2> {
}

13.1. 逻辑删除

首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案

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

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项

测试代码

user2Mapper.deleteById(1);

分析控制台SQL

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

可以看到,发出的SQL不再是DELETE,而是UPDATE

此时我们再执行一次SELECT

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

分析控制台SQL

==>  Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user2 WHERE deleted=0
==> Parameters: 
<==    Columns: id, name, age, email, manager_id, create_time, update_time, version, deleted
<==        Row: 2, 王狗蛋, 40, gd@baomidou.com, 1, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 3, 王鸡蛋, 40, jd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 4, 王鸭蛋, 40, yd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 5, 王猪蛋, 40, zd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 6, 王软蛋, 40, rd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 7, 王铁蛋, 40, td@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==      Total: 6

可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为1的老板

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

@TableField(select = false)
private Integer deleted;

分析控制台SQL

SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE deleted=0

查询的列中已经不包括逻辑删除的那一列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进行局部配置。

13.2 自动填充

表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。

比较原始的方式,是每次插入或更新时,手动进行设置。

mp可以通过配置,对某些字段进行自动填充,实用实例如下

原理

  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
  • 注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置

在实体类中的某些字段上,通过@TableField设置自动填充

@Data
public class User2 implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    //自动填充
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.UPDATE)
    private Date updateTime;
    private Integer version;
    //逻辑删除 - '逻辑删除标识,0-未删除,1-已删除'
    @TableField(select = false)
//    @TableLogic(delval = "1",value = "0")
    private Integer deleted;
}

实现自动填充处理器

package tech.aistar.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Date;
import java.util.function.Supplier;
/**
 * 本类用来演示: 实现自动填充处理器
 *
 * @author: success
 * @date: 2021/6/3 9:59 上午
 */
@Component //需要注册到Spring容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
//        this.strictFillStrategy(metaObject, "createTime", new Supplier<Object>() {
//            @Override
//            public Object get() {
//                return new Date();
//            }
//        });
        this.strictFillStrategy(metaObject, "createTime",Date::new);
    }
    @Override
    public void updateFill(MetaObject metaObject) {
         this.strictFillStrategy(metaObject,"updateTime",Date::new);
    }
}

单元测试-insert

@SpringBootTest
public class TestMyMetaObjectHandler {
    @Autowired
    private User2Mapper user2Mapper;
    /**
     * 测试自动填充 - createTime字段
     */
    @Test
    public void testInsert(){
        User2 user = new User2();
        user.setName("强老师");
        user.setAge(29);
        user.setEmail("yt@baomidou.com");
        user.setManagerId(2L);
        user2Mapper.insert(user);
    }
}

可以看到对createTime进行了自动填充,注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值.

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

@Test
public void testUpdate(){
  User2 user2 = new User2();
  user2.setId(8L);
  user2.setName("平老师");
  user2Mapper.updateById(user2);
}

14. 乐观锁插件

当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。

在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。

在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。

实现原理

乐观锁的实现如下:

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

这种思想和CAS(Compare And Swap)非常相似。

实现步骤

  1. 配置乐观锁插件
/**
     * 乐观锁插件 - 新版
  */
@Bean
public MybatisPlusInterceptor oPtimisticLockerInterceptor() {
  MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
  mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
  return mybatisPlusInterceptor;
}
  1. 在实体类中表示版本的字段上添加注解@Version
//乐观锁- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
@Version
private Integer version;
  1. 测试代码
@Test
public void testLock(){
  // 假设这个version是先前查询时获得的
  int version=1;
  User2 user2 = new User2();
  user2.setId(1L);
  user2.setName("管管");
  user2.setVersion(version);
  user2Mapper.updateById(user2);
}

SQL打印如下,可以发现version从一开始的值1变成2

==>  Preparing: UPDATE user2 SET name=?, update_time=?, version=? WHERE id=? AND version=?
==> Parameters: 管管(String), 2021-06-08 09:10:44.678(Timestamp), 2(Integer), 1(Long), 1(Integer)
<==    Updates: 1

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

注意点

注意,乐观锁插件仅支持updateById(id)update(entity, 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,是有问题的。

15. 性能分析插件

该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。

注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐使第三方性能分析插件

该插件有性能损耗,不建议生产环境使用

实现步骤

  1. 引入第三方依赖
<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
    url: jdbc:p6spy:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    # url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
#    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  1. src/main/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
  1. 随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来
Consume Time:8 ms 2021-06-08 09:30:35
 Execute SQL:select * from user

16. 多租户SQL解析器

多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。

此处内容作为课外了解 - 想要进一步学习,可参考 - https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/105002089

17. 防止全表更新和删除

在实际项目开发中,可能由于不小心或者有人恶意将整个表格的数据修改、或删除。这对项目/产品的影响是非常大的,可能会导致项目/产品失败。

MyBatis Plus 提供了 BlockAttackInnerInterceptor 插件,该插件可以阻止全表更新和删除操作。在一定程度上,保证了数据库数据的安全。下面将通过示例介绍怎样使用该插件:

实现步骤

  1. 添加配置
@Bean
public MybatisPlusInterceptor blockAttackInterceptor() {
  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  // 防止全表更新与删除
  // 针对 update 和 delete 语句
  interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
  return interceptor;
}
  1. 测试
@Test
public void testUpdateAll(){
  User2 user2 = new User2();
  user2.setAge(100);
  //更新全表
  user2Mapper.update(user2,null);
}

控制台报错

### Error updating database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation

上面的错误信息中,“Prohibition of table update operation” 禁止全表更新操作,这样就能阻止全表更新和删除操作

18.Mybatis-plus逆向工程

官网指南 - https://baomidou.com/guide/

这个具体的代码实现不在此工程中,如果想要更加清晰的知道工程的结构,

请移步到https://gitee.com/guancg/success-yeb

具体步骤

  1. 依赖
<?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>tech.aistar</groupId>
        <artifactId>yeb-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>yeb-generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>yeb-generator</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--freemarker-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 配置类
package tech.aistar.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
    /**
        * <p>
        * 读取控制台内容
        * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }
    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/yeb-generator/src/main/java");
        gc.setAuthor("亲爱的小管");
        // 打开输出目录
        gc.setOpen(false);
        // xml开启BaseResultMap
        gc.setBaseResultMap(true);
        // xml开启BaseColumnList
        gc.setBaseColumnList(true);
        //实体属性 Swagger2 注解
        gc.setSwagger2(true);
        mpg.setGlobalConfig(gc);
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);
        // 包配置
        PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("模块名"));
        pc.setParent("tech.aistar")
                .setEntity("pojo")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);
        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };
        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";
        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/yeb-generator/src/main/resources/mapper/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);
        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        // 数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.no_change);
//        strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        // lombok
        strategy.setEntityLombokModel(true);
        // 生产restController
        strategy.setRestControllerStyle(true);
        // 公共父类
//        strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
//        strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

目录

目录1. 人生苦短2. Mybatis-Plus简介3. 适合人群5. 核心功能6. CRUD接口6.1 Mapper CRUD接口6.1.1 Insert方法6.1.2 Delete方法6.1.3 Update方法6.1.4 Select方法6.2 Service接口6.2.1 使用方略示例6.2.2 Save方法6.2.3 SaveOrUpdate6.2.4 Remove方法6.2.5 Update6.2.6 Get6.2.7 List6.2.8 Page6.2.9 Count6.3.0 Chain7. 条件构造器7.1 常用方法7.2 常用方法示例7.3 Condition7.4 实体对象作为条件8. lambda条件构造器8.1 补充8.2 反思9. 自定义SQL9.1 原生mybatis9.2 mybatis-plus9.2.1 注解配置方式9.2.1 xml配置方式10. 分页查询10.1 selectPage方式10.2 多表联查11.1 局部策略和全局策略12. 配置12.1 基本配置12.2 进阶配置12.2.1 updateStrategy13. 高级功能13.1. 逻辑删除13.2 自动填充14. 乐观锁插件实现原理实现步骤注意点15. 性能分析插件实现步骤16. 多租户SQL解析器17. 防止全表更新和删除实现步骤18.Mybatis-plus逆向工程具体步骤

1. 人生苦短

2. Mybatis-Plus简介

  1. MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
  2. 官网指南 - https://baomidou.com/guide/

3. 适合人群

  1. 拥有 Java 开发环境以及相应 IDE
  2. 熟悉 Spring Boot
  3. 熟悉 Maven
  4. 熟悉mybatis框架

#4. 快速开始

  1. 创建SpringBoot工程
  2. 导入依赖
<?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.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>mybatis-plus-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybatis-plus-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');
  1. 实体类
package tech.aistar.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
    * 本类用来演示: 用户实体类
 *
    * @author: success
    * @date: 2021/5/31 9:51 上午
 */
@Data
public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    private Date createTime;
}
  1. application.yml文件配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  1. 主程序类
package tech.aistar;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("tech.aistar.mapper")
@SpringBootApplication
public class MybatisPlusDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusDemoApplication.class, args);
    }
}
  1. 单元测试
package tech.aistar.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
    * 本类用来演示:
 *
    * @author: success
    * @date: 2021/5/31 11:14 上午
 */
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;
    /**
        * 测试 - 查询所有
     */
    @Test
    public void testFindAll(){
        userMapper.selectList(null).forEach(System.out::println);
    }
}

总结:可以看到,针对单表的基本CRUD操作,只需要创建好实体类,并创建一个继承自BaseMapper的接口即可,可谓非常简洁。并且,我们注意到,User类中的managerId,createTime属性,自动和数据库表中的manager_id,create_time对应了起来,这是因为mp自动做了数据库下划线命名,到Java类的驼峰命名之间的转化。

5. 核心功能

注解

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

  1. @TableName注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。
  2. @TableId注解在实体类的某一字段上,表示这个字段对应数据库表的主键。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略。
  3. @TableField注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。
  • 排除非表字段
  1. 若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist属性为false,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用statictransient关键字,但个人觉得不是很合理,不做赘述
  • 字段验证策略
  1. 通过insertStrategyupdateStrategywhereStrategy属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。
  • 字段填充策略
  1. 通过fill属性指定,字段为空时会进行自动填充
  2. @Version乐观锁注解
  3. @EnumValue注解在枚举字段上
  4. @TableLogic逻辑删除
  5. @KeySequence序列主键策略(oracle
  6. @InterceptorIgnore插件过滤规则

6. CRUD接口

  1. mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。
  2. mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。
  3. 并且mp还提供了条件构造器Wrapper,可以方便地组装SQL语句中的WHERE条件

6.1 Mapper CRUD接口

Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器

  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器

学习目标 - BaseMapper里提供的方法

6.1.1 Insert方法

// 插入一条记录
int insert(T entity);

参数说明

类型

参数名

描述

T

entity

实体对象

测试代码

/**
  * 测试Insert方法
  */
@Test
public void testInsert(){
  User user = new User();
  user.setName("success");
  user.setAge(18);
  user.setEmail("849962874@qq.com");
  user.setCreateTime(new Date());
  user.setManagerId(2L);
  userMapper.insert(user);
}

6.1.2 Delete方法

// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

参数说明

类型

参数名

描述

Wrapper<T>

wrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

测试代码

@Test
public void testDelete(){
  //根据 ID 删除
  //userMapper.deleteById(1399249825152729090L);
  //删除(根据ID 批量删除)
  //        List<Long> ids = new ArrayList<>();
  //        ids.add(5L);
  //        ids.add(3L);
  //        userMapper.deleteBatchIds(ids);
  // 根据 entity 条件,删除记录
  //DELETE FROM user WHERE (manager_id = ?)
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.eq("manager_id",2L);
  //        userMapper.delete(queryWrapper);
  //根据 columnMap 条件,删除记录
  //DELETE FROM user WHERE name = ? 
  Map<String,Object> maps = new HashMap<>();
  maps.put("name","吴组长");
  userMapper.deleteByMap(maps);
}

6.1.3 Update方法

// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

参数说明

类型

参数名

描述

T

entity

实体对象 (set 条件值,可为 null)

Wrapper<T>

updateWrapper

实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

测试代码

@Test
public void testUpdate(){
  //        User user = userMapper.selectById(1L);
  //        user.setName("success");
  //        userMapper.updateById(user);
  //第一种写法 - 使用set()方法单独更新某个列
  //        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  //        updateWrapper.ge("age",40).set("age",100);
  //
  //        userMapper.update(null,updateWrapper);
  //第二种写法 - 使用对象
  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",23).le("age",30);
  User user = new User();
  user.setAge(18);
  userMapper.update(user,updateWrapper);
}

6.1.4 Select方法

// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类(可以为 null)

Collection<? extends Serializable>

idList

主键ID列表(不能为 null 以及 empty)

Map<String, Object>

columnMap

表字段 map 对象

IPage<T>

page

分页查询条件(可以为 RowBounds.DEFAULT)

部分测试代码

select Maps

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

该方法的使用场景如下:

  1. 只查部分列

当某个表的列特别多,而SELECT的时候只需要选取个别列,查询出的结果也没必要封装成Java实体类对象时(只查部分列时,封装成实体后,实体对象中的很多属性会是null),则可以用selectMaps,获取到指定的列后,再自行进行处理即可

测试代码

//SELECT id,name FROM user WHERE (name LIKE ?)
//==>  Preparing: SELECT id,name FROM user WHERE (name LIKE ?)
//==> Parameters: 黄%(String)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("id","name");
queryWrapper.likeRight("name","黄");
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);

进行数据统计

测试代码

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("manager_id","avg(age)")
  .groupBy("manager_id")
  .having("avg(age)>={0}",59);
List<Map<String,Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
  1. selectObjs

只会返回第一个字段(第一列)的值,其他字段会被舍弃 - 得到的结果,只封装了第一列的id

QueryWrapper<User> wrapper = new QueryWrapper<>();  
wrapper.select("id", "name").like("name", "黄");  
List<Object> objects = userMapper.selectObjs(wrapper);  
objects.forEach(System.out::println);
  1. selectCount

查询满足条件的总数,注意,使用这个方法,不能调用QueryWrapperselect方法设置要查询的列了。这个方法会自动添加select count(1)

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("age",18);
Integer size = userMapper.selectCount(queryWrapper);
System.out.println(size);

6.2 Service接口

说明:

  • 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行remove 删除list 查询集合page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper 为 条件构造器

6.2.1 使用方略

另外一套CRUD是Service层的,只需要编写一个接口,继承IService,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,比较明显的区别在于IService支持了更多的批量化操作,如saveBatchsaveOrUpdateBatch等方法。

示例

  1. 首先,新建一个接口,继承IService
public interface UserService extends IService<User2> {
}
  1. 创建这个接口的实现类,并继承ServiceImpl,最后打上@Service注解,注册到Spring容器中,即可使用
@Service
public class UserServiceImpl extends ServiceImpl<User2Mapper, User2> implements UserService{
}

6.2.2 Save方法

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)- batchSize - 表示一次批量插入的数据量,默认为 1000
boolean saveBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

单元测试

@Test
public void testSave(){
  List<User2> list = new ArrayList<>();
  for (int i = 0; i < 5; i++) {
    User2 user2 = new User2();
    user2.setAge(22+i);
    user2.setEmail("99999"+i+"@qq.com");
    user2.setName("tom"+i);
    user2.setManagerId(2L);
    list.add(user2);
  }
  //批量插入
  userService.saveBatch(list);
}

6.2.3 SaveOrUpdate

// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

T

entity

实体对象

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

Collection<T>

entityList

实体对象集合

int

batchSize

插入批次数量

测试01-根据主键值id进行更新操作

/**
  * 测试根据主键值id进行更新
  * 如果实体类的id属性名和表的列主键值id列名保持一致,那么可以不使用@TableId
  * 否则必须使用
  */
@Test
public void testUpdateByPrimaryKey(){
  //        User2 user2 = userService.getById(1);
  //        user2.setAge(33);
  //        //测试对已经存在的记录进行更新
  //        userService.saveOrUpdate(user2);
  User2 user01 = new User2();
  user01.setName("亲爱的管");
  user01.setAge(100);
  //测试对不存在的记录进行插入
  userService.saveOrUpdate(user01);
}

测试02-根据Wrapper条件进行更新

/**
   * 测试根据条件进行更新
   * 如果存在,则更新,否则执行插入
   */
@Test
public void testUpdateByWrapper(){
  UpdateWrapper<User2> updateWrapper = new UpdateWrapper<>();
  updateWrapper.ge("age",300);
  User2 u = new User2();
  u.setName("ping");
  userService.saveOrUpdate(u,updateWrapper);
}

6.2.4 Remove方法

// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体包装类 QueryWrapper

Serializable

id

主键ID

Map<String, Object>

columnMap

表字段 map 对象

Collection<? extends Serializable>

idList

主键ID列表

测试代码

@Test
public void testRemove(){
  //本人更加推荐lambda写法
  userService.lambdaUpdate().eq(User2::getId,3).remove();
}

6.2.5 Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

参数说明

类型

参数名

描述

Wrapper<T>

updateWrapper

实体对象封装操作类 UpdateWrapper

T

entity

实体对象

Collection<T>

entityList

实体对象集合

int

batchSize

更新批次数量

6.2.6 Get

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
//getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Serializable

id

主键ID

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

boolean

throwEx

有多个 result 是否抛出异常

T

entity

实体对象

Function<? super Object, V>

mapper

转换函数

测试

@Test
public void testGet(){
  //        LambdaQueryWrapper<User2> queryWrapper = new LambdaQueryWrapper<>();
  //        queryWrapper.eq(User2::getAge,40);
  //本人更喜欢如下方式
  //这个是方法返回结果不止一条则会抛出异常,如果想默认取第一条结果,可以给这方法传第二个参数为false。
  //User2 user2 = userService.getOne(Wrappers.<User2>lambdaQuery().eq(User2::getAge,40),false);
  //System.out.println(user2);
  //getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值)
   Integer id = userService.getObj(Wrappers.<User2>lambdaQuery().eq(User2::getId,1),o->{return    Integer.parseInt(o.toString());});
        System.out.println(id);
}

6.2.7 List

// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

Collection<? extends Serializable>

idList

主键ID列表

Map<?String, Object>

columnMap

表字段 map 对象

Function<? super Object, V>

mapper

转换函数

测试代码

@Test
public void testList(){
  userService.listObjs(o->{
    return "user_"+o;
  }).forEach(e-> System.out.println(e));
}

6.2.8 Page

// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

IPage<T>

page

翻页对象

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testPage(){
  Page<User2> page = new Page<>(3,2);
  //        userService.page(page).getRecords().forEach(e-> System.out.println(e));
  userService.page(page,Wrappers.<User2>lambdaQuery().eq(User2::getAge,40))
    .getRecords().forEach(e-> System.out.println(e));
}

6.2.9 Count

// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

参数说明

类型

参数名

描述

Wrapper<T>

queryWrapper

实体对象封装操作类 QueryWrapper

测试

@Test
public void testCount(){
  System.out.println(userService.count(Wrappers.<User2>lambdaQuery().eq(User2::getAge, 40)));
}

6.3.0 Chain

  1. query
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery(); 
// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();

测试

// Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (age = ? OR (id = ?))
userService.lambdaQuery()
  .eq(User2::getAge,40)
  .or(e1->e1.eq(User2::getId,30))
  .list().forEach(e-> System.out.println(e));
SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE (id = ?)
System.out.println(userService.lambdaQuery().eq(User2::getId, 1).one());
  1. update
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin 
LambdaUpdateChainWrapper<T> lambdaUpdate();
// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

测试

@Test
public void testUpdate(){
  //直接更新具体的某列,不需要实体对象
  //userService.lambdaUpdate().eq(User2::getId,1).set(User2::getName,"ss").update();
  //更新一个对象
  User2 user2 = new User2();
  user2.setName("xx");
  user2.setAge(100);
  userService.lambdaUpdate().eq(User2::getId,1).update(user2);
}

7. 条件构造器

mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器主要涉及到3个类,AbstractWrapperQueryWrapperUpdateWrapper,它们的类关系如下:

AbstractWrapper中提供了非常多的方法用于构建WHERE条件,而QueryWrapper针对SELECT语句,提供了select()方法,可自定义需要查询的列,而UpdateWrapper针对UPDATE语句,提供了set()方法,用于构造set语句。条件构造器也支持lambda表达式,写起来非常舒爽。

7.1 常用方法

查询方式

说明

setSqlSelect

设置 SELECT 查询字段

where

WHERE 语句,拼接 +?WHERE 条件

and

AND 语句,拼接 +?AND 字段=值

andNew

AND 语句,拼接 +?AND (字段=值)

or

OR 语句,拼接 +?OR 字段=值

orNew

OR 语句,拼接 +?OR (字段=值)

eq

等于=

allEq

基于 map 内容等于=

ne

不等于<>

gt

大于>

ge

大于等于>=

lt

小于<

le

小于等于<=

like

模糊查询 LIKE

notLike

模糊查询 NOT LIKE

in

IN 查询

notIn

NOT IN 查询

isNull

NULL 值查询

isNotNull

IS NOT NULL

groupBy

分组 GROUP BY

having

HAVING 关键词

orderBy

排序 ORDER BY

orderAsc

ASC 排序 ORDER BY

orderDesc

DESC 排序 ORDER BY

exists

EXISTS 条件语句

notExists

NOT EXISTS 条件语句

between

BETWEEN 条件语句

notBetween

NOT BETWEEN 条件语句

addFilter

自由拼接 SQL

last

拼接在最后,例如:last(“LIMIT 1”)

下面对AbstractWrapper中用于构建SQL语句中的WHERE条件的方法进行部分列举:

  1. allEq - 全部eq
@Test
public void test条件构造器(){
  //SELECT id,name,age,email,manager_id,create_time FROM user WHERE (name = ? AND age = ?)
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> conditional = new HashMap<>();
  conditional.put("age",18);
  conditional.put("name","小菜");
  queryWrapper.allEq(conditional);
  List<User> users = userMapper.selectList(queryWrapper);
  users.forEach(e-> System.out.println(e));
}

细节01-当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);  
}

细节02 - 若想忽略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);  
}

细节03 - 忽略某个key的查询 - SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age = ?)

查询语句中只会出现age列,不会出现name列

QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 18);
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);

7.2 常用方法示例

@Test
public void test条件构造器02(){
  //1. 名字中包含三,且年龄小于25
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.like("name","三")
  //                    .lt("age",25);
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //2.姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.likeRight("name","黄").and(
  //                (w->w.gt("age",20)
  //                        .lt("age",40).isNotNull("email"))
  //        );
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //3.或者年龄大于等于23,按照年龄降序排列,年龄相同则按照id升序排列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.ge("age",23)
  //                    .orderByDesc("age")
  //                    .orderByAsc("id");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //4.创建日期为2021年2月22日,并且直属上级的名字为小姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //        queryWrapper.apply("date_format(create_time,'%Y-%m-%d')={0}","2021-02-22")
  //                     .inSql("manager_id","select id from user where name like '小%'");
  //
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //5. 正常嵌套
  //(年龄小于40或者邮箱不为空) 并且名字为黄姓
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.nested(c->c.lt("age",40).or().isNotNull("email"))
  //                .likeRight("name","黄")
  //
  //        //6.last
  //        //.只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
  //                .last("limit 1");
  //
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //7. 筛选部分列
  //        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //
  //        queryWrapper.select("id","name");
  //        List<User> userList = userMapper.selectList(queryWrapper);
  //        userList.forEach(System.out::println);
  //8. 选出id, name, age, email, 等同于排除 manager_id 和 create_time
  // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列,
  // 可以采用重载的select方法,指定需要排除的列
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.select(User.class,info->{
    String colName = info.getColumn();
    return !colName.equals("manager_id") && !colName.equals("create_time");
  });
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}

7.3 Condition

条件构造器的诸多方法中,均可以指定一个boolean类型的参数condition,用来决定该条件是否加入最后生成的WHERE语句中,比如

/**
  * 测试boolean参数
*/
@Test
public void testBooleanParam(){
String name = "";
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中
// 其实就是对下面代码的简化
//如果字符串里面的值为null, "", "   ",那么返回值为false;否则为true
//        if (StringUtils.hasText(name)) {
//            wrapper.like("name", name);
//        }
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(e-> System.out.println(e));
}

7.4 实体对象作为条件

调用构造函数创建一个Wrapper对象时,可以传入一个实体对象。

后续使用这个Wrapper时,会以实体对象中的非空属性,构建WHERE条件(默认构建等值匹配的WHERE条件,

这个行为可以通过实体类里各个字段上的@TableField注解中的condition属性进行改变)

  1. 示例01 - 会以实体对象中的非空属性,构建WHERE条件

执行结果可以看到,是根据实体对象中的非空属性,进行了等值匹配查询

@Test
public void testObjParams(){
  User user = new User();
  user.setName("小菜");
  QueryWrapper<User> queryWrapper = new QueryWrapper<>(user);
  List<User> userList = userMapper.selectList(queryWrapper);
  userList.forEach(System.out::println);
}
  1. 示例02 - 若希望针对某些属性,改变等值匹配的行为,则可以在实体类中用@TableField注解进行配置,示例如下
@TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择
SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如
@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;  
}

8. lambda条件构造器

lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的方法引用来指定列。示例如下

@Test
public void testLambda(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(User::getName,"三");
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(System.out::println);
}

8.1 补充

  1. 还有个链式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);  
}
  1. 更新操作
@Test
public void testLambdaUpdate(){
  User user = new User();
  user.setName("james");
  LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
  lambdaUpdateWrapper.eq(User::getId,1);
  userMapper.update(user,lambdaUpdateWrapper);
}
  1. 再额外演示一下,链式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();  
}

8.2 反思

由于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);  
 }

9. 自定义SQL

当mp提供的方法还不能满足需求时,则可以自定义SQL。

9.1 原生mybatis

public interface UserMapper extends BaseMapper<User> {
    @Select("select * from user")
    List<User> findAll();
}

9.2 mybatis-plus

也可以使用mp提供的Wrapper条件构造器,来自定义SQL

在UserMapper.java接口添加

9.2.1 注解配置方式

//自定义sql - mybatis-plus方式
@Select("select * from user ${ew.customSqlSegment}")
List<User> findAllWhere(@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

/**
  * 自定义SQL
  * 2. 采用原生的mybatis
  * 也可以使用mp提供的Wrapper条件构造器,来自定义SQL
  */
@Test
public void testFindAllWhere(){
  LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  wrapper.like(User::getName,"三");
  List<User> userList = userMapper.findAllWhere(wrapper);
  userList.forEach(user -> System.out.println(user));
}

9.2.1 xml配置方式

List<User> findAll(Wrapper<User> wrapper);
<mapper namespace="tech.aistar.mapper.UserMapper">  
  <select id="findAll" resultType="tech.aistar.pojo.User">  
    SELECT * FROM user ${ew.customSqlSegment}  
  </select>  
</mapper>

10. 分页查询

BaseMapper中提供了2个方法进行分页查询,分别是

  1. selectPage:将查询的结果封装成Java实体对象
  2. selectMapsPage:将查询的结果封装成封装成Map<String,Object>

10.1 selectPage方式

  1. 创建mp的分页拦截器,注册到Spring容器中

config/

@Configuration
public class MybatisplusConfig {
 @Bean
 public MybatisPlusInterceptor mybatisPlusInterceptor(){
     MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
     PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
     paginationInnerInterceptor.setDbType(DbType.MYSQL);
     paginationInnerInterceptor.setOverflow(true);
     interceptor.addInnerInterceptor(paginationInnerInterceptor);
     return interceptor;
 }
}
@Test
public void testSelectPage(){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.ge(User::getAge,20);
//设置分页信息,查询第3页,每页显示2条
Page<User> page = new Page<>(3,2);
//执行分页查询
Page<User> pageInfo = userMapper.selectPage(page,lambdaQueryWrapper);
//展示分页的信息
System.out.println("总的记录数[条数rows]:"+page.getTotal());
System.out.println("总的页数:"+page.getPages());
System.out.println("每页显示条数:"+page.getSize());
//展示分页具体返回的内容
page.getRecords().forEach(System.out::println);
}

控制台sql分析

==>  Preparing: SELECT COUNT(*) FROM user WHERE (age >= ?)
==> Parameters: 20(Integer)
<==    Columns: COUNT(*)
<==        Row: 6
<==      Total: 1
==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2

注意到,分页查询总共发出了2次SQL,一次查总记录数,一次查具体数据。若希望不查总记录数,仅查分页结果。可以通过Page的重载构造函数,指定isSearchCountfalse即可

public Page(long current, long size, boolean isSearchCount);
Page<User> page = new Page<>(3,2,false);

设置成第三个参数为false,之后那么只会出现一条sql,与此同时总的记录数和总的页数都将返回默认值0.

==>  Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE (age >= ?) LIMIT ? OFFSET ?
==> Parameters: 20(Integer), 2(Long), 4(Long)
<==    Columns: id, name, age, email, manager_id, create_time
<==        Row: 5, 小菜, 23, boss5@baomidou.com, 2, 2021-02-22 09:48:00
<==        Row: 6, 黄主任, 23, null, 5, 2021-02-22 09:48:00
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@57545c3f]
总的记录数[条数rows]:0
总的页数:0
每页显示条数:2

10.2 多表联查

在实际开发中,可能遇到多表联查的场景,此时BaseMapper中提供的单表分页查询的方法无法满足需求,需要自定义SQL,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,修改SQL语句即可)

/**
  *在实际开发中,可能遇到**多表联查**的场景,此时`BaseMapper`中提供的单表分页查询的方法无法满足需求,
  * 需要**自定义SQL**,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,
  * 修改SQL语句即可)
  *
  * 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式
  * @param page
  * @param wrapper
  * @return
  */
@Select("select * from user ${ew.customSqlSegment}")
Page<User> selectUserPage(Page<User> page,@Param(Constants.WRAPPER)Wrapper<User> wrapper);

单元测试

@Test
public void testSelectUserPage(){
  LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
  lambdaQueryWrapper.ge(User::getAge,20);
  //设置分页信息,查询第3页,每页显示2条
  //第三个参数设置成false,不会查询总的记录数
  //Page<User> page = new Page<>(3,2,false);
  Page<User> page = new Page<>(3,2);
  //执行分页查询
  Page<User> pageInfo = userMapper.selectUserPage(page,lambdaQueryWrapper);
  //展示分页的信息
  System.out.println("总的记录数[条数rows]:"+page.getTotal());
  System.out.println("总的页数:"+page.getPages());
  System.out.println("每页显示条数:"+page.getSize());
  //展示分页具体返回的内容
  page.getRecords().forEach(System.out::println);
}

#11. Sequence主键生成策略

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

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

  • AUTO

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

并且插入成功之后,主键会被写回实体对象。

  • NONE

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

  • INPUT

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

  • ASSIGN_ID

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

  • ASSIGN_UUID

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

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

11.1 局部策略和全局策略

可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略

mybatis-plus:
  global-config:
    db-config:
      id-type: auto

注意一旦设置成了auto,需要修改主键为自增长,修改脚本文件-添加auto_increment否则会抛出id does not default value的异常

DROP TABLE IF EXISTS user;  
   CREATE TABLE user (  
     id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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, 'boss1@baomidou.com', NULL, '2021-03-22 09:48:00'),  
   (2, '李三儿', 30, 'boss2@baomidou.com', 1, '2021-01-22 09:48:00'),  
   (3, '黄小三', 35, 'boss3@baomidou.com', 2, '2021-01-22 09:48:00'),  
   (4, '三德子', 25, 'boss4@baomidou.com', 2, '2021-02-22 09:48:00'),  
   (5, '小菜', 23, 'boss5@baomidou.com', 2, '2021-02-22 09:48:00'),
   (6, '黄主任',23,NULL,5, '2021-02-22 09:48:00');

12. 配置

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

12.1 基本配置

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

12.2 进阶配置

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

12.2.1 updateStrategy

有如下几种可选配置这个配置项,可在application.yml中进行全局配置,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置

这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。

而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL

  • IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。
  • NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中
  • NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL
  • NEVER:不加入SQL。所有字段不加入到SQL语句
  • tablePrefix:添加表名前缀
mybatis-plus:  
   global-config:  
     db-config:  
       table-prefix: xx_
@Test  
public void test3() {  
 QueryWrapper<User> wrapper = new QueryWrapper<>();  
 wrapper.like("name", "黄");  
 Integer count = userMapper.selectCount(wrapper);  
 System.out.println(count);  
}

可以看到拼接出来的SQL,在表名前面添加了前缀

13. 高级功能

高级功能的演示需要用到一张新的表user2

  • sql脚本
DROP TABLE IF EXISTS user2;  
CREATE TABLE user2 (  
id BIGINT(20) PRIMARY KEY NOT NULL auto_increment 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_user2 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实体类
@Data  
public class User2 {  
private Long id;  
private String name;  
private Integer age;  
private String email;  
private Long managerId;  
private Date createTime;  
private Date updateTime;  
private Integer version;  
private Integer deleted;  
}
  • User2Mapper接口
public interface User2Mapper extends BaseMapper<User2> {
}

13.1. 逻辑删除

首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案

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

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项

测试代码

user2Mapper.deleteById(1);

分析控制台SQL

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

可以看到,发出的SQL不再是DELETE,而是UPDATE

此时我们再执行一次SELECT

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

分析控制台SQL

==>  Preparing: SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user2 WHERE deleted=0
==> Parameters: 
<==    Columns: id, name, age, email, manager_id, create_time, update_time, version, deleted
<==        Row: 2, 王狗蛋, 40, gd@baomidou.com, 1, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 3, 王鸡蛋, 40, jd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 4, 王鸭蛋, 40, yd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 5, 王猪蛋, 40, zd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 6, 王软蛋, 40, rd@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==        Row: 7, 王铁蛋, 40, td@baomidou.com, 2, 2021-03-28 13:12:40, null, 1, 0
<==      Total: 6

可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为1的老板

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

@TableField(select = false)
private Integer deleted;

分析控制台SQL

SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE deleted=0

查询的列中已经不包括逻辑删除的那一列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进行局部配置。

13.2 自动填充

表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。

比较原始的方式,是每次插入或更新时,手动进行设置。

mp可以通过配置,对某些字段进行自动填充,实用实例如下

原理

  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
  • 注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置

在实体类中的某些字段上,通过@TableField设置自动填充

@Data
public class User2 implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long managerId;
    //自动填充
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    @TableField(fill = FieldFill.UPDATE)
    private Date updateTime;
    private Integer version;
    //逻辑删除 - '逻辑删除标识,0-未删除,1-已删除'
    @TableField(select = false)
//    @TableLogic(delval = "1",value = "0")
    private Integer deleted;
}

实现自动填充处理器

package tech.aistar.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Date;
import java.util.function.Supplier;
/**
 * 本类用来演示: 实现自动填充处理器
 *
 * @author: success
 * @date: 2021/6/3 9:59 上午
 */
@Component //需要注册到Spring容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
//        this.strictFillStrategy(metaObject, "createTime", new Supplier<Object>() {
//            @Override
//            public Object get() {
//                return new Date();
//            }
//        });
        this.strictFillStrategy(metaObject, "createTime",Date::new);
    }
    @Override
    public void updateFill(MetaObject metaObject) {
         this.strictFillStrategy(metaObject,"updateTime",Date::new);
    }
}

单元测试-insert

@SpringBootTest
public class TestMyMetaObjectHandler {
    @Autowired
    private User2Mapper user2Mapper;
    /**
     * 测试自动填充 - createTime字段
     */
    @Test
    public void testInsert(){
        User2 user = new User2();
        user.setName("强老师");
        user.setAge(29);
        user.setEmail("yt@baomidou.com");
        user.setManagerId(2L);
        user2Mapper.insert(user);
    }
}

可以看到对createTime进行了自动填充,注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值.

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

@Test
public void testUpdate(){
  User2 user2 = new User2();
  user2.setId(8L);
  user2.setName("平老师");
  user2Mapper.updateById(user2);
}

14. 乐观锁插件

当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。

在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。

在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。

实现原理

乐观锁的实现如下:

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

这种思想和CAS(Compare And Swap)非常相似。

实现步骤

  1. 配置乐观锁插件
/**
     * 乐观锁插件 - 新版
  */
@Bean
public MybatisPlusInterceptor oPtimisticLockerInterceptor() {
  MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
  mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
  return mybatisPlusInterceptor;
}
  1. 在实体类中表示版本的字段上添加注解@Version
//乐观锁- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
@Version
private Integer version;
  1. 测试代码
@Test
public void testLock(){
  // 假设这个version是先前查询时获得的
  int version=1;
  User2 user2 = new User2();
  user2.setId(1L);
  user2.setName("管管");
  user2.setVersion(version);
  user2Mapper.updateById(user2);
}

SQL打印如下,可以发现version从一开始的值1变成2

==>  Preparing: UPDATE user2 SET name=?, update_time=?, version=? WHERE id=? AND version=?
==> Parameters: 管管(String), 2021-06-08 09:10:44.678(Timestamp), 2(Integer), 1(Long), 1(Integer)
<==    Updates: 1

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

注意点

注意,乐观锁插件仅支持updateById(id)update(entity, 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,是有问题的。

15. 性能分析插件

该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。

注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐使第三方性能分析插件

该插件有性能损耗,不建议生产环境使用

实现步骤

  1. 引入第三方依赖
<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
    url: jdbc:p6spy:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    # url: jdbc:mysql://localhost:3306/dev?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
#    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  1. src/main/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
  1. 随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来
Consume Time:8 ms 2021-06-08 09:30:35
 Execute SQL:select * from user

16. 多租户SQL解析器

多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。

此处内容作为课外了解 - 想要进一步学习,可参考 - https://blog.csdn.net/uxiAD7442KMy1X86DtM3/article/details/105002089

17. 防止全表更新和删除

在实际项目开发中,可能由于不小心或者有人恶意将整个表格的数据修改、或删除。这对项目/产品的影响是非常大的,可能会导致项目/产品失败。

MyBatis Plus 提供了 BlockAttackInnerInterceptor 插件,该插件可以阻止全表更新和删除操作。在一定程度上,保证了数据库数据的安全。下面将通过示例介绍怎样使用该插件:

实现步骤

  1. 添加配置
@Bean
public MybatisPlusInterceptor blockAttackInterceptor() {
  MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  // 防止全表更新与删除
  // 针对 update 和 delete 语句
  interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
  return interceptor;
}
  1. 测试
@Test
public void testUpdateAll(){
  User2 user2 = new User2();
  user2.setAge(100);
  //更新全表
  user2Mapper.update(user2,null);
}

控制台报错

### Error updating database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation

上面的错误信息中,“Prohibition of table update operation” 禁止全表更新操作,这样就能阻止全表更新和删除操作

18.Mybatis-plus逆向工程

官网指南 - https://baomidou.com/guide/

这个具体的代码实现不在此工程中,如果想要更加清晰的知道工程的结构,

请移步到https://gitee.com/guancg/success-yeb

具体步骤

  1. 依赖
<?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>tech.aistar</groupId>
        <artifactId>yeb-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tech.aistar</groupId>
    <artifactId>yeb-generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>yeb-generator</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--freemarker-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 配置类
package tech.aistar.generator;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
    /**
        * <p>
        * 读取控制台内容
        * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }
    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/yeb-generator/src/main/java");
        gc.setAuthor("亲爱的小管");
        // 打开输出目录
        gc.setOpen(false);
        // xml开启BaseResultMap
        gc.setBaseResultMap(true);
        // xml开启BaseColumnList
        gc.setBaseColumnList(true);
        //实体属性 Swagger2 注解
        gc.setSwagger2(true);
        mpg.setGlobalConfig(gc);
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);
        // 包配置
        PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("模块名"));
        pc.setParent("tech.aistar")
                .setEntity("pojo")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);
        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };
        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";
        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/yeb-generator/src/main/resources/mapper/" +  tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);
        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        // 数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.no_change);
//        strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        // lombok
        strategy.setEntityLombokModel(true);
        // 生产restController
        strategy.setRestControllerStyle(true);
        // 公共父类
//        strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
//        strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

目录



目录
相关文章
|
2天前
|
存储 弹性计算 人工智能
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
2025年9月24日,阿里云弹性计算团队多位产品、技术专家及服务器团队技术专家共同在【2025云栖大会】现场带来了《通用计算产品发布与行业实践》的专场论坛,本论坛聚焦弹性计算多款通用算力产品发布。同时,ECS云服务器安全能力、资源售卖模式、计算AI助手等用户体验关键环节也宣布升级,让用云更简单、更智能。海尔三翼鸟云服务负责人刘建锋先生作为特邀嘉宾,莅临现场分享了关于阿里云ECS g9i推动AIoT平台的场景落地实践。
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
|
3天前
|
云安全 数据采集 人工智能
古茗联名引爆全网,阿里云三层防护助力对抗黑产
阿里云三层校验+风险识别,为古茗每一杯奶茶保驾护航!
古茗联名引爆全网,阿里云三层防护助力对抗黑产
|
4天前
|
存储 机器学习/深度学习 人工智能
大模型微调技术:LoRA原理与实践
本文深入解析大语言模型微调中的关键技术——低秩自适应(LoRA)。通过分析全参数微调的计算瓶颈,详细阐述LoRA的数学原理、实现机制和优势特点。文章包含完整的PyTorch实现代码、性能对比实验以及实际应用场景,为开发者提供高效微调大模型的实践指南。
514 1
kde
|
4天前
|
人工智能 关系型数据库 PostgreSQL
n8n Docker 部署手册
n8n是一款开源工作流自动化平台,支持低代码与可编程模式,集成400+服务节点,原生支持AI与API连接,可自托管部署,助力团队构建安全高效的自动化流程。
kde
347 3
|
2天前
|
Linux 虚拟化 iOS开发
VMware Workstation Pro 25H2 for Windows & Linux - 领先的免费桌面虚拟化软件
VMware Workstation Pro 25H2 for Windows & Linux - 领先的免费桌面虚拟化软件
692 4
VMware Workstation Pro 25H2 for Windows & Linux - 领先的免费桌面虚拟化软件
|
2天前
|
JavaScript 开发工具 Android开发
如何在原生 App 中调用 Uniapp 的页面?
如何在原生 App 中调用 Uniapp 的页面?
240 138
|
3天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段四:学术分析 AI 项目 RAG 落地指南:基于 Spring AI 的本地与阿里云知识库实践
本文介绍RAG(检索增强生成)技术,结合Spring AI与本地及云知识库实现学术分析AI应用,利用阿里云Qwen-Plus模型提升回答准确性与可信度。
248 91
AI 超级智能体全栈项目阶段四:学术分析 AI 项目 RAG 落地指南:基于 Spring AI 的本地与阿里云知识库实践