大家在日常开发中应该能发现,单表的CRUD功能代码重复度很高,也没有什么难度。而这部分代码量往往比较大,开发起来比较费时。
因此,目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus.
官方网站: MyBatis-Plus
快速入门 HelloWorld 首先我们通过一个入门案例来感受MybatisPlus的魅力
准备数据库表和表对应的实体类
1 2 3 4 5 6 7 8 9 10 11 create database mp;create table user ( id bigint auto_increment primary key, name varchar (32 ) not null , password varchar (32 ) not null , age int not null , tel varchar (32 ) not null , ); insert into
1 2 3 4 5 6 7 8 9 10 11 12 @Data @Builder @NoArgsConstructor @AllArgsConstructor @TableName("user") public class User { private Long id; private String name; private String password; private Integer age; private String tel; }
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: root logging: level: com.example: debug pattern: dateformat: HH:mm:ss
引入MybatisPlus的依赖
MybatisPlus提供了starter,实现了自动Mybatis以及MybatisPlus的自动装配功能,坐标如下:
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.4</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.4</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 >
定义Mapper
为了简化单表CRUD,MybatisPlus提供了一个基础的BaseMapper
接口,其中已经实现了单表的CRUD:
因此我们自定义的Mapper只要继承了这个BaseMapper
,就无需自己实现单表CRUD了。
1 public interface UserMapper extends BaseMapper <User> {}
测试
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 @SpringBootTest public class Test1 { @Autowired private UserMapper userMapper; @Test public void testSelectList () { QueryWrapper<User> wrapper = new QueryWrapper <User>(); wrapper.lambda().lt(User::getAge, 10 ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } @Test public void testInsert () { User user = new User (); user.setAge(18 ); user.setName("李四" ); user.setStatus(UserStatus.NORMAL); user.setInfo(UserInfo.builder() .gender("男" ) .hobby("学习" ) .salary(5000 ).build()); user.setPassword("123456" ); user.setTel("123546" ); userMapper.insert(user); } @Test public void testUpdate () { userMapper.updateById(User.builder() .id(8L ) .age(10 ) .status(UserStatus.NORMAL) .build()); } @Test public void testUpdate1 () { User user1 = userMapper.selectById(8L ); User user2 = userMapper.selectById(8L ); user1.setName("张二" ); user2.setName("张四" ); userMapper.updateById(user1); userMapper.updateById(user2); } @Test public void testGetById () { userMapper.selectById(8L ); } @Test public void testDelete () { userMapper.deleteById(9L ); } }
常见注解 在刚刚的入门案例中,我们仅仅引入了依赖,继承了BaseMapper就能使用MybatisPlus,非常简单。但是问题来了: MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢?
大家回忆一下,UserMapper在继承BaseMapper的时候指定了一个泛型:
泛型中的User就是与数据库对应的实体类.
MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:
MybatisPlus会把PO实体的类名驼峰转下划线 作为表名
MybatisPlus会把PO实体的所有变量名驼峰转下划线 作为表的字段名,并根据变量类型推断字段类型
MybatisPlus会把名为id的字段作为主键
但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解 便于我们声明表信息。
@TableName
描述:表名注解,标识实体类对应的数据库表
使用位置:实体类
示例:
1 2 3 4 5 @TableName("user") public class User { private Long id; private String name; }
TableName注解除了指定表名以外,还可以指定很多其它属性:
属性
类型
必须指定
默认值
描述
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
@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
:手动生成id
ASSIGN_ID
:雪花算法生成Long
类型的全局唯一id,这是默认的ID策略
@TableField
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data @Builder @NoArgsConstructor @AllArgsConstructor @TableName("user") public class User { @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; @TableField(select = false) private String password; private Integer age; private String tel; }
一般情况下我们并不需要给字段添加@TableField
注解,一些特殊情况除外:
成员变量名与数据库字段名不一致
成员变量是以isXXX
命名,按照JavaBean
的规范,**MybatisPlus
识别字段时会把is
去除**,这就导致与数据库不符。
成员变量名与数据库一致,但是与数据库的关键字冲突 。使用@TableField
注解给字段名添加````转义
支持的其它属性如下:
常见配置 MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:
https://www.baomidou.com/pages/56bac0/#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
1 2 3 4 5 mybatis-plus: type-aliases-package: com.example.popj.entity 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 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.example.mapper.UserMapper" > <select id ="queryById" resultType ="com.example.popj.entity.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); }
核心功能 刚才讲的都是以id为条件的简单CRUD,一些复杂的SQL语句就需要用到更高级的功能了
条件构造器 除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where中包含的所有条件构造方法:
而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:
而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:
接下来,我们就来看看如何利用Wrapper
实现复杂查询。
QueryWrapper 无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。接下来看一些例子:
查询 :查询出名字中带o
的,年龄大于等于15岁的人。代码如下:
1 2 3 4 5 6 7 8 9 10 11 @Test void testQueryWrapper () { QueryWrapper<User> wrapper = new QueryWrapper <User>() .select("id" , "name" , "age" , "tel" ) .like("name" , "o" ) .ge("age" , 15 ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
更新 :更新用户名为jack的用户的年龄为20,代码如下:
1 2 3 4 5 6 7 8 9 @Test void testUpdateByQueryWrapper () { QueryWrapper<User> wrapper = new QueryWrapper <User>().eq("name" , "Jack" ); User user = new User (); user.setBalance(2000 ); userMapper.update(user, wrapper); }
UpdateWrapper 基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新id为1,2,4
的用户的年龄,扣2,对应的SQL应该是:
1 UPDATE user SET age = age - 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("age = age - 2" ) .in("id" , ids); userMapper.update(null , wrapper); }
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 @Test void testLambdaQueryWrapper () { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper <>(); wrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance) .like(User::getName, "o" ) .ge(User::getAge, 15 ); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
自定义SQL 在演示UpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:
其中有setSql("age = age - 2")
这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。
这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。
所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL
基本用法 以当前案例来说,我们可以这样写:
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("age") int age, @Param("ew") QueryWrapper<User> wrapper) ; }
这里必须写成”ew”,如果你忘了可以写Constants.WRAPPER
多表关联 理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。
例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户 要是自己基于mybatis实现SQL,大概是这样的:
1 2 3 4 5 6 7 8 9 10 <select id ="queryUserByIdAndAddr" resultType ="com.itheima.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.itheima.mp.domain.po.User" > SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment} </select >
Service接口 MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增
remove
:删除
update
:更新
get
:查询单个结果
list
:查询集合结果
count
:计数
page
:分页查询
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
时可以用这个方法获取到Mapper
List:
listByIds
:根据id批量查询
list(Wrapper<T>)
:根据Wrapper条件查询多条数据
list()
:查询所有
Count :
count()
:统计所有数量
count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper : 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
基本用法 由于Service
中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService
,而是自定义Service
接口,然后继承IService
以拓展方法。同时,让自定义的Service实现类
继承ServiceImpl
,这样就不用自己实现IService
中的接口了。
首先,定义IUserService
,继承IService
:
1 2 3 4 5 6 7 8 package com.itheima.mp.service;import com.baomidou.mybatisplus.extension.service.IService;import com.itheima.mp.domain.po.User;public interface IUserService extends IService <User> { }
然后,编写UserServiceImpl
类,继承ServiceImpl
,实现UserService
:
1 2 3 @Service public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements UserService {}
项目结构如下:
接下来,我们快速实现下面4个接口:
编号
接口
请求方式
请求路径
请求参数
返回值
1
新增用户
POST
/users
用户表单实体
无
2
删除用户
DELETE
/users/{id}
用户id
无
3
根据id查询用户
GET
/users/{id}
用户id
用户VO
4
根据id批量查询
GET
/users
用户id集合
用户VO集合
微服务框架
-
空间目录
-
day01-MybatisPlus
-
day02-Docker
-
day03-微服务01
-
微服务拆分作业参考
-
day04-微服务02
-
day05-服务保护和分布式事务
-
day06-MQ基础
-
day07-MQ高级
-
day08-Elasticsearch
-
day09-Elasticsearch02
-
day10-Redis面试篇
-
day11-微服务面试篇
-
day01-MybatisPlus
最新修改时间为10月12日
登录/注册
day01-MybatisPlus
3596
8010
13
2
接下来,我们快速实现下面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 12 13 14 15 16 17 <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 > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 5.8.22</version > </dependency >
然后需要配置swagger信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 knife4j: enable: true openapi: title: 用户管理接口文档 description: "用户管理接口文档" email: email concat: jiahao url: jiahao.love version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.example.controller
然后,接口需要两个实体:
UserDTO
:代表新增时的用户表单
UserVO
:代表查询的返回结果
首先是UserDTO
:
1 2 3 4 5 6 7 8 9 10 11 12 @Data @ApiModel(description = "用户表单实体") public class UserDTO { @ApiModelProperty("姓名") private String name; @ApiModelProperty("密码") private String password; @ApiModelProperty("年龄") private Integer age; @ApiModelProperty("电话") private String tel; }
然后是UserVO:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Data @ApiModel(description = "用户VO实体") public class UserVO { @ApiModelProperty("id") private Long id; @ApiModelProperty("姓名") private String name; @ApiModelProperty("密码") private String password; @ApiModelProperty("年龄") private Integer age; @ApiModelProperty("电话") private String tel; @ApiModelProperty("状态 (0 冻结 1 正常)") private UserStatus status; @ApiModelProperty("地址") private String address; }
最后,按照Restful风格编写Controller接口方法:
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 @Slf4j @RestController @Api(tags = "用户相关接口") @RequestMapping("/users") public class UserController { @Autowired private UserIService userIService; @GetMapping("/{id}") @ApiOperation("根据id查询用户") public Result<UserVO> getById (@PathVariable Long id) { return Result.success(userIService.selectById(id)); } @GetMapping @ApiOperation("根据id批量查询") public Result<List<UserVO>> getByIds (@RequestParam("ids") List<Long> ids) { return Result.success(userIService.selectBathIds(ids)); } @PostMapping @ApiOperation("新增用户") public Result save (@RequestBody UserDTO userDTO) { User user = new User (); BeanUtils.copyProperties(userDTO, user); userIService.save(user); return Result.success(); } @DeleteMapping("/{id}") @ApiOperation("删除用户") public Result delete (@PathVariable Long id) { userIService.removeById(id); return Result.success(); } }
可以看到上述接口都直接在controller即可实现,无需编写任何service代码,非常方便。
不过,一些带有业务逻辑的接口则需要在service中自定义实现了。例如下面的需求:
这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:
这些业务逻辑都要在service层来做,另外更新余额需要自定义SQL,要在mapper中来实现。因此,我们除了要编写controller以外,具体的业务还要在service和mapper中编写。
首先在UserController中定义一个方法:
1 2 3 4 5 6 @PutMapping("/{id}/deduction/{age}") @ApiOperation("根据id减少年龄") public Result deductBalance (@PathVariable("id") Long id , @PathVariable("age") Integer age) { userIService.deductAge(id, age); return Result.success(); }
然后是UserService接口:
1 2 3 4 5 6 void deductAge (Long id, Integer age) ;
最后是UserServiceImpl实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public void deductAge (Long id, Integer age) { User user = userMapper.selectById(id); if (user.getStatus().equals(0 )) throw new RuntimeException ("账户异常" ); if (user.getAge() <= age) { user.setStatus(UserStatus.FREEZE); } lambdaUpdate().set(User::getAge, user.getAge() - age) .set(User::getStatus, user.getStatus()) .eq(User::getId, id) .eq(User::getAge, user.getAge()) .update(); }
Lambda IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
name:用户名关键字,可以为空
status:用户状态,可以为空
minAge:最小年龄,可以为空
maxAge:最大年龄,可以为空
可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。
我们首先需要定义一个查询条件实体,UserQueryDTO实体:
1 2 3 4 5 6 7 8 9 10 11 12 @Data @ApiModel(description = "用户条件查询实体") public class UserQueryDTO { @ApiModelProperty("姓名") private String name; @ApiModelProperty("状态 (1 正常 0 冻结)") private Integer status; @ApiModelProperty("最小年龄") private Integer minAge; @ApiModelProperty("最大年龄") private Integer maxAge; }
接下来我们在UserController中定义一个controller方法:
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/queryList") @ApiOperation("条件查询用户") public Result<List<UserVO>> queryList (UserQueryDTO userQueryDTO) { List<User> list = userIService.lambdaQuery() .like(userQueryDTO.getName() != null , User::getName, userQueryDTO.getName()) .eq(userQueryDTO.getStatus() != null , User::getStatus, userQueryDTO.getStatus()) .ge(userQueryDTO.getMinAge() != null , User::getAge, userQueryDTO.getMinAge()) .le(userQueryDTO.getMaxAge() != null , User::getAge, userQueryDTO.getMaxAge()) .list(); return Result.success(BeanUtil.copyToList(list, UserVO.class)); }
在组织查询条件的时候,我们加入了 name != null
这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>
标签。这样就实现了动态查询条件效果了。
可以发现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 @Override public void deductAge (Long id, Integer age) { User user = userMapper.selectById(id); if (user.getStatus().equals(0 )) throw new RuntimeException ("账户异常" ); if (user.getAge() <= age) { user.setStatus(UserStatus.FREEZE); } lambdaUpdate().set(User::getAge, user.getAge() - age) .set(User::getStatus, user.getStatus()) .eq(User::getId, id) .eq(User::getAge, user.getAge()) .update(); }
这里的UserStatus为枚举类,FREEZE代表冻结
批量新增 IService中的批量新增功能使用起来非常方便,但有一点注意事项,我们先来测试一下。 首先我们测试逐条插入数据:
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; }
执行结果如下:
可以看到速度非常慢。
然后再试试MybatisPlus的批处理:
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
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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)); } 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语句,逐条插入数据。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合并为一条,像这样:
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 );
该怎么做呢?
MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements
。顾名思义,就是重写批处理的statement
语句。参考文档:
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements
这个参数的默认值是false,我们需要修改连接参数,将其配置为true
修改项目中的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: root password: root
再次测试插入10万条数据,可以发现速度有非常明显的提升:
在ClientPreparedStatement
的executeBatchInternal
中,有判断rewriteBatchedStatements
值是否为true并重写SQL的功能:
最终,SQL被重写了:
扩展功能 代码生成 在使用MybatisPlus以后,基础的Mapper
、Service
、PO
代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO
、Mapper
、Service
等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus
的插件,它可以基于图形化界面完成MybatisPlus
的代码生成,非常简单。
安装插件 在Idea
的plugins市场中搜索并安装MyBatisPlus
插件:
然后重启你的Idea即可使用。
使用 刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other
,选择Config Database
:
在弹出的窗口中填写数据库连接的基本信息:
点击OK保存。
然后再次点击Idea顶部菜单中的other,然后选择Code Generator
:
在弹出的表单中填写信息:
最终,代码自动生成到指定的位置了
静态工具 有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db
,其中的一些静态方法与IService
中方法签名基本一致,也可以帮助我们实现CRUD功能:
3.5.4之后有这个工具类Db
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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::getName, "o" ) .ge(User::getAge, 15 ) .list(); list.forEach(System.out::println); } @Test void testDbUpdate () { Db.lambdaUpdate(User.class) .set(User::getAge, 20 ) .eq(User::getUsername, "Rose" ); }
逻辑删除 对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
在表中添加一个字段标记数据是否被删除
当删除数据时把标记置为true
查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意 ,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
例如,我们给address
表添加一个逻辑删除字段:
1 alter table user add status int default 1 null comment '状态' ;
然后给User
实体添加status
字段:
1 2 @TableLogic private UserStatus status;
接下来,我们要在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中全都需要对逻辑删除字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
通用枚举 User类中有一个用户状态字段:
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int
类型,对应的PO也是Integer
。因此业务操作时必须手动把枚举
与Integer
转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换 。
定义枚举 我们定义一个用户状态的枚举:
1 2 3 4 5 6 7 8 9 10 11 @AllArgsConstructor @Getter public enum UserStatus { NORMAL(1 , "正常" ), FREEZE(0 , "冻结" ); @EnumValue private final Integer value; @JsonValue private final String desc; }
然后把User
类中的status
字段改为UserStatus
类型:
要让MybatisPlus
处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus
,枚举中的哪个字段的值作为数据库值。 MybatisPlus
提供了@EnumValue
注解来标记枚举属性:
配置枚举处理器 在application.yaml文件中添加配置:
1 2 3 mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
测试 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序列化时展示的字段:
最后,在页面查询,结果如下:
JSON类型处理器 数据库的user表中有一个info
字段,是JSON类型:
而目前User
实体类中却是String
类型:
这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map
或者实体类。
而一旦我们把info
改为对象
类型,就需要在写入数据库时手动转为String
,再读取数据库时,手动转换为对象
,这会非常麻烦。
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler
处理器。
接下来,我们就来看看这个处理器该如何使用。
定义实体 首先,我们定义一个单独实体类来与info字段的属性匹配:
1 2 3 4 5 6 7 8 9 @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UserInfo { private Integer salary; private String hobby; private String gender; }
使用类型处理器 接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:
1 2 @TableField(typeHandler = JacksonTypeHandler.class) private UserInfo info;
测试可以发现,所有数据都正确封装到UserInfo当中了:
插件功能 MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor
:自动分页
TenantLineInnerInterceptor
:多租户
DynamicTableNameInnerInterceptor
:动态表名
OptimisticLockerInnerInterceptor
:乐观锁
IllegalSQLInnerInterceptor
:sql 性能规范
BlockAttackInnerInterceptor
:防止全表更新与删除
注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
多租户,动态表名
分页,乐观锁
sql 性能规范,防止全表更新与删除
这里我们以分页插件为里来学习插件的用法。
配置分页插件 在项目中新建一个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor ()); return interceptor; } }
分页API 编写一个分页查询的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test void testSelectPage () { Page<User> page = new Page <>(1 , 3 ); page.addOrder(new OrderItem ("age" , false )); userMapper.selectPage(page, null ); System.out.println("当前页码值:" +page.getCurrent()); System.out.println("每页显示数:" +page.getSize()); System.out.println("总页数:" +page.getPages()); System.out.println("总条数:" +page.getTotal()); System.out.println("当前页数据:" +page.getRecords()); }
这里用到了分页参数,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 ("age" , false )); userService.page(page);