# mybatis-plus-demo **Repository Path**: jayem/mybatis-plus-demo ## Basic Information - **Project Name**: mybatis-plus-demo - **Description**: 适合人群 - 小白就算了吧! 1. 熟练并使用过mybatis框架 2. 熟练并使用过springboot框架 3. 熟练并使用sql语句 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2021-12-28 - **Last Updated**: 2021-12-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 目录 [TOC] # 1. 人生苦短 ![](logo.png) # 2. Mybatis-Plus简介 > 1. [MyBatis-Plus (opens new window)](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis (opens new window)](http://www.mybatis.org/mybatis-3/)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生 > 2. 官网指南 - https://baomidou.com/guide/ # 3. 适合人群 > 1. 拥有 Java 开发环境以及相应 IDE > > 2. 熟悉 Spring Boot > > 3. 熟悉 Maven > 4. 熟悉mybatis框架 #4. 快速开始 > 1. 创建SpringBoot工程 > > 2. 导入依赖 > > ~~~xml > > xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> > 4.0.0 > > org.springframework.boot > spring-boot-starter-parent > 2.5.0 > > > tech.aistar > mybatis-plus-demo > 0.0.1-SNAPSHOT > mybatis-plus-demo > Demo project for Spring Boot > > 1.8 > > > > org.springframework.boot > spring-boot-starter-web > > > > mysql > mysql-connector-java > runtime > > > org.projectlombok > lombok > true > > > org.springframework.boot > spring-boot-starter-test > test > > > com.baomidou > mybatis-plus-boot-starter > 3.4.3 > > > > > > > org.springframework.boot > spring-boot-maven-plugin > > > > org.projectlombok > lombok > > > > > > > > ~~~ > > 3. 表设计 > > ~~~sql > 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'); > ~~~ > > 4. 实体类 > > ~~~java > 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; > } > ~~~ > > 5. application.yml文件配置 > > ~~~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 > ~~~ > > 6. 主程序类 > > ~~~java > 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); > } > } > ~~~ > > 7. 单元测试 > > ~~~java > 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实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。 > > - **排除非表字段** > > 若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置`exist`属性为`false`,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用`static`或`transient`关键字,但个人觉得不是很合理,不做赘述 > > - **字段验证策略** > > 通过`insertStrategy`,`updateStrategy`,`whereStrategy`属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。 > > - **字段填充策略** > > 通过`fill`属性指定,字段为空时会进行自动填充 4. @Version > 乐观锁注解 5. @EnumValue > 注解在枚举字段上 6. @TableLogic > 逻辑删除 7. @KeySequence > 序列主键策略(`oracle`) 8. @InterceptorIgnore > 插件过滤规则 # 6. CRUD接口 > 1. mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。 > > 2. mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。 > > 3. 并且mp还提供了条件构造器`Wrapper`,可以方便地组装SQL语句中的WHERE条件 ## 6.1 Mapper CRUD接口 > - 通用 CRUD 封装[BaseMapper (opens new window)](https://gitee.com/baomidou/mybatis-plus/blob/3.0/mybatis-plus-core/src/main/java/com/baomidou/mybatisplus/core/mapper/BaseMapper.java)接口, > > 为 `Mybatis-Plus` 启动时自动解析实体表关系映射转换为 `Mybatis` 内部对象注入容器 > > - 泛型 `T` 为任意实体对象 > > - 参数 `Serializable` 为任意类型主键 `Mybatis-Plus` ***不推荐使用复合主键约定每一张表都有自己的唯一 `id` 主键*** > > - 对象 `Wrapper` 为 [条件构造器](https://baomidou.com/guide/wrapper.html) > > **学习目标 - `BaseMapper`里提供的方法** ### 6.1.1 Insert方法 ~~~java // 插入一条记录 int insert(T entity); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--: | :----: | :------: | | T | entity | 实体对象 | `测试代码` ~~~java /** * 测试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方法 ~~~java // 根据 entity 条件,删除记录 int delete(@Param(Constants.WRAPPER) Wrapper wrapper); // 删除(根据ID 批量删除) int deleteBatchIds(@Param(Constants.COLLECTION) Collection idList); // 根据 ID 删除 int deleteById(Serializable id); // 根据 columnMap 条件,删除记录 int deleteByMap(@Param(Constants.COLUMN_MAP) Map columnMap); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--------------------------------: | :-------: | :--------------------------------: | | Wrapper | wrapper | 实体对象封装操作类(可以为 null) | | Collection | idList | 主键ID列表(不能为 null 以及 empty) | | Serializable | id | 主键ID | | Map | columnMap | 表字段 map 对象 | `测试代码` ~~~java @Test public void testDelete(){ //根据 ID 删除 //userMapper.deleteById(1399249825152729090L); //删除(根据ID 批量删除) // List ids = new ArrayList<>(); // ids.add(5L); // ids.add(3L); // userMapper.deleteBatchIds(ids); // 根据 entity 条件,删除记录 //DELETE FROM user WHERE (manager_id = ?) // QueryWrapper queryWrapper = new QueryWrapper<>(); // queryWrapper.eq("manager_id",2L); // userMapper.delete(queryWrapper); //根据 columnMap 条件,删除记录 //DELETE FROM user WHERE name = ? Map maps = new HashMap<>(); maps.put("name","吴组长"); userMapper.deleteByMap(maps); } ~~~ ### 6.1.3 Update方法 ~~~java // 根据 whereWrapper 条件,更新记录 int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper whereWrapper); // 根据 ID 修改 int updateById(@Param(Constants.ENTITY) T entity); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--------: | :-----------: | :----------------------------------------------------------: | | T | entity | 实体对象 (set 条件值,可为 null) | | Wrapper | updateWrapper | 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句) | `测试代码` ~~~java @Test public void testUpdate(){ // User user = userMapper.selectById(1L); // user.setName("success"); // userMapper.updateById(user); //第一种写法 - 使用set()方法单独更新某个列 // UpdateWrapper updateWrapper = new UpdateWrapper<>(); // updateWrapper.ge("age",40).set("age",100); // // userMapper.update(null,updateWrapper); //第二种写法 - 使用对象 UpdateWrapper 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方法 ~~~java // 根据 ID 查询 T selectById(Serializable id); // 根据 entity 条件,查询一条记录 T selectOne(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 查询(根据ID 批量查询) List selectBatchIds(@Param(Constants.COLLECTION) Collection idList); // 根据 entity 条件,查询全部记录 List selectList(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 查询(根据 columnMap 条件) List selectByMap(@Param(Constants.COLUMN_MAP) Map columnMap); // 根据 Wrapper 条件,查询全部记录 List> selectMaps(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值 List selectObjs(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 entity 条件,查询全部记录(并翻页) IPage selectPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 Wrapper 条件,查询全部记录(并翻页) IPage> selectMapsPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 Wrapper 条件,查询总记录数 Integer selectCount(@Param(Constants.WRAPPER) Wrapper queryWrapper); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--------------------------------: | :----------: | :--------------------------------------: | | Serializable | id | 主键ID | | Wrapper | queryWrapper | 实体对象封装操作类(可以为 null) | | Collection | idList | 主键ID列表(不能为 null 以及 empty) | | Map | columnMap | 表字段 map 对象 | | IPage | page | 分页查询条件(可以为 RowBounds.DEFAULT) | `部分测试代码` > select Maps > > `BaseMapper`接口还提供了一个`selectMaps`方法,这个方法会将查询结果封装为一个Map,Map的key为结果的列,value为值 > > 该方法的使用场景如下: > > 1. **只查部分列** > > 当某个表的列特别多,而SELECT的时候只需要选取个别列,**查询出的结果也没必要封装成Java实体类对象时**(只查部分列时,封装成实体后,实体对象中的很多属性会是null),则可以用`selectMaps`,获取到指定的列后,再自行进行处理即可 > > `测试代码` > > ~~~java > //SELECT id,name FROM user WHERE (name LIKE ?) > //==> Preparing: SELECT id,name FROM user WHERE (name LIKE ?) > //==> Parameters: 黄%(String) > QueryWrapper queryWrapper = new QueryWrapper<>(); > queryWrapper.select("id","name"); > queryWrapper.likeRight("name","黄"); > > List> maps = userMapper.selectMaps(queryWrapper); > maps.forEach(System.out::println); > ~~~ > > **进行数据统计** > > `测试代码` > > ~~~java > QueryWrapper queryWrapper = new QueryWrapper<>(); > queryWrapper.select("manager_id","avg(age)") > .groupBy("manager_id") > .having("avg(age)>={0}",59); > List> maps = userMapper.selectMaps(queryWrapper); > maps.forEach(System.out::println); > ~~~ > 2. selectObjs > > 只会返回第一个字段(第一列)的值,其他字段会被舍弃 - 得到的结果,只封装了第一列的id > > ~~~java > QueryWrapper wrapper = new QueryWrapper<>(); > wrapper.select("id", "name").like("name", "黄"); > List objects = userMapper.selectObjs(wrapper); > objects.forEach(System.out::println); > ~~~ > > 3. selectCount > > 查询满足条件的总数,注意,使用这个方法,不能调用`QueryWrapper`的`select`方法设置要查询的列了。这个方法会自动添加`select count(1)` > > ~~~java > QueryWrapper 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)](https://gitee.com/baomidou/mybatis-plus/blob/3.0/mybatis-plus-extension/src/main/java/com/baomidou/mybatisplus/extension/service/IService.java)接口,进一步封装 CRUD 采用 `get 查询单行` `remove 删除` `list 查询集合` `page 分页` 前缀命名方式区分 `Mapper` 层避免混淆, > - 泛型 `T` 为任意实体对象 > - 建议如果存在自定义通用 Service 方法的可能,请创建自己的 `IBaseService` 继承 `Mybatis-Plus` 提供的基类 > - 对象 `Wrapper` 为 条件构造器 ## 6.2.1 使用方略 > 另外一套CRUD是Service层的,只需要编写一个接口,继承`IService`,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,**比较明显的区别在于**`IService`支持了更多的批量化操作,如`saveBatch`,`saveOrUpdateBatch`等方法。 ### 示例 > 1. 首先,新建一个接口,继承`IService` > > ~~~java > public interface UserService extends IService { > } > ~~~ > > 2. 创建这个接口的实现类,并继承`ServiceImpl`,最后打上`@Service`注解,注册到Spring容器中,即可使用 > > ~~~java > @Service > public class UserServiceImpl extends ServiceImpl implements UserService{ > } > ~~~ ## 6.2.2 Save方法 ~~~java // 插入一条记录(选择字段,策略插入) boolean save(T entity); // 插入(批量) boolean saveBatch(Collection entityList); // 插入(批量)- batchSize - 表示一次批量插入的数据量,默认为 1000 boolean saveBatch(Collection entityList, int batchSize); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :-----------: | :--------: | :----------: | | T | entity | 实体对象 | | Collection | entityList | 实体对象集合 | | int | batchSize | 插入批次数量 | `单元测试` ~~~java @Test public void testSave(){ List 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 ~~~java // TableId 注解存在更新记录,否插入一条记录 boolean saveOrUpdate(T entity); // 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法 boolean saveOrUpdate(T entity, Wrapper updateWrapper); // 批量修改插入 boolean saveOrUpdateBatch(Collection entityList); // 批量修改插入 boolean saveOrUpdateBatch(Collection entityList, int batchSize); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :-----------: | :-----------: | :------------------------------: | | T | entity | 实体对象 | | Wrapper | updateWrapper | 实体对象封装操作类 UpdateWrapper | | Collection | entityList | 实体对象集合 | | int | batchSize | 插入批次数量 | `测试01-根据主键值id进行更新操作` ~~~java /** * 测试根据主键值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条件进行更新` ~~~java /** * 测试根据条件进行更新 * 如果存在,则更新,否则执行插入 */ @Test public void testUpdateByWrapper(){ UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.ge("age",300); User2 u = new User2(); u.setName("ping"); userService.saveOrUpdate(u,updateWrapper); } ~~~ ## 6.2.4 Remove方法 ```java // 根据 entity 条件,删除记录 boolean remove(Wrapper queryWrapper); // 根据 ID 删除 boolean removeById(Serializable id); // 根据 columnMap 条件,删除记录 boolean removeByMap(Map columnMap); // 删除(根据ID 批量删除) boolean removeByIds(Collection idList); ``` `参数说明` | 类型 | 参数名 | 描述 | | :--------------------------------: | :----------: | :---------------------: | | Wrapper | queryWrapper | 实体包装类 QueryWrapper | | Serializable | id | 主键ID | | Map | columnMap | 表字段 map 对象 | | Collection | idList | 主键ID列表 | `测试代码` ~~~java @Test public void testRemove(){ //本人更加推荐lambda写法 userService.lambdaUpdate().eq(User2::getId,3).remove(); } ~~~ ## 6.2.5 Update ~~~java // 根据 UpdateWrapper 条件,更新记录 需要设置sqlset boolean update(Wrapper updateWrapper); // 根据 whereWrapper 条件,更新记录 boolean update(T updateEntity, Wrapper whereWrapper); // 根据 ID 选择修改 boolean updateById(T entity); // 根据ID 批量更新 boolean updateBatchById(Collection entityList); // 根据ID 批量更新 boolean updateBatchById(Collection entityList, int batchSize); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :-----------: | :-----------: | :------------------------------: | | Wrapper | updateWrapper | 实体对象封装操作类 UpdateWrapper | | T | entity | 实体对象 | | Collection | entityList | 实体对象集合 | | int | batchSize | 更新批次数量 | ## 6.2.6 Get ~~~java // 根据 ID 查询 T getById(Serializable id); // 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1") T getOne(Wrapper queryWrapper); // 根据 Wrapper,查询一条记录 T getOne(Wrapper queryWrapper, boolean throwEx); // 根据 Wrapper,查询一条记录 Map getMap(Wrapper queryWrapper); // 根据 Wrapper,查询一条记录 //getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值) V getObj(Wrapper queryWrapper, Function mapper); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :-------------------------: | :----------: | :-----------------------------: | | Serializable | id | 主键ID | | Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper | | boolean | throwEx | 有多个 result 是否抛出异常 | | T | entity | 实体对象 | | Function | mapper | 转换函数 | `测试` ~~~java @Test public void testGet(){ // LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); // queryWrapper.eq(User2::getAge,40); //本人更喜欢如下方式 //这个是方法返回结果不止一条则会抛出异常,如果想默认取第一条结果,可以给这方法传第二个参数为false。 //User2 user2 = userService.getOne(Wrappers.lambdaQuery().eq(User2::getAge,40),false); //System.out.println(user2); //getObj(使用查询构造器,查询一条记录,返回这条记录的第一个字段值) Integer id = userService.getObj(Wrappers.lambdaQuery().eq(User2::getId,1),o->{return Integer.parseInt(o.toString());}); System.out.println(id); } ~~~ ## 6.2.7 List ~~~java // 查询所有 List list(); // 查询列表 List list(Wrapper queryWrapper); // 查询(根据ID 批量查询) Collection listByIds(Collection idList); // 查询(根据 columnMap 条件) Collection listByMap(Map columnMap); // 查询所有列表 List> listMaps(); // 查询列表 List> listMaps(Wrapper queryWrapper); // 查询全部记录 List listObjs(); // 查询全部记录 List listObjs(Function mapper); // 根据 Wrapper 条件,查询全部记录 List listObjs(Wrapper queryWrapper); // 根据 Wrapper 条件,查询全部记录 List listObjs(Wrapper queryWrapper, Function mapper); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--------------------------------: | :----------: | :-----------------------------: | | Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper | | Collection | idList | 主键ID列表 | | Map | columnMap | 表字段 map 对象 | | Function | mapper | 转换函数 | `测试代码` ~~~java @Test public void testList(){ userService.listObjs(o->{ return "user_"+o; }).forEach(e-> System.out.println(e)); } ~~~ ## 6.2.8 Page ~~~java // 无条件分页查询 IPage page(IPage page); // 条件分页查询 IPage page(IPage page, Wrapper queryWrapper); // 无条件分页查询 IPage> pageMaps(IPage page); // 条件分页查询 IPage> pageMaps(IPage page, Wrapper queryWrapper); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--------: | :----------: | :-----------------------------: | | IPage | page | 翻页对象 | | Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper | `测试` ~~~java @Test public void testPage(){ Page page = new Page<>(3,2); // userService.page(page).getRecords().forEach(e-> System.out.println(e)); userService.page(page,Wrappers.lambdaQuery().eq(User2::getAge,40)) .getRecords().forEach(e-> System.out.println(e)); } ~~~ ## 6.2.9 Count ~~~java // 查询总记录数 int count(); // 根据 Wrapper 条件,查询总记录数 int count(Wrapper queryWrapper); ~~~ `参数说明` | 类型 | 参数名 | 描述 | | :--------: | :----------: | :-----------------------------: | | Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper | `测试` ~~~java @Test public void testCount(){ System.out.println(userService.count(Wrappers.lambdaQuery().eq(User2::getAge, 40))); } ~~~ ## 6.3.0 Chain > 1. query > > ~~~java > // 链式查询 普通 > QueryChainWrapper query(); > // 链式查询 lambda 式。注意:不支持 Kotlin > LambdaQueryChainWrapper lambdaQuery(); > > // 示例: > query().eq("column", value).one(); > lambdaQuery().eq(Entity::getId, value).list(); > ~~~ > > `测试` > > ~~~java > // 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)); > ~~~ > > ~~~java > 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()); > ~~~ > > > > 2. update > > ~~~java > // 链式更改 普通 > UpdateChainWrapper update(); > // 链式更改 lambda 式。注意:不支持 Kotlin > LambdaUpdateChainWrapper lambdaUpdate(); > > // 示例: > update().eq("column", value).remove(); > lambdaUpdate().eq(Entity::getId, value).update(entity); > ~~~ > > `测试` > > ~~~java > @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个类,`AbstractWrapper`。`QueryWrapper`,`UpdateWrapper`,它们的类关系如下: ![](wrapper.png) > 在`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 > > ~~~java > @Test > public void test条件构造器(){ > //SELECT id,name,age,email,manager_id,create_time FROM user WHERE (name = ? AND age = ?) > QueryWrapper queryWrapper = new QueryWrapper<>(); > Map conditional = new HashMap<>(); > conditional.put("age",18); > conditional.put("name","小菜"); > queryWrapper.allEq(conditional); > List users = userMapper.selectList(queryWrapper); > users.forEach(e-> System.out.println(e)); > } > ~~~ > > `细节01`-当allEq方法传入的Map中有value为`null`的元素时,默认会设置为`is null` > > ~~~java > @Test > public void test3() { > QueryWrapper wrapper = new QueryWrapper<>(); > Map param = new HashMap<>(); > param.put("age", 40); > param.put("name", null); > wrapper.allEq(param); > List users = userMapper.selectList(wrapper); > users.forEach(System.out::println); > } > ~~~ > > `细节02` - 若想忽略map中value为`null`的元素,可以在调用allEq时,设置参数`boolean null2IsNull`为`false` > > ```java > @Test > public void test3() { > QueryWrapper wrapper = new QueryWrapper<>(); > Map param = new HashMap<>(); > param.put("age", 40); > param.put("name", null); > wrapper.allEq(param, false); > List 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列 > > ~~~java > QueryWrapper wrapper = new QueryWrapper<>(); > Map param = new HashMap<>(); > param.put("age", 18); > param.put("name", "黄主管"); > wrapper.allEq((k,v) -> !"name".equals(k), param); // 过滤掉map中key为name的元素 > List users = userMapper.selectList(wrapper); > users.forEach(System.out::println); > ~~~ ## 7.2 常用方法示例 ~~~java @Test public void test条件构造器02(){ //1. 名字中包含三,且年龄小于25 // QueryWrapper queryWrapper = new QueryWrapper<>(); // queryWrapper.like("name","三") // .lt("age",25); // List userList = userMapper.selectList(queryWrapper); // userList.forEach(System.out::println); //2.姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空 // QueryWrapper queryWrapper = new QueryWrapper<>(); // queryWrapper.likeRight("name","黄").and( // (w->w.gt("age",20) // .lt("age",40).isNotNull("email")) // ); // List userList = userMapper.selectList(queryWrapper); // userList.forEach(System.out::println); //3.或者年龄大于等于23,按照年龄降序排列,年龄相同则按照id升序排列 // QueryWrapper queryWrapper = new QueryWrapper<>(); // queryWrapper.ge("age",23) // .orderByDesc("age") // .orderByAsc("id"); // List userList = userMapper.selectList(queryWrapper); // userList.forEach(System.out::println); //4.创建日期为2021年2月22日,并且直属上级的名字为小姓 // QueryWrapper 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 userList = userMapper.selectList(queryWrapper); // userList.forEach(System.out::println); //5. 正常嵌套 //(年龄小于40或者邮箱不为空) 并且名字为黄姓 // QueryWrapper queryWrapper = new QueryWrapper<>(); // // queryWrapper.nested(c->c.lt("age",40).or().isNotNull("email")) // .likeRight("name","黄") // // //6.last // //.只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用 // .last("limit 1"); // // List userList = userMapper.selectList(queryWrapper); // userList.forEach(System.out::println); //7. 筛选部分列 // QueryWrapper queryWrapper = new QueryWrapper<>(); // // queryWrapper.select("id","name"); // List userList = userMapper.selectList(queryWrapper); // userList.forEach(System.out::println); //8. 选出id, name, age, email, 等同于排除 manager_id 和 create_time // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列, // 可以采用重载的select方法,指定需要排除的列 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.select(User.class,info->{ String colName = info.getColumn(); return !colName.equals("manager_id") && !colName.equals("create_time"); }); List userList = userMapper.selectList(queryWrapper); userList.forEach(System.out::println); } ~~~ ## 7.3 Condition > 条件构造器的诸多方法中,均可以指定一个`boolean`类型的参数`condition`,用来决定该条件是否加入最后生成的WHERE语句中,比如 > > ~~~java > /** > * 测试boolean参数 > */ > @Test > public void testBooleanParam(){ > String name = ""; > QueryWrapper 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 userList = userMapper.selectList(wrapper); > userList.forEach(e-> System.out.println(e)); > } > ~~~ ## 7.4 实体对象作为条件 > 调用构造函数创建一个`Wrapper`对象时,可以传入一个实体对象。 > > 后续使用这个`Wrapper`时,会以实体对象中的非空属性,构建WHERE条件(默认构建**等值匹配**的WHERE条件, > > **这个行为可以通过实体类里各个字段上的`@TableField`注解中的`condition`属性进行改变)** > 1. 示例01 - 会以实体对象中的非空属性,构建WHERE条件 > > 执行结果可以看到,是根据实体对象中的非空属性,进行了**等值匹配查询**。 > > ~~~java > @Test > public void testObjParams(){ > User user = new User(); > user.setName("小菜"); > > QueryWrapper queryWrapper = new QueryWrapper<>(user); > List userList = userMapper.selectList(queryWrapper); > userList.forEach(System.out::println); > } > ~~~ > > 2. 示例02 - 若希望针对某些属性,改变**等值匹配**的行为,则可以在实体类中用`@TableField`注解进行配置,示例如下 > > ~~~java > @TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择 > SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如 > ~~~ > > ~~~java > @Data > public class User { > private Long id; > @TableField(condition = SqlCondition.LIKE) > private String name; > @TableField(condition = "%s > #{%s}") // 这里相当于大于, 其中 > 是字符实体 > private Integer age; > private String email; > private Long managerId; > private LocalDateTime createTime; > } > ~~~ # 8. lambda条件构造器 > lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的**方法引用**来指定列。示例如下 > > ~~~java > @Test > public void testLambda(){ > LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); > lambdaQueryWrapper.like(User::getName,"三"); > List userList = userMapper.selectList(lambdaQueryWrapper); > userList.forEach(System.out::println); > } > ~~~ ## 8.1 补充 > 1. 还有个**链式lambda条件构造器**,使用示例如下 > > ~~~java > @Test > public void testLambda() { > LambdaQueryChainWrapper chainWrapper = new LambdaQueryChainWrapper<>(userMapper); > List users = chainWrapper.like(User::getName, "黄").gt(User::getAge, 30).list(); > users.forEach(System.out::println); > } > ~~~ > > 2. 更新操作 > > ~~~java > @Test > public void testLambdaUpdate(){ > User user = new User(); > user.setName("james"); > LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper<>(); > lambdaUpdateWrapper.eq(User::getId,1); > userMapper.update(user,lambdaUpdateWrapper); > } > ~~~ > 3. 再额外演示一下,链式lambda条件构造器的使用 > > ~~~java > @Test > public void testUpdate5() { > LambdaUpdateChainWrapper 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语句,此时可以不必传入实体对象,示例如下 ~~~java @Test public void testUpdate4() { LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.likeRight(User::getEmail, "share").set(User::getManagerId, 9L); userMapper.update(null, wrapper); } ~~~ # 9. 自定义SQL > 当mp提供的方法还不能满足需求时,则可以自定义SQL。 ## 9.1 原生mybatis ~~~java public interface UserMapper extends BaseMapper { @Select("select * from user") List findAll(); } ~~~ ## 9.2 mybatis-plus > 也可以使用mp提供的Wrapper条件构造器,来自定义SQL > > 在UserMapper.java接口添加 ### 9.2.1 注解配置方式 ~~~java //自定义sql - mybatis-plus方式 @Select("select * from user ${ew.customSqlSegment}") List findAllWhere(@Param(Constants.WRAPPER)Wrapper wrapper); ~~~ `单元测试` ~~~java /** * 自定义SQL * 2. 采用原生的mybatis * 也可以使用mp提供的Wrapper条件构造器,来自定义SQL */ @Test public void testFindAllWhere(){ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.like(User::getName,"三"); List userList = userMapper.findAllWhere(wrapper); userList.forEach(user -> System.out.println(user)); } ~~~ ### 9.2.1 xml配置方式 ~~~java List findAll(Wrapper wrapper); ~~~ ~~~xml ~~~ # 10. 分页查询 > `BaseMapper`中提供了2个方法进行分页查询,分别是 > > 1. `selectPage`:将查询的结果封装成Java实体对象 > 2. `selectMapsPage`:将查询的结果封装成封装成`Map` ## 10.1 selectPage方式 > 1. 创建mp的分页拦截器,注册到Spring容器中 > > ~~~java > @Test > public void testSelectPage(){ > LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper(); > lambdaQueryWrapper.ge(User::getAge,20); > > //设置分页信息,查询第3页,每页显示2条 > Page page = new Page<>(3,2); > > //执行分页查询 > Page 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分析 > > ~~~java > ==> 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`的重载构造函数,指定`isSearchCount`为`false`即可** > > ~~~java > public Page(long current, long size, boolean isSearchCount); > > Page page = new Page<>(3,2,false); > ~~~ > > 设置成第三个参数为false,之后那么只会出现一条sql,与此同时总的记录数和总的页数都将返回默认值0. > > ~~~java > ==> 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语句即可) ~~~java /** *在实际开发中,可能遇到**多表联查**的场景,此时`BaseMapper`中提供的单表分页查询的方法无法满足需求, * 需要**自定义SQL**,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时, * 修改SQL语句即可) * * 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式 * @param page * @param wrapper * @return */ @Select("select * from user ${ew.customSqlSegment}") Page selectUserPage(Page page,@Param(Constants.WRAPPER)Wrapper wrapper); ~~~ `单元测试` ~~~java @Test public void testSelectUserPage(){ LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper(); lambdaQueryWrapper.ge(User::getAge,20); //设置分页信息,查询第3页,每页显示2条 //第三个参数设置成false,不会查询总的记录数 //Page page = new Page<>(3,2,false); Page page = new Page<>(3,2); //执行分页查询 Page 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`进行配置即可。比如,配置了全局采用自增主键策略 ~~~yml mybatis-plus: global-config: db-config: id-type: auto ~~~ `注意一旦设置成了auto,需要修改主键为自增长,修改脚本文件-添加auto_increment` **否则会抛出id does not default value的异常** ~~~sql 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文档中已经找不到了**,被细分成了`insertStrategy`,`updateStrategy`,`selectStrategy`。默认值是`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`:添加表名前缀 > > ~~~yml > mybatis-plus: > global-config: > db-config: > table-prefix: xx_ > ~~~ > > ~~~java > @Test > public void test3() { > QueryWrapper wrapper = new QueryWrapper<>(); > wrapper.like("name", "黄"); > Integer count = userMapper.selectCount(wrapper); > System.out.println(count); > } > ~~~ > > 可以看到拼接出来的SQL,在表名前面添加了前缀 # 13. 高级功能 > 高级功能的演示需要用到一张新的表`user2` > * sql脚本 > > ~~~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实体类 > > ~~~java > @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接口 > > ~~~java > public interface User2Mapper extends BaseMapper { > } > ~~~ ## 13.1. 逻辑删除 > 首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。**逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案**。 > > 日常中,我们在电脑中删除一个文件后,也仅仅是把该文件放入了回收站,日后若有需要还能进行查看或恢复。当我们确定不再需要某个文件,可以将其从回收站中彻底删除。这也是类似的道理。 > > mp提供的逻辑删除实现起来非常简单 > > 只需要在`application.yml`中进行逻辑删除的相关配置即可 > > ~~~yml > mybatis-plus: > global-config: > db-config: > logic-delete-field: deleted # 全局逻辑删除的实体字段名 > logic-delete-value: 1 # 逻辑已删除值(默认为1) > logic-not-delete-value: 0 # 逻辑未删除值(默认为0) > # 若逻辑已删除和未删除的值和默认值一样,则可以不配置这2项 > ~~~ > 测试代码 > > ~~~java > user2Mapper.deleteById(1); > ~~~ > > `分析控制台SQL` > > ~~~java > ==> Preparing: UPDATE user2 SET deleted=1 WHERE id=? AND deleted=0 > ==> Parameters: 1(Integer) > <== Updates: 1 > ~~~ > > 可以看到,发出的SQL不再是`DELETE`,而是`UPDATE` > > 此时我们再执行一次`SELECT` > > ~~~java > @Test > public void testSelect() { > List users = mapper.selectList(null); > } > ~~~ > > `分析控制台SQL` > > ~~~java > ==> 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`进行配置 > > ~~~java > @TableField(select = false) > private Integer deleted; > ~~~ > > `分析控制台SQL` > > ~~~java > SELECT id,name,age,email,manager_id,create_time,update_time,version FROM user2 WHERE deleted=0 > ~~~ > > 查询的列中已经不包括逻辑删除的那一列deleted **注解配置方式** > 前面在`application.yml`中做的配置,是全局的。通常来说,对于多个表,我们也会统一逻辑删除字段的名称,统一逻辑已删除和未删除的值,所以全局配置即可。当然,若要对某些表进行单独配置,在实体类的对应字段上使用`@TableLogic`即可 > ~~~java > @TableLogic(value = "0", delval = "1") > private Integer deleted; > ~~~ **逻辑删除小结** > 开启mp的逻辑删除后,会对SQL产生如下的影响 > > - INSERT语句:没有影响 > - SELECT语句:追加WHERE条件,过滤掉已删除的数据 > - UPDATE语句:追加WHERE条件,防止更新到已删除的数据 > - DELETE语句:转变为UPDATE语句 > > 注意,上述的影响,只针对mp自动注入的SQL生效。如果是自己手动添加的自定义SQL,则不会生效。比如 > > ~~~java > public interface User2Mapper extends BaseMapper { > @Select("select * from user2") > List selectRaw(); > } > ~~~ > 调用这个`selectRaw`,则mp的逻辑删除不会生效。 > > 另,逻辑删除可在`application.yml`中进行全局配置,也可在实体类中用`@TableLogic`进行局部配置。 ## 13.2 自动填充 > 表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。 > > 比较原始的方式,是每次插入或更新时,手动进行设置。 > > mp可以通过配置,对某些字段进行自动填充,实用实例如下 `原理` > - 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler > - 注解填充字段 `@TableField(.. fill = FieldFill.INSERT)` 生成器策略部分也可以配置 `在实体类中的某些字段上,通过`@TableField`设置自动填充` ~~~java @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; } ~~~ `实现自动填充处理器` ~~~java 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() { // @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` ~~~java @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进行了自动填充,**注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值**. **更新时的自动填充,测试如下:** ~~~java @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. 配置乐观锁插件 > > ~~~java > /** > * 乐观锁插件 - 新版 > */ > @Bean > public MybatisPlusInterceptor oPtimisticLockerInterceptor() { > MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); > mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); > return mybatisPlusInterceptor; > } > ~~~ > > 2. 在实体类中表示版本的字段上添加注解`@Version` > > ~~~java > //乐观锁- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime > @Version > private Integer version; > ~~~ > > 3. 测试代码 > > ~~~java > @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 > > ~~~java > ==> 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)`方法** > ~~~java > @Test > public void testOpLocker() { > User2 user = new User2(); > user.setId(8L); > user.setVersion(1); > user.setAge(2); > > // 第一次使用 > LambdaQueryWrapper 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. 引入第三方依赖 > > ~~~xml > > p6spy > p6spy > 3.9.1 > > ~~~ > > 2. 修改application.yml文件 > > ~~~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 > ~~~ > > 3. 在`src/main/resources`资源目录下添加`spy.properties` > > ~~~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 > ~~~ > > 4. 随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来 > > ~~~java > 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. 添加配置 > > ~~~java > @Bean > public MybatisPlusInterceptor blockAttackInterceptor() { > MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); > // 防止全表更新与删除 > // 针对 update 和 delete 语句 > interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); > return interceptor; > } > ~~~ > > 2. 测试 > > ~~~java > @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 > > xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> > 4.0.0 > > tech.aistar > yeb-demo > 0.0.1-SNAPSHOT > > > tech.aistar > yeb-generator > 0.0.1-SNAPSHOT > yeb-generator > Demo project for Spring Boot > > 1.8 > > > > > org.freemarker > freemarker > > > com.baomidou > mybatis-plus-boot-starter > 3.4.3 > > > mysql > mysql-connector-java > runtime > > > com.baomidou > mybatis-plus-generator > 3.4.1 > > > io.springfox > springfox-boot-starter > 3.0.0 > > > > org.projectlombok > lombok > true > > > > com.github.xiaoymin > swagger-bootstrap-ui > 1.9.6 > > > > > > > org.springframework.boot > spring-boot-maven-plugin > > > > > > > ~~~ > > 2. 配置类 > > ~~~java > 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 { > /** > *

> * 读取控制台内容 > *

> */ > 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 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(); > } > ~~~