在学习 MyBatis Plus 的过程中,我参考了黑马程序员的相关课程。
课程地址如下:
黑马程序员SpringCloud微服务开发与实战,java黑马商城项目微服务实战开发(涵盖MybatisPlus、Docker、MQ、ES、Redis高级等)
本文主要内容:
使用 MyBatis Plus 完成基础 CRUD 使用条件构造器编写查询与更新 Mybatis Plus的“伪”批量插入 常用注解的应用 处理枚举、JSON 类型字段 分页功能的使用 1.快速入门为了让后续的示例更好跑起来,我先准备了一个小项目和几张表。整体流程非常简单,就是把环境搭好,然后开始用 MP 写 CRUD。
1.1.环境准备首先,复制课前资料提供好的一个项目到你的工作空间(不要包含空格和特殊字符):
然后用你的IDEA工具打开,项目结构如下:
里面已经包含了一个 User 实体类、对应的 UserMapper 以及最基本的 Spring Boot 项目结构,拿来跑 MP 示例是足够的。
User UserMapper UserMapper.xml application.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @TableName(value = "tb_user", autoResultMap = true) @Data public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private String phone; @TableField(typeHandler = JacksonTypeHandler.class) private UserInfo info; private UserStatus status; private Integer balance; private LocalDateTime createTime; private LocalDateTime updateTime; }
上面这个类就是我们后面用来演示各种 MP 功能的基础模型:包含普通字段、枚举字段,还有一个 JSON 字段用于测试 MP 对复杂类型的支持。
UserMapper 也很简洁,只继承了 BaseMapper:
1 2 public interface UserMapper extends BaseMapper <User> {}
这里最妙的地方就是: 只要继承了 BaseMapper,单表 CRUD 自动到位,不用你自己写 SQL,也不用写 XML。
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.chenpi.mp.mapper.UserMapper" > </mapper >
UserMapper.xml暂时不需要写内容。
1 2 3 4 5 6 7 8 9 10 11 12 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: username password: password logging: level: com.chenpi: debug pattern: dateformat: HH:mm:ss
注意配置一下项目的JDK版本为JDK11。
课程资料里提供了一个mp.sql,执行后会有两张表:user 和 address。 我把 SQL 一并放到下面了,复制后在你本地库里跑一下即可:
对应的数据库表结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 ; ; ; ; ; ; ; ; CREATE DATABASE IF NOT EXISTS `mp` ;USE `mp`; CREATE TABLE IF NOT EXISTS `address` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT '用户ID' , `province` varchar (10 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '省' , `city` varchar (10 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '市' , `town` varchar (10 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '县/区' , `mobile` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手机' , `street` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '详细地址' , `contact` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '联系人' , `is_default` bit(1 ) DEFAULT b'0' COMMENT '是否是默认 1默认 0否' , `notes` varchar (255 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注' , PRIMARY KEY (`id`) USING BTREE, KEY `user_id` (`user_id`) USING BTREE ) ENGINE= InnoDB AUTO_INCREMENT= 67 DEFAULT CHARSET= utf8mb3 ROW_FORMAT= COMPACT; DELETE FROM `address`;INSERT INTO `address` (`id`, `user_id`, `province`, `city`, `town`, `mobile`, `street`, `contact`, `is_default`, `notes`) VALUES (59 , 2 , '北京' , '北京' , '朝阳区' , '13900112222' , '金燕龙办公楼' , 'Rose' , b'1' , NULL ), (60 , 1 , '北京' , '北京' , '朝阳区' , '13700221122' , '修正大厦' , 'Jack' , b'0' , NULL ), (61 , 1 , '上海' , '上海' , '浦东新区' , '13301212233' , '航头镇航头路' , 'Jack' , b'1' , NULL ), (63 , 2 , '广东' , '佛山' , '永春' , '13301212233' , '永春武馆' , 'Rose' , b'0' , NULL ), (64 , 3 , '浙江' , '杭州' , '拱墅区' , '13567809102' , '浙江大学' , 'Hope' , b'1' , NULL ), (65 , 3 , '浙江' , '杭州' , '拱墅区' , '13967589201' , '左岸花园' , 'Hope' , b'0' , NULL ), (66 , 4 , '湖北' , '武汉' , '汉口' , '13967519202' , '天天花园' , 'Thomas' , b'1' , NULL ); CREATE TABLE IF NOT EXISTS `user ` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id' , `username` varchar (50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名' , `password` varchar (128 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码' , `phone` varchar (20 ) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '注册手机号' , `info` json NOT NULL COMMENT '详细信息' , `status` int DEFAULT '1' COMMENT '使用状态(1正常 2冻结)' , `balance` int DEFAULT NULL COMMENT '账户余额' , `create_time` datetime NOT NULL COMMENT '创建时间' , `update_time` datetime NOT NULL COMMENT '更新时间' , PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `username` (`username`) USING BTREE ) ENGINE= InnoDB AUTO_INCREMENT= 1674613593516095922 DEFAULT CHARSET= utf8mb3 ROW_FORMAT= COMPACT COMMENT= '用户表' ; DELETE FROM `user `;INSERT INTO `user ` (`id`, `username`, `password`, `phone`, `info`, `status`, `balance`, `create_time`, `update_time`) VALUES (1 , 'Jack' , '123' , '13900112224' , '{"age": 20, "intro": "佛系青年", "gender": "male"}' , 1 , 1600 , '2023-05-19 20:50:21' , '2023-06-19 20:50:21' ), (2 , 'Rose' , '123' , '13900112223' , '{"age": 19, "intro": "青涩少女", "gender": "female"}' , 1 , 300 , '2023-05-19 21:00:23' , '2023-06-19 21:00:23' ), (3 , 'Hope' , '123' , '13900112222' , '{"age": 25, "intro": "上进青年", "gender": "male"}' , 1 , 100000 , '2023-06-19 22:37:44' , '2023-06-19 22:37:44' ), (4 , 'Thomas' , '123' , '17701265258' , '{"age": 29, "intro": "伏地魔", "gender": "male"}' , 1 , 800 , '2023-06-19 23:44:45' , '2023-06-19 23:44:45' ); ; ; ; ; ;
最后,在application.yaml中修改jdbc参数为你自己的数据库参数:
1 2 3 4 5 6 7 8 9 10 11 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: MySQL123 logging: level: com.chenpi: debug pattern: dateformat: HH:mm:ss
1.2.快速开始 环境准备好了之后,就可以正式体验一下 MyBatis Plus 的“零 SQL” CRUD 了。
整个流程非常简单,总结一下就两步:
引入 MP 的依赖 创建一个继承 BaseMapper 的 Mapper 做完这两件事,单表 CRUD 就已经全部自动生成了。
1.2.1引入依赖 MyBatis Plus 给我们准备了 starter,所以不用自己配 SqlSessionFactory,也不用管扫描配置,只需要在 Maven 中加一行依赖:
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.3.1</version > </dependency >
由于这个 starter 已经包含了 MyBatis 的自动装配,你完全可以把原来的 MyBatis starter 删掉,保持依赖干净一点。
最终整个项目的常规依赖大概是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <dependencies > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.3.1</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</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 > </dependencies >
加上这些就够跑单表 CRUD 了。
1.2.2.定义Mapper MyBatis Plus 的理念很清爽: 既然绝大部分项目都要做基础的 CRUD,那为什么还要每个人自己写 SQL?
所以 MP 直接给我们准备了一个 BaseMapper<T>,里面已经提供了常见的单表操作,包括:
insert delete update select 批量查询 条件查询…… 你想用谁,就直接调用谁。
所以我们自己的 Mapper 就不需要写 XML,也不需要写方法,只要继承一下:
1 2 public interface UserMapper extends BaseMapper <User> {}
到这里,User 表的基础 CRUD 已经全部就绪了。
1.2.3.测试 新建一个测试类,编写几个单元测试,来感受一下 MP 的操作方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test void testInsert () { User user = new User (); user.setId(5L ); user.setUsername("Lucy" ); user.setPassword("123" ); user.setPhone("18688990011" ); user.setBalance(200 ); user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}" ); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); userMapper.insert(user); } @Test void testSelectById () { User user = userMapper.selectById(5L ); System.out.println("user = " + user); } @Test void testSelectByIds () { List<User> users = userMapper.selectBatchIds(List.of(1L , 2L , 3L , 4L , 5L )); users.forEach(System.out::println); } @Test void testUpdateById () { User user = new User (); user.setId(5L ); user.setBalance(20000 ); userMapper.updateById(user); } @Test void testDelete () { userMapper.deleteById(5L ); } }
跑完之后,应该能在控制台看到完整的 SQL 日志,比如:
1 2 3 4 5 6 11:05:01 INFO 15524 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 11:05:02 INFO 15524 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 11:05:02 DEBUG 15524 --- [ main] c.i.mp.mapper.UserMapper.selectById : ==> Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM user WHERE id=? 11:05:02 DEBUG 15524 --- [ main] c.i.mp.mapper.UserMapper.selectById : ==> Parameters: 5(Long) 11:05:02 DEBUG 15524 --- [ main] c.i.mp.mapper.UserMapper.selectById : <== Total: 1 user = User(id=5, username=Lucy, password=123, phone=18688990011, info={"age": 21}, status=1, balance=20000, createTime=Fri Jun 30 11:02:30 CST 2023, updateTime=Fri Jun 30 11:02:30 CST 2023)
这些 SQL 都是 MP 自动帮你生成的,语法也非常规范。
说实话,第一次用 MP 时我挺惊讶的:不写 XML、不写 SQL,却能跑出完整 CRUD,这感觉真挺轻松的。
1.3.常见注解在快速入门里,我们只是引入了依赖,加了一个继承 BaseMapper 的接口,就能跑 CRUD。 看起来非常轻松,但这也引出了一个问题:
MyBatis Plus 是怎么知道你想查哪张表、字段有哪些、主键是什么的?
核心就是:MP 会根据实体类推断数据库表结构。
比如,在继承 BaseMapper<User> 时,泛型里的 User 就是你和数据库绑定的那张表的 PO 类。
默认情况下 MP 的推断规则是:
类名驼峰转下划线 → 表名UserInfo → user_info 字段名驼峰转下划线 → 列名createTime → create_time 名为 id 的字段默认当主键 这些默认规则一般够用,但在真实项目里,总会遇到不匹配的情况: 字段名不一致、关键字冲突、主键策略不同等等。
所以 MP 提供了一套常用注解,用来“手动声明”实体和表的对应关系。
1.3.1.@TableName@TableName 用来声明实体类对应的表名。
示例:
1 2 3 4 5 @TableName("user") public class User { private Long id; private String name; }
除了直接指定表名以外,这个注解还有不少扩展属性,例如 schema、自动 resultMap、字段排除等。
属性 类型 必须指定 默认值 描述 value String 否 “” 表名 schema String 否 “” schema keepGlobalPrefix boolean 否 false 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) resultMap String 否 “” xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) autoResultMap boolean 否 false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) excludeProperty String[] 否 {} 需要排除的属性名 @since 3.3.1
常用场景主要是两类:
实体类命名和表名不一致 有 schema、前缀、或者多数据源要求 1.3.2.@TableId 顾名思义,就是给主键字段标识用的。
示例:
1 2 3 4 5 6 @TableName("user") public class User { @TableId private Long id; private String name; }
TableId注解支持两个属性:
属性 类型 必须指定 默认值 描述 value String 否 “” 表名 type Enum 否 IdType.NONE 指定主键类型
IdType支持的类型有:
值 描述 AUTO 数据库 ID 自增 NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) INPUT insert 前自行 set 主键值 ASSIGN_ID 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) ASSIGN_UUID 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) ID_WORKER 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) UUID 32 位 UUID 字符串(please use ASSIGN_UUID) ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)
这里比较常见的有三种:
AUTO:利用数据库的id自增长INPUT:主键你自己 set,MP 不管ASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略,在微服务项目里非常常用,不依赖数据库自增 1.3.3.@TableField 绝大多数字段不用加 @TableField,但以下情况必须加:
字段名和数据库列名不一致 字段以 isXxx 命名(按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符) 成员变量名与数据库一致,但是与数据库的关键字冲突。字段名是数据库关键字,比如 order、desc、concat;使用@TableField注解给字段名添加转义字符:```` 示例:
1 2 3 4 5 6 7 8 9 10 11 @TableName("user") public class User { @TableId private Long id; private String name; private Integer age; @TableField(is_married") private Boolean isMarried; @TableField("`concat`") private String concat; }
支持的其它属性如下:
属性 类型 必填 默认值 描述 value String 否 “” 数据库字段名 exist boolean 否 true 是否为数据库表字段 condition String 否 “” 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 , 参考(opens new window) update String 否 “” 字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) insertStrategy Enum 否 FieldStrategy.DEFAULT 举例:NOT_NULL insert into table_a(column ) values (#{columnProperty} ) updateStrategy Enum 否 FieldStrategy.DEFAULT 举例:IGNORED update table_a set column=#{columnProperty} whereStrategy Enum 否 FieldStrategy.DEFAULT 举例:NOT_EMPTY wherecolumn=#{columnProperty} fill Enum 否 FieldFill.DEFAULT 字段自动填充策略 select boolean 否 true 是否进行 select 查询 keepGlobalFormat boolean 否 false 是否保持使用全局的 format 进行处理 jdbcType JdbcType 否 JdbcType.UNDEFINED JDBC 类型 (该默认值不代表会按照该值生效) typeHandler TypeHander 否 类型处理器 (该默认值不代表会按照该值生效) numericScale String 否 “” 指定小数点后保留的位数
1.4.常见配置 MyBatis Plus 的配置项都可以写在 application.yml 里,详见官方文档:
https://baomidou.com/reference/
虽然默认值已经足够应付大多数场景,但有些配置必须你自己声明,比如:
1 2 3 4 5 mybatis-plus: type-aliases-package: com.chenpi.mp.domain.po global-config: db-config: id-type: auto
需要注意的是,MyBatisPlus也支持手写SQL的,而mapper文件的读取地址可以自己配置:
1 2 mybatis-plus: mapper-locations: "classpath*:/mapper/**/*.xml"
默认值是classpath*:/mapper/**/*.xml,也就是说我们只要把mapper.xml文件放置这个目录下就一定会被加载。
例如,我们新建一个UserMapper.xml文件:
然后在其中定义一个方法:
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.chenpi.mp.mapper.UserMapper" > <select id ="queryById" resultType ="User" > SELECT * FROM user WHERE id = #{id} </select > </mapper >
然后在测试类UserMapperTest中测试该方法:
1 2 3 4 5 @Test void testQuery () { User user = userMapper.queryById(1L ); System.out.println("user = " + user); }
2.核心功能 刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了。
2.1.条件构造器除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。
参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:
而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:
而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:
接下来,我们就来看看如何利用Wrapper实现复杂查询。
2.1.1.QueryWrapper 无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。
例如:
查询 :查询出名字中带o的,存款大于等于1000元的人。代码如下:
1 2 3 4 5 6 7 8 9 10 11 @Test void testQueryWrapper () { QueryWrapper<User> wrapper = new QueryWrapper <User>() .select("id" , "username" , "info" , "balance" ) .like("username" , "o" ) .ge("balance" , 1000 ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
更新 :更新用户名为jack的用户的余额为2000,代码如下:
1 2 3 4 5 6 7 8 9 @Test void testUpdateByQueryWrapper () { QueryWrapper<User> wrapper = new QueryWrapper <User>().eq("username" , "Jack" ); User user = new User (); user.setBalance(2000 ); userMapper.update(user, wrapper); }
2.1.2.UpdateWrapper 基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例如:更新id为1,2,4的用户的余额,扣200,对应的SQL应该是:
1 UPDATE user SET balance = balance - 200 WHERE id in (1 , 2 , 4 )
SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql()功能了:
1 2 3 4 5 6 7 8 9 10 11 @Test void testUpdateWrapper () { List<Long> ids = List.of(1L , 2L , 4L ); UpdateWrapper<User> wrapper = new UpdateWrapper <User>() .setSql("balance = balance - 200" ) .in("id" , ids); userMapper.update(null , wrapper); }
2.1.3.LambdaQueryWrapper无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter方法结合反射技术。因此我们只要将条件对应的字段的getter方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用和Lambda表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
LambdaQueryWrapper LambdaUpdateWrapper 分别对应QueryWrapper和UpdateWrapper
其使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 @Test void testLambdaQueryWrapper () { QueryWrapper<User> wrapper = new QueryWrapper <>(); wrapper.lambda() .select(User::getId, User::getUsername, User::getInfo, User::getBalance) .like(User::getUsername, "o" ) .ge(User::getBalance, 1000 ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
2.2 .自定义SQL在演示UpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:
这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。 这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。
所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL
2.2.1.基本用法 以当前案例来说,我们可以这样写:
1 2 3 4 5 6 7 8 9 @Test void testCustomWrapper () { List<Long> ids = List.of(1L , 2L , 4L ); QueryWrapper<User> wrapper = new QueryWrapper <User>().in("id" , ids); userMapper.deductBalanceByIds(200 , wrapper); }
然后在UserMapper中自定义SQL:
1 2 3 4 public interface UserMapper extends BaseMapper <User> { @Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}") void deductBalanceByIds (@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper) ; }
这样就省去了编写复杂查询条件的烦恼了。
2.2.2.多表关联 理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。 例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户 要是自己基于mybatis实现SQL,大概是这样的:
1 2 3 4 5 6 7 8 9 10 <select id ="queryUserByIdAndAddr" resultType ="com.chenpi.mp.domain.po.User" > SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id WHERE u.id <foreach collection ="ids" separator ="," item ="id" open ="IN (" close =")" > #{id} </foreach > AND a.city = #{city} </select >
可以看出其中最复杂的就是WHERE条件的编写,如果业务复杂一些,这里的SQL会更变态。
但是基于自定义SQL结合Wrapper的玩法,我们就可以利用Wrapper来构建查询条件,然后手写SELECT及FROM部分,实现多表查询。
查询条件这样来构建:
1 2 3 4 5 6 7 8 9 10 11 12 @Test void testCustomJoinWrapper () { QueryWrapper<User> wrapper = new QueryWrapper <User>() .in("u.id" , List.of(1L , 2L , 4L )) .eq("a.city" , "北京" ); List<User> users = userMapper.queryUserByWrapper(wrapper); users.forEach(System.out::println); }
然后在UserMapper中自定义方法:
1 2 @Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}") List<User> queryUserByWrapper (@Param("ew") QueryWrapper<User> wrapper) ;
当然,也可以在UserMapper.xml中写SQL:
1 2 3 <select id ="queryUserByIdAndAddr" resultType ="com.chenpi.mp.domain.po.User" > SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment} </select >
2.3.Service接口MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:
save:新增remove:删除update:更新get:查询单个结果list:查询集合结果count:计数page:分页查询 2.3.1.CRUD我们先俩看下基本的CRUD接口。 新增 :
save是新增单个元素saveBatch是批量新增saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch是批量的新增或修改删除:
removeById:根据id删除removeByIds:根据id批量删除removeByMap:根据Map中的键值对为条件删除remove(Wrapper<T>):根据Wrapper条件删除~~removeBatchByIds~~:暂不支持修改:
updateById:根据id修改update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含set和where部分update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据updateBatchById:根据id批量修改Get:
getById:根据id查询1条数据getOne(Wrapper<T>):根据Wrapper查询1条数据getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到MapperList:
listByIds:根据id批量查询list(Wrapper<T>):根据Wrapper条件查询多条数据list():查询所有Count :
count():统计所有数量count(Wrapper<T>):统计符合Wrapper条件的数据数量getBaseMapper : 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
2.3.2.基本用法 由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。
首先,定义IUserService,继承IService:
1 2 3 public interface IUserService extends IService <User> { }
然后,编写UserServiceImpl类,继承ServiceImpl,实现UserService:
1 2 3 4 @Service public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements IUserService { }
项目结构如下:
接下来,我们快速实现下面4个接口:
编号 接口 请求方式 请求路径 请求参数 返回值 1 新增用户 POST /users 用户表单实体 无 2 删除用户 DELETE /users/{id} 用户id 无 3 根据id查询用户 GET /users/{id} 用户id 用户VO 4 根据id批量查询 GET /users 用户id集合 用户VO集合
首先,我们在项目中引入几个依赖:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-openapi2-spring-boot-starter</artifactId > <version > 4.1.0</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
然后需要配置swagger信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 knife4j: enable: true openapi: title: 用户管理接口文档 description: "用户管理接口文档" email: nano.chenpi@foxmail.com concat: nano.chenpi url: version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.chenpi.mp.controller
然后,接口需要一个Controller以及两个实体:
UserFormDTO:代表新增时的用户表单 UserVO:代表查询的返回结果 UserFormDTO UserVO UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data @ApiModel(description = "用户表单实体") public class UserFormDTO { @ApiModelProperty("id") private Long id; @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; @ApiModelProperty("注册手机号") private String phone; @ApiModelProperty("详细信息,JSON风格") private String info; @ApiModelProperty("账户余额") private Integer balance; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data @ApiModel(description = "用户VO实体") public class UserVO { @ApiModelProperty("用户id") private Long id; @ApiModelProperty("用户名") private String username; @ApiModelProperty("详细信息") private String info; @ApiModelProperty("使用状态(1正常 2冻结)") private Integer status; @ApiModelProperty("账户余额") private Integer balance; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Api(tags = "用户管理接口") @RequiredArgsConstructor @RestController @RequestMapping("users") public class UserController { private final IUserService userService; @PostMapping @ApiOperation("新增用户") public void saveUser (@RequestBody UserFormDTO userFormDTO) { User user = BeanUtil.copyProperties(userFormDTO, User.class); userService.save(user); } @DeleteMapping("/{id}") @ApiOperation("删除用户") public void removeUserById (@PathVariable("id") Long userId) { userService.removeById(userId); } @GetMapping("/{id}") @ApiOperation("根据id查询用户") public UserVO queryUserById (@PathVariable("id") Long userId) { User user = userService.getById(userId); return BeanUtil.copyProperties(user, UserVO.class); } @GetMapping @ApiOperation("根据id集合查询用户") public List<UserVO> queryUserByIds (@RequestParam("ids") List<Long> ids) { List<User> users = userService.listByIds(ids); return BeanUtil.copyToList(users, UserVO.class); } }
可以看到上述接口都直接在controller即可实现,无需编写任何service代码,非常方便。
不过,一些带有业务逻辑的接口则需要在service中自定义实现了。例如下面的需求:
这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:
这些业务逻辑都要在service层来做,另外更新余额需要自定义SQL,要在mapper中来实现。因此,我们除了要编写controller以外,具体的业务还要在service和mapper中编写。
首先在UserController中定义一个方法:
1 2 3 4 5 @PutMapping("{id}/deduction/{money}") @ApiOperation("扣减用户余额") public void deductBalance (@PathVariable("id") Long id, @PathVariable("money") Integer money) { userService.deductBalance(id, money); }
然后是UserService接口:
1 2 3 public interface IUserService extends IService <User> { void deductBalance (Long id, Integer money) ; }
最后是UserServiceImpl实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements IUserService { @Override public void deductBalance (Long id, Integer money) { User user = getById(id); if (user == null || user.getStatus() == 2 ) { throw new RuntimeException ("用户状态异常" ); } if (user.getBalance() < money) { throw new RuntimeException ("用户余额不足" ); } baseMapper.deductMoneyById(id, money); } }
最后是mapper:
1 2 @Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}") void deductMoneyById (@Param("id") Long id, @Param("money") Integer money) ;
2.3.3.LambdaIService中还提供了Lambda功能来简化我们的复杂查询及更新功能。通过两个案例来学习一下。
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
name:用户名关键字,可以为空 status:用户状态,可以为空 minBalance:最小余额,可以为空 maxBalance:最大余额,可以为空 可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。
我们首先需要定义一个查询条件实体,UserQuery实体:
1 2 3 4 5 6 7 8 9 10 11 12 @Data @ApiModel(description = "用户查询条件实体") public class UserQuery { @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private Integer status; @ApiModelProperty("余额最小值") private Integer minBalance; @ApiModelProperty("余额最大值") private Integer maxBalance; }
接下来我们在UserController中定义一个controller方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @GetMapping("/list") @ApiOperation("根据id集合查询用户") public List<UserVO> queryUsers (UserQuery query) { String username = query.getName(); Integer status = query.getStatus(); Integer minBalance = query.getMinBalance(); Integer maxBalance = query.getMaxBalance(); LambdaQueryWrapper<User> wrapper = new QueryWrapper <User>().lambda() .like(username != null , User::getUsername, username) .eq(status != null , User::getStatus, status) .ge(minBalance != null , User::getBalance, minBalance) .le(maxBalance != null , User::getBalance, maxBalance); List<User> users = userService.list(wrapper); return BeanUtil.copyToList(users, UserVO.class); }
在组织查询条件的时候,我们加入了 username != null 这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。
不过上述条件构建的代码太麻烦了。 因此Service中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuery和lambdaUpdate方法:
基于Lambda查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @GetMapping("/list") @ApiOperation("根据id集合查询用户") public List<UserVO> queryUsers (UserQuery query) { String username = query.getName(); Integer status = query.getStatus(); Integer minBalance = query.getMinBalance(); Integer maxBalance = query.getMaxBalance(); List<User> users = userService.lambdaQuery() .like(username != null , User::getUsername, username) .eq(status != null , User::getStatus, status) .ge(minBalance != null , User::getBalance, minBalance) .le(maxBalance != null , User::getBalance, maxBalance) .list(); return BeanUtil.copyToList(users, UserVO.class); }
可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:
.one():最多1个结果.list():返回集合结果.count():返回计数结果MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。
与lambdaQuery方法类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务。
例如下面的需求:
需求:改造根据id修改用户余额的接口,要求如下
如果扣减后余额为0,则将用户status修改为冻结状态(2) 在扣减用户余额时,需要对用户剩余余额做出判断,如果发现剩余余额为0,则应该将status修改为2,就是说update语句的set部分是动态的。
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override @Transactional public void deductBalance (Long id, Integer money) { User user = getById(id); if (user == null || user.getStatus() == 2 ) { throw new RuntimeException ("用户状态异常!" ); } if (user.getBalance() < money) { throw new RuntimeException ("用户余额不足!" ); } int remainBalance = user.getBalance() - money; lambdaUpdate() .set(User::getBalance, remainBalance) .set(remainBalance == 0 , User::getStatus, 2 ) .eq(User::getId, id) .eq(User::getBalance, user.getBalance()) .update(); }
2.3.4.批量新增IService 提供的批量新增功能用起来确实非常方便,不过这里面有几个容易被忽略的性能差异。我们先通过一个简单的测试来看看到底有什么不同。
2.3.4.1.逐条插入1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test void testSaveOneByOne () { long b = System.currentTimeMillis(); for (int i = 1 ; i <= 100000 ; i++) { userService.save(buildUser(i)); } long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); } private User buildUser (int i) { User user = new User (); user.setUsername("user_" + i); user.setPassword("123" ); user.setPhone("" + (18688190000L + i)); user.setBalance(2000 ); user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}" ); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(user.getCreateTime()); return user; }
执行结果如下:
这段代码没什么技巧,就是纯循环 insert,实际的耗时也印证了这一点:非常慢 。
2.3.4.2.MyBatisPlus 批处理插入接下来使用 MP 的批量插入功能,每 1000 条做一次批处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test void testSaveBatch () { List<User> list = new ArrayList <>(1000 ); long b = System.currentTimeMillis(); for (int i = 1 ; i <= 100000 ; i++) { list.add(buildUser(i)); if (i % 1000 == 0 ) { userService.saveBatch(list); list.clear(); } } long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); }
执行最终耗时如下:
实际测试显示:使用了批处理以后,比逐条新增效率提高了10倍左右,性能还是不错的。
虽然性能提升了,但看一下 MyBatisPlus 的源码,可以发现它的批处理实现方式其实是基于 PreparedStatement 的预编译 + 批量提交 :
1 2 3 4 5 6 @Transactional(rollbackFor = Exception.class) @Override public boolean saveBatch (Collection<T> entityList, int batchSize) { String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); }
继续跟进 SqlHelper 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static <E> boolean executeBatch (Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) { Assert.isFalse(batchSize < 1 , "batchSize must not be less than one" ); return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> { int size = list.size(); int idxLimit = Math.min(batchSize, size); int i = 1 ; for (E element : list) { consumer.accept(sqlSession, element); if (i == idxLimit) { sqlSession.flushStatements(); idxLimit = Math.min(idxLimit + batchSize, size); } i++; } }); }
从源码可以发现,其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。
MyBatisPlus 的批处理不是“多值 insert”,而是在循环里调用多次 insert,只是每一批用预编译减少 SQL 解析开销。
SQL类似这样:
1 2 3 4 Preparing : INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )Parameters : user_1, 123 , 18688190001 , "" , 2000 , 2023 -07 -01 , 2023 -07 -01 Parameters : user_2, 123 , 18688190002 , "" , 2000 , 2023 -07 -01 , 2023 -07 -01 Parameters : user_3, 123 , 18688190003 , "" , 2000 , 2023 -07 -01 , 2023 -07 -01
也就是说:
SQL 仍然是一条一条执行的 只是 prepare 一次、执行多次 性能提升来自“减少 SQL 解析”,不是减少 SQL 数量 2.3.4.4.进一步提升:让 SQL 变成真正的批量插入如果想获得最好的插入性能,关键在于“让多条 insert 合并为一条多值 insert”,像这样:
1 2 3 4 5 6 INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )VALUES (user_1, 123 , 18688190001 , "", 2000 , 2023 -07 -01 , 2023 -07 -01 ), (user_2, 123 , 18688190002 , "", 2000 , 2023 -07 -01 , 2023 -07 -01 ), (user_3, 123 , 18688190003 , "", 2000 , 2023 -07 -01 , 2023 -07 -01 ), (user_4, 123 , 18688190004 , "", 2000 , 2023 -07 -01 , 2023 -07 -01 );
但这不是 MP 控制的,而是 JDBC 驱动在满足条件后自动帮你合并。 要让驱动开启 SQL 重写,需要在 MySQL 连接参数中设置:
rewriteBatchedStatements = true(重写批处理的statement语句)。
默认值是false,所以必须手动开启。开启之后,无论是 MyBatisPlus 的 saveBatch 还是你手写批处理,都会被自动“重写”为多值 insert,从而达到最佳插入性能。
参考文档
修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:
1 2 3 4 5 6 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true driver-class-name: com.mysql.cj.jdbc.Driver username: username password: password
再次测试插入10万条数据,可以发现速度有非常明显的提升:
在ClientPreparedStatement的executeBatchInternal中,有判断rewriteBatchedStatements值是否为true并重写SQL的功能:
最终,SQL被重写了:
3.扩展功能 3.1.代码生成 在使用MybatisPlus以后,基础的Mapper、Service、PO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。
3.1.1.安装插件 在Idea的plugins市场中搜索并安装MyBatisPlus插件:
然后重启你的Idea即可使用。
3.1.2.使用 刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到 Other → Config Database :
在弹出的窗口中填写数据库连接的基本信息:
点击OK 保存。
再次点击顶部菜单 Other → Code Generator :
在弹出的表单中填写信息:
最终,代码自动生成到指定的位置了。
3.2.静态工具 有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:
1. 示例代码:Db 查询示例 2. 需求:根据 id 查询用户并返回地址列表 3. Controller & Service 改造 4. 扩展:根据 id 批量查询用户及地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test void testDbGet () { User user = Db.getById(1L , User.class); System.out.println(user); } @Test void testDbList () { List<User> list = Db.lambdaQuery(User.class) .like(User::getUsername, "o" ) .ge(User::getBalance, 1000 ) .list(); list.forEach(System.out::println); } @Test void testDbUpdate () { Db.lambdaUpdate(User.class) .set(User::getBalance, 2000 ) .eq(User::getUsername, "Rose" ) .update(); }
为了在根据用户 id 查询用户时,同时返回用户的收货地址列表,我们需要先准备一个 AddressVO 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Data @ApiModel(description = "收货地址VO") public class AddressVO { @ApiModelProperty("id") private Long id; @ApiModelProperty("用户ID") private Long userId; @ApiModelProperty("省") private String province; @ApiModelProperty("市") private String city; @ApiModelProperty("县/区") private String town; @ApiModelProperty("手机") private String mobile; @ApiModelProperty("详细地址") private String street; @ApiModelProperty("联系人") private String contact; @ApiModelProperty("是否默认 1默认 0否") private Boolean isDefault; @ApiModelProperty("备注") private String notes; }
同时扩展原来的 UserVO,加上地址列表属性:
Controller 添加接口:
1 2 3 4 5 @GetMapping("/{id}") @ApiOperation("根据id查询用户") public UserVO queryUserById (@PathVariable("id") Long userId) { return userService.queryUserAndAddressById(userId); }
IUserService 中定义方法:
1 2 3 4 5 6 public interface IUserService extends IService <User> { void deduct (Long id, Integer money) ; UserVO queryUserAndAddressById (Long userId) ; }
Service 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public UserVO queryUserAndAddressById (Long userId) { User user = getById(userId); if (user == null ) { return null ; } List<Address> addresses = Db.lambdaQuery(Address.class) .eq(Address::getUserId, userId) .list(); UserVO userVO = BeanUtil.copyProperties(user, UserVO.class); userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class)); return userVO; }
实现“根据多个 id 查询用户及其全部地址”的写法可以这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public List<UserVO> queryUsersAndAddresses (List<Long> userIds) { if (CollUtil.isEmpty(userIds)) { return Collections.emptyList(); } List<User> users = listByIds(userIds); List<Address> allAddresses = Db.lambdaQuery(Address.class) .in(Address::getUserId, userIds) .list(); Map<Long, List<Address>> addressMap = allAddresses.stream() .collect(Collectors.groupingBy(Address::getUserId)); return users.stream().map(user -> { UserVO vo = BeanUtil.copyProperties(user, UserVO.class); List<Address> addrList = addressMap.get(user.getId()); vo.setAddresses(BeanUtil.copyToList(addrList, AddressVO.class)); return vo; }).collect(Collectors.toList()); }
这个实现一次性查用户、一次性查地址,避免循环查询,性能好也更符合 MP 的使用方式。
3.3.逻辑删除对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
在表中添加一个字段标记数据是否被删除 当删除数据时把标记置为true 查询时过滤掉标记为true的数据 一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意 ,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
例如,我们给address表添加一个逻辑删除字段:
1 alter table address add deleted bit default b'0' null comment '逻辑删除' ;
然后给Address实体添加deleted字段:
接下来,我们要在application.yml中配置逻辑删除字段:
1 2 3 4 5 6 mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0
测试: 首先,我们执行一个删除操作:
1 2 3 4 5 @Test void testDeleteByLogic () { addressService.removeById(59L ); }
方法与普通删除一模一样,但是底层的SQL逻辑变了:
查询一下:
1 2 3 4 5 @Test void testQuery () { List<Address> list = addressService.list(); list.forEach(System.out::println); }
会发现id为59的确实没有查询出来,而且SQL中也对逻辑删除字段做了判断:
综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD,基本不用考虑代码逻辑问题。还是非常方便的。
注意 : 逻辑删除本身也有自己的问题,比如:
会导致数据库表垃圾数据越来越多,从而影响查询效率 SQL中全都需要对逻辑删除字段做判断,影响查询效率 因此,不太推荐采用逻辑删除功能 ,如果数据不能删除,可以采用把数据迁移到其它表的办法。
3.3.通用枚举 User类中有一个用户状态字段:
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举与Integer转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换 。
3.3.1.定义枚举 我们定义一个用户状态的枚举:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Getter public enum UserStatus { NORMAL(1 , "正常" ), FREEZE(2 , "冻结" ) ; private final int value; private final String desc; UserStatus(int value, String desc) { this .value = value; this .desc = desc; } }
然后把User类中的status字段改为UserStatus 类型:
要让Mybatis Plus处理枚举与数据库类型自动转换,必须告诉Mybatis Plus,枚举中的哪个字段的值作为数据库值。 Mybatis Plus提供了@EnumValue注解来标记枚举属性:
3.3.2.配置枚举处理器在application.yaml文件中添加配置:
1 2 3 mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
3.3.3.测试 1 2 3 4 5 @Test void testService () { List<User> list = userService.list(); list.forEach(System.out::println); }
最终,查询出的User类的status字段会是枚举类型:
同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性:
并且,在UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段:
最后,在页面查询,结果如下:
3.4.JSON类型处理器数据库的user表中有一个info字段,是JSON类型:
格式像这样:
1 { "age" : 20 , "intro" : "佛系青年" , "gender" : "male" }
而目前User实体类中却是String类型:
这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。
而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。
接下来,我们就来看看这个处理器该如何使用。
3.4.1.定义实体 首先,我们定义一个单独实体类来与info字段的属性匹配:
代码如下:
1 2 3 4 5 6 @Data public class UserInfo { private Integer age; private String intro; private String gender; }
3.4.2.使用类型处理器 接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:
同时,在User类上添加一个注解,声明自动映射:
测试可以发现,所有数据都正确封装到UserInfo当中了:
同时,为了让页面返回的结果也以对象格式返回,我们要修改UserVO中的info字段:
此时,在页面查询结果如下:
3.5.配置加密(选学) 目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。
我们以数据库的用户名和密码为例。
3.5.1.生成秘钥首先,我们利用AES工具生成一个随机秘钥,然后对用户名、密码加密:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class MpDemoApplicationTests { @Test void contextLoads () { String randomKey = AES.generateRandomKey(); System.out.println("randomKey = " + randomKey); String username = AES.encrypt("root" , randomKey); System.out.println("username = " + username); String password = AES.encrypt("MySQL123" , randomKey); System.out.println("password = " + password); } }
打印结果如下:
1 2 3 randomKey = 6234633a66fb399f username = px2bAbnUfiY8K/IgsKvscg == password = FGvCSEaOuga3ulDAsxw68 Q==
3.5.2.修改配置 修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文:
1 2 3 4 5 6 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true driver-class-name: com.mysql.cj.jdbc.Driver username: mpw:QWWVnk1Oal3258x5rVhaeQ== password: mpw:EUFmeH3cNAzdRGdOQcabWg==
3.5.3.测试 在启动项目的时候,需要把刚才生成的秘钥添加到启动参数中,像这样:
–mpw.key=6234633a66fb399f
单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:
然后随意运行一个单元测试,可以发现数据库查询正常。
4.插件功能 MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor:自动分页TenantLineInnerInterceptor:多租户DynamicTableNameInnerInterceptor:动态表名OptimisticLockerInnerInterceptor:乐观锁IllegalSQLInnerInterceptor:sql 性能规范BlockAttackInnerInterceptor:防止全表更新与删除注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
多租户,动态表名 分页,乐观锁 sql 性能规范,防止全表更新与删除 这里我们以分页插件为里来学习插件的用法。
4.1.分页插件 在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IService和BaseMapper中的分页方法都无法正常起效。 所以,我们必须配置分页插件。
4.1.1.配置分页插件 在项目中新建一个配置类:
其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
4.1.2.分页API编写一个分页查询的测试:
1 2 3 4 5 6 7 8 9 10 11 12 @Test void testPageQuery () { Page<User> p = userService.page(new Page <>(2 , 2 )); System.out.println("total = " + p.getTotal()); System.out.println("pages = " + p.getPages()); List<User> records = p.getRecords(); records.forEach(System.out::println); }
运行的SQL如下:
这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的API如下:
1 2 3 4 5 6 7 int pageNo = 1 , pageSize = 5 ;Page<User> page = Page.of(pageNo, pageSize); page.addOrder(new OrderItem ("balance" , false )); userService.page(page);
4.2.通用分页实体 现在要实现一个用户分页查询的接口,接口规范如下:
参数 说明 请求方式 GET 请求路径 /users/page 请求参数 { "pageNo": 1, "pageSize": 5, "sortBy": "balance", "isAsc": false, "name": "o", "status": 1 }返回值 { "total": 100006, "pages": 50003, "list": [ { "id": 1685100878975279298, "username": "user_9****", "info": { "age": 24, "intro": "英文老师", "gender": "female" }, "status": "正常", "balance": 2000 } ] }特殊说明 如果排序字段为空,默认按照更新时间排序排序字段不为空,则按照排序字段排序
这里需要定义3个实体:
UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件PageDTO:分页结果实体,包含总条数、总页数、当前页数据UserVO:用户页面视图实体 4.2.1.实体 由于UserQuery之前已经定义过了,并且其中已经包含了过滤条件,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 @Data @ApiModel(description = "用户查询条件实体") public class UserQuery { @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private Integer status; @ApiModelProperty("余额最小值") private Integer minBalance; @ApiModelProperty("余额最大值") private Integer maxBalance; }
其中缺少的仅仅是分页条件,而分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageQuery实体:
PageQuery是前端提交的查询参数,一般包含四个属性:
pageNo:页码pageSize:每页数据条数sortBy:排序字段isAsc:是否升序1 2 3 4 5 6 7 8 9 10 11 12 @Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty("页码") private Long pageNo; @ApiModelProperty("页码") private Long pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; }
然后,让我们的UserQuery继承这个实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 @EqualsAndHashCode(callSuper = true) @Data @ApiModel(description = "用户查询条件实体") public class UserQuery extends PageQuery { @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private Integer status; @ApiModelProperty("余额最小值") private Integer minBalance; @ApiModelProperty("余额最大值") private Integer maxBalance; }
返回值的用户实体沿用之前定一个UserVO实体:
最后,则是分页实体PageDTO:
代码如下:
1 2 3 4 5 6 7 8 9 10 @Data @ApiModel(description = "分页结果") public class PageDTO <T> { @ApiModelProperty("总条数") private Long total; @ApiModelProperty("总页数") private Long pages; @ApiModelProperty("集合") private List<T> list; }
4.2.2.开发接口 我们在UserController中定义分页查询用户的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/page") public PageDTO<UserVO> queryUsersPage (UserQuery query) { return userService.queryUsersPage(query); } }
然后在IUserService中创建queryUsersPage方法:
1 PageDTO<UserVO> queryUsersPage (PageQuery query) ;
接下来,在UserServiceImpl中实现该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override public PageDTO<UserVO> queryUsersPage (PageQuery query) { Page<User> page = Page.of(query.getPageNo(), query.getPageSize()); if (query.getSortBy() != null ) { page.addOrder(new OrderItem (query.getSortBy(), query.getIsAsc())); }else { page.addOrder(new OrderItem ("update_time" , false )); } page(page); List<User> records = page.getRecords(); if (records == null || records.size() <= 0 ) { return new PageDTO <>(page.getTotal(), page.getPages(), Collections.emptyList()); } List<UserVO> list = BeanUtil.copyToList(records, UserVO.class); return new PageDTO <UserVO>(page.getTotal(), page.getPages(), list); }
启动项目,在页面查看:
4.2.3.改造PageQuery实体 在刚才的代码中,从PageQuery到MybatisPlus的Page之间转换的过程还是比较麻烦的。
我们完全可以在PageQuery这个实体中定义一个工具方法,简化开发。 像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Data public class PageQuery { private Integer pageNo; private Integer pageSize; private String sortBy; private Boolean isAsc; public <T> Page<T> toMpPage (OrderItem ... orders) { Page<T> p = Page.of(pageNo, pageSize); if (sortBy != null ) { p.addOrder(new OrderItem (sortBy, isAsc)); return p; } if (orders != null ){ p.addOrder(orders); } return p; } public <T> Page<T> toMpPage (String defaultSortBy, boolean isAsc) { return this .toMpPage(new OrderItem (defaultSortBy, isAsc)); } public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc () { return toMpPage("create_time" , false ); } public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc () { return toMpPage("update_time" , false ); } }
这样我们在开发也时就可以省去对从PageQuery到Page的的转换:
1 2 Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
4.2.4.改造PageDTO实体 在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。
我们完全可以将其封装到PageDTO的工具方法中,简化整个过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 @Data @NoArgsConstructor @AllArgsConstructor public class PageDTO <V> { private Long total; private Long pages; private List<V> list; public static <V, P> PageDTO<V> empty (Page<P> p) { return new PageDTO <>(p.getTotal(), p.getPages(), Collections.emptyList()); } public static <V, P> PageDTO<V> of (Page<P> p, Class<V> voClass) { List<P> records = p.getRecords(); if (records == null || records.size() <= 0 ) { return empty(p); } List<V> vos = BeanUtil.copyToList(records, voClass); return new PageDTO <>(p.getTotal(), p.getPages(), vos); } public static <V, P> PageDTO<V> of (Page<P> p, Function<P, V> convertor) { List<P> records = p.getRecords(); if (records == null || records.size() <= 0 ) { return empty(p); } List<V> vos = records.stream().map(convertor).collect(Collectors.toList()); return new PageDTO <>(p.getTotal(), p.getPages(), vos); } }
最终,业务层的代码可以简化为:
1 2 3 4 5 6 7 8 9 @Override public PageDTO<UserVO> queryUserByPage (PageQuery query) { Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc(); page(page); return PageDTO.of(page, UserVO.class); }
如果是希望自定义PO到VO的转换过程,可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public PageDTO<UserVO> queryUserByPage (PageQuery query) { Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc(); page(page); return PageDTO.of(page, user -> { UserVO vo = BeanUtil.copyProperties(user, UserVO.class); String username = vo.getUsername(); vo.setUsername(username.substring(0 , username.length() - 2 ) + "**" ); return vo; }); }
最终查询的结果如下:
5.作业 尝试改造项目一中的Service层和Mapper层实现,用MybatisPlus代替单表的CRUD