目录
一、入门
二、常用注解
MybatisPlus中比较常用的几个注解如下:
IdType枚举:
使用@TableField的常见场景:
总结:
三、常见配置
MyBatisPlus使用的基本流程是什么?
四、核心功能
1、条件构造器
QueryWrapper
UpdateWrapper
LambdaQueryWrapper
总结:
自定义SQL
1、基于Wrapper构建where条件
2、在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
3、自定义SQL,并使用Wrapper条件
Service接口
新增:
删除:
修改:
Get:
List:
Count:
基本用法
总结:
MP的Service接口使用流程是怎样的?
1、自定义Service接口继承IService接口
2、自定义Service实现类,实现自定义接口并继承ServiceImpl类
基于Restful风格实现下列接口
IService开发基础业务接口:
IService开发复杂业务接口:
Lambda
批量新增
扩展功能
代码生成
安装插件
使用
静态工具
案例:静态工具查询
逻辑删除
通用枚举
定义枚举
配置枚举处理器
测试
总结:
JSON类型处理器
定义实体
使用类型处理器
插件功能
分页插件
配置分页插件
分页API
通用分页实体
实体
开发接口
改造PageQuery实体
改造PageDTO实体
MyBatis-Plus
1、引入Mybatis-plus依赖,代替Mybatis依赖(pom.xml引入)
com.baomidou
mybatis-plus-boot-starter
3.5.3.1
2、定义Mapper接口并继承BaseMapper
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
@TableName:用来指定表名
@TableId:用来指定表中的主键字段信息
@TableField:用来指定表中的普通字段信息
AUTO:数据库自增长
INPUT:通过set方法自行输入
ASSIGN_ID:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法
成员变量名与数据库字段名不一致
成员变量名以is开头,且是布尔值
成员变量名与数据库关键字冲突
成员变量不是数据库字段
MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。例如:
引入起步依赖
自定义Mapper基础BaseMapper
在实体类上添加注解声明 表信息
在application.yml中根据需要添加配置
除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where中包含的所有条件构造方法:
而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:
而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。接下来看一些例子: 查询:查询出名字中带o
的,存款大于等于1000元的人。代码如下:
@Test
void testQueryWrapper() {
// 1.构建查询条件 where name like "%o%" AND balance >= 1000
QueryWrapper wrapper = new QueryWrapper()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2.查询数据
List users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
更新:更新用户名为jack的用户的余额为2000,代码如下:
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 where name = "Jack"
QueryWrapper wrapper = new QueryWrapper().eq("username", "Jack");
// 2.更新数据,user中非null字段都会作为set语句
User user = new User();
user.setBalance(2000);
userMapper.update(user, wrapper);
}
基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例如:更新id为1,2,4
的用户的余额,扣200,对应的SQL应该是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql功能了:
@Test
void testUpdateWrapper() {
List ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper wrapper = new UpdateWrapper()
.setSql("balance = balance - 200") // SET balance = balance - 200
.in("id", ids); // WHERE id in (1, 2, 4)
// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
// 而是基于UpdateWrapper中的setSQL来更新
userMapper.update(null, wrapper);
}
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值
。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用
和Lambda
表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
LambdaQueryWrapper
LambdaUpdateWrapper
分别对应QueryWrapper和UpdateWrapper
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
@Test
void testCustomWrapper() {
// 1.准备自定义查询条件
List ids = List.of(1L, 2L, 4L);
QueryWrapper wrapper = new QueryWrapper().in("id", ids);
// 2.调用mapper的自定义方法,直接传递Wrapper
userMapper.deductBalanceByIds(200, wrapper);
}
然后在UserMapper中自定义SQL:
public interface UserMapper extends BaseMapper {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper wrapper);
}
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增
remove
:删除
update
:更新
get
:查询单个结果
list
:查询集合结果
count
:计数
page
:分页查询
save
是新增单个元素
saveBatch
是批量新增
saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增
saveOrUpdateBatch
是批量的新增或修改
removeById
:根据id删除
removeByIds
:根据id批量删除
removeByMap
:根据Map中的键值对为条件删除
remove(Wrapper
:根据Wrapper条件删除
~~removeBatchByIds~~
:暂不支持
updateById
:根据id修改
update(Wrapper
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分
update(T,Wrapper
:按照T
内的数据修改与Wrapper
匹配到的数据
updateBatchById
:根据id批量修改
getById
:根据id查询1条数据
getOne(Wrapper
:根据Wrapper
查询1条数据
getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
listByIds
:根据id批量查询
list(Wrapper
:根据Wrapper条件查询多条数据
list()
:查询所有
count()
:统计所有数量
count(Wrapper
:统计符合Wrapper
条件的数据数量
getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
由于Service
中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService
,而是自定义Service
接口,然后继承IService
以拓展方法。同时,让自定义的Service实现类
继承ServiceImpl
,这样就不用自己实现IService
中的接口了。
首先,定义IUserService
,继承IService
:
首先,我们在项目中引入几个依赖:
com.github.xiaoymin
knife4j-openapi2-spring-boot-starter
4.1.0
org.springframework.boot
spring-boot-starter-web
然后需要配置swagger信息:
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: [email protected]
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.itheima.mp.controller
然后,接口需要两个实体:
UserFormDTO:代表新增时的用户表单
UserVO:代表查询的返回结果
首先是UserFormDTO:
最后,按照Restful风格编写Controller接口方法:
UserController代码:
package cn.edu.seig.controller;
import cn.edu.seig.Service.IUserService;
import cn.edu.seig.domain.dto.UserFormDTO;
import cn.edu.seig.pojo.User;
import cn.edu.seig.vo.UserVO;
import com.fasterxml.jackson.databind.util.BeanUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class Usercontroller {
private final IUserService userService;
@ApiOperation("新增用户接口")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userFormDTO){
// 1、把DTO拷贝到PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
//2、新增
userService.save(user);
}
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){
userService.removeById(id);
}
@ApiOperation("根据id查询用户接口")
@GetMapping("{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
//1、查询用户接口
User user = userService.getById(id);
//2、把PO拷贝到VO
return BeanUtil.copyProperties(user, UserVO.class);
}
@GetMapping
@ApiOperation("根据id集合查询用户")
public List queryUserByIds(@RequestParam("ids") List ids){
// 1.查询用户
List users = userService.listByIds(ids);
// 2.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
}
可以看到上述接口都直接在controller即可实现,无需编写任何service代码,非常方便。
不过,一些带有业务逻辑的接口则需要在service中自定义实现了。例如下面的需求:
根据id扣减用户余额
这看起来是个简单修改功能,只要修改用户余额即可。但这个业务包含一些业务逻辑处理:
判断用户状态是否正常
判断用户余额是否充足
这些业务逻辑都要在service层来做,另外更新余额需要自定义SQL,要在mapper中来实现。因此,我们除了要编写controller以外,具体的业务还要在service和mapper中编写。
首先在UserController中定义一个方法:
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
userService.deductBalance(id, money);
}
然后是UserService接口
package cn.edu.seig.Service;
import cn.edu.seig.pojo.User;
import com.baomidou.mybatisplus.extension.service.IService;
public interface IUserService extends IService {
void deductBalance(Long id, Integer money);
}
最后是UserServiceImpl实现类:
package cn.edu.seig.Service.impl;
import cn.edu.seig.Service.IUserService;
import cn.edu.seig.mapper.UserMapper;
import cn.edu.seig.pojo.User;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class IUserServiceImpl extends ServiceImpl implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {
//1、查询用户
User user = getById(id);
//2、校验用户状态
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常!");
}
//3、校验余额是否充足
if (user.getBalance() < money){
throw new RuntimeException("用户余额不足!");
}
//4、扣减余额 update user set balance = balance - ?
baseMapper.deductBalance(id,money);
}
}
最后是mapper:
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);
IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
name:用户名关键字,可以为空
status:用户状态,可以为空
minBalance:最小余额,可以为空
maxBalance:最大余额,可以为空
可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。
我们首先需要定义一个查询条件实体,UserQuery实体:
package cn.edu.seig.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@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方法:
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
LambdaQueryWrapper wrapper = new QueryWrapper().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);
// 2.查询用户
List users = userService.list(wrapper);
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
在组织查询条件的时候,我们加入了 username != null
这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的
标签。这样就实现了动态查询条件效果了。
不过,上述条件构建的代码太麻烦了。 因此Service中对LambdaQueryWrapper
和LambdaUpdateWrapper
的用法进一步做了简化。我们无需自己通过new
的方式来创建Wrapper
,而是直接调用lambdaQuery
和lambdaUpdate
方法:
基于Lambda查询:
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
// 2.查询用户
List 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();
// 3.处理vo
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部分是动态的。
UserServiceImpl代码:
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3.校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4.扣减余额 update tb_user set balance = balance - ?
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance) // 更新余额
.set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance()) // 乐观锁
.update();
}
//批量新增
@Test
void testSaveBatch() {
// 准备10万条数据
List list = new ArrayList<>(1000);
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
list.add(buildUser(i));
// 每1000条批量插入一次
if (i % 1000 == 0) {
userService.saveBatch(list);
list.clear();
}
}
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
的批处理是基于PrepareStatement
的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。SQL类似这样:
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合并为一条,像这样:
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
:
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: MySQL123
再次测试插入10万条数据,可以发现速度有非常明显的提升:
在ClientPreparedStatement
的executeBatchInternal
中,有判断rewriteBatchedStatements
值是否为true并重写SQL的功能:
最终,SQL被重写了:
在使用MybatisPlus以后,基础的Mapper
、Service
、PO
代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO
、Mapper
、Service
等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus
的插件,它可以基于图形化界面完成MybatisPlus
的代码生成,非常简单。
在Idea
的plugins市场中搜索并安装MyBatisPlus
插件:
然后重启你的Idea即可使用。
在Idea顶部菜单中,找到other
,选择Config Database
:
在弹出的窗口中填写数据库连接的基本信息:
报错:The server time zone value ‘�й���ʱ��‘ is unrecognied-CSDN博客
点击OK保存。
然后再次点击Idea顶部菜单中的other,然后选择Code Generator
:
在弹出的表单中填写信息:
最终,代码自动生成到指定的位置了
有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db
,其中的一些静态方法与IService
中方法签名基本一致,也可以帮助我们实现CRUD功能:
需求:
改造根据id查询用户的接口,查询用户的同时,查询出用户对应的所有地址
改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)
修改UserController中根据id查询用户的业务接口:
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 基于自定义service方法查询
return userService.queryUserAndAddressById(userId);
}
由于查询业务复杂,所以要在service层来实现。首先在IUserService中定义方法:
package cn.edu.seig.Service;
import cn.edu.seig.pojo.User;
import cn.edu.seig.vo.UserVO;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface IUserService extends IService {
void deductBalance(Long id, Integer money);
List queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
UserVO queryUserAndAddressById(Long id);
}
然后,在UserServiceImpl中实现该方法:
@Override
public UserVO queryUserAndAddressById(Long userId) {
// 1.查询用户
User user = getById(userId);
if (user == null) {
return null;
}
// 2.查询收货地址
List addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, userId)
.list();
// 3.处理vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
return userVO;
}
在查询地址时,我们采用了Db的静态方法,因此避免了注入AddressService,减少了循环依赖的风险。
再来实现一个功能:
根据id批量查询用户,并查询出用户对应的所有地址
UserControlle代码:
@GetMapping
@ApiOperation("根据id集合查询用户")
public List queryUserByIds(@RequestParam("ids") List ids){
return userService.queryUserAndAddressByIds(ids);
}
IUserService代码:
List queryUserAndAddressByIds(List ids);
IUserServiceImpl代码:
public List queryUserAndAddressByIds(List ids) {
//1、查询用户
List users = listByIds(ids);
if (CollectionUtils.isEmpty(users)){
return Collections.emptyList();
}
//2、查询地址
//2.1.获取用户id集合
List userIds = users.stream().map(User::getId).collect(Collectors.toList());
//2.2.根据用户id查询地址
List addresses = Db.lambdaQuery(Address.class).in(Address::getUserId,userIds).list();
//2.3.转换地址VO
List addressVOS = BeanUtil.copyToList(addresses,AddressVO.class);
//2.4.用户地址集合分组处理,相同用户的放入一个集合中(组)
Map> addressMap=new HashMap<>(0);
if (CollectionUtils.isEmpty(addressVOS)){
addressMap = addressVOS.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
//3、转换VO返回
List list =new ArrayList<>(users.size());
for (User user:users){
//3.1.转换user的PO为VO
UserVO vo = BeanUtils.copyProperties(user,UserVO.class);
list.add(vo);
//3.2.转换地址VO
vo.setAddress(addressMap.get(user.getId()));
}
return list;
}
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
在表中添加一个字段标记数据是否被删除
当删除数据时把标记置为true
查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
例如,我们给address
表添加一个逻辑删除字段:
alter table address add deleted bit default b'0' null comment '逻辑删除';
然后给Address
实体添加deleted
字段:
接下来,我们要在application.yml
中配置逻辑删除字段:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
逻辑删除本身也有自己的问题,比如:
会导致数据库表垃圾数据越来越多,从而影响查询效率
SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int
类型,对应的PO也是Integer
。因此业务操作时必须手动把枚举
与Integer
转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
代码:
package cn.edu.seig.enums;
import lombok.Getter;
@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
类型:
要让MybatisPlus
处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus
,枚举中的哪个字段的值作为数据库值。 MybatisPlus
提供了@EnumValue
注解来标记枚举属性:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
可读性强!
@Test
void testService() {
List list = userService.list();
list.forEach(System.out::println);
}
最终,查询出的User
类的status
字段会是枚举类型:
同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性:
并且,在UserStatus枚举中通过@JsonValue
注解标记JSON序列化时展示的字段:
最后,在页面查询,结果如下:
如何实现PO类中的枚举类型变量与数据库字段的转换? 给枚举中的与数据库对应value值添加
1、@EnumValue注解
2、在配置文件中配置统一的枚举处理器,实现类型转换
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler
处理器。
代码:
package cn.edu.seig.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
将User类的info字段修改为UserInfo类型,并声明类型处理器:
为了让页面返回的结果也以对象格式返回,我们要修改UserVO中的info字段:
在页面查询结果如下:
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor
:自动分页
TenantLineInnerInterceptor
:多租户
DynamicTableNameInnerInterceptor
:动态表名
OptimisticLockerInnerInterceptor
:乐观锁
IllegalSQLInnerInterceptor
:sql 性能规范
BlockAttackInnerInterceptor
:防止全表更新与删除
注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
多租户,动态表名
分页,乐观锁
sql 性能规范,防止全表更新与删除
在未引入分页插件的情况下,MybatisPlus
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。 所以,我们必须配置分页插件。
代码:
package cn.edu.seig.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
@Test
void testPageQuery() {
// 1.分页查询,new Page()的两个参数分别是:页码、每页大小
Page p = userService.page(new Page<>(2, 2));
// 2.总条数
System.out.println("total = " + p.getTotal());
// 3.总页数
System.out.println("pages = " + p.getPages());
// 4.数据
List records = p.getRecords();
records.forEach(System.out::println);
}
@Test
void testPageQuery(){
int pageNo = 1,pageSize = 2;
//1、准备分页条件
//1.1分页条件
Page page = Page.of(pageNo,pageSize);
//1.2排序条件
page.addOrder(new OrderItem("balance",true));
page.addOrder(new OrderItem("id",true));
//2、分页查询
Page p =userService.page(page);
//3、解析
long total = p.getTotal();
System.out.println("total="+total);
long pages=p.getPages();
System.out.println("page="+pages);
List users = p.getRecords();
users.forEach(System.out::println);
}
这里需要定义3个实体:
UserQuery
:分页查询条件的实体,包含分页、排序参数、过滤条件
PageDTO
:分页结果实体,包含总条数、总页数、当前页数据
UserVO
:用户页面视图实体
PageQuery
是前端提交的查询参数,一般包含四个属性:
pageNo
:页码
pageSize
:每页数据条数
sortBy
:排序字段
isAsc
:是否升序
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("页码")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
让我们的UserQuery继承这个实体:
返回值的用户实体沿用之前定一个UserVO
实体:
最后,则是分页实体PageDTO:
package cn.edu.seig.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "分页结果")
public class PageDTO {
@ApiModelProperty("总条数")
private Integer total;
@ApiModelProperty("总页数")
private Integer pages;
@ApiModelProperty("集合")
private List list;
}
UserController
@ApiOperation("根据条件分页查询用户接口")
@GetMapping("/page")
public PageDTO queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
UserServiceImpl
@Override
public PageDTO queryUsersPage(PageQuery query) {
// 1.构建条件
// 1.1.分页条件
Page page = Page.of(query.getPageNo(), query.getPageSize());
// 1.2.排序条件
if (query.getSortBy() != null) {
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}else{
// 默认按照更新时间排序
page.addOrder(new OrderItem("update_time", false));
}
// 2.查询
page(page);
// 3.数据非空校验
List records = page.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 4.有数据,转换
List list = BeanUtil.copyToList(records, UserVO.class);
// 5.封装返回
return new PageDTO(page.getTotal(), page.getPages(), list);
}
在刚才的代码中,从PageQuery
到MybatisPlus
的Page
之间转换的过程还是比较麻烦的。
我们完全可以在PageQuery
这个实体中定义一个工具方法,简化开发。 像这样:
@Data
public class PageQuery {
private Integer pageNo;
private Integer pageSize;
private String sortBy;
private Boolean isAsc;
public Page toMpPage(OrderItem ... orders){
// 1.分页条件
Page p = Page.of(pageNo, pageSize);
// 2.排序条件
// 2.1.先看前端有没有传排序字段
if (sortBy != null) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
// 2.2.再看有没有手动指定排序字段
if(orders != null){
p.addOrder(orders);
}
return p;
}
public Page toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
public Page toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage("create_time", false);
}
public Page toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage("update_time", false);
}
}
这样我们在开发也时就可以省去对从PageQuery
到Page
的的转换:
// 1.构建条件
Page page = query.toMpPageDefaultSortByCreateTimeDesc();
在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。
我们完全可以将其封装到PageDTO的工具方法中,简化整个过程:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO {
private Long total;
private Long pages;
private List list;
/**
* 返回空分页结果
* @param p MybatisPlus的分页结果
* @param 目标VO类型
* @param 原始PO类型
* @return VO的分页对象
*/
public static PageDTO empty(Page p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为 VO分页结果
* @param p MybatisPlus的分页结果
* @param voClass 目标VO类型的字节码
* @param 目标VO类型
* @param 原始PO类型
* @return VO的分页对象
*/
public static PageDTO of(Page p, Class voClass) {
// 1.非空校验
List records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List vos = BeanUtil.copyToList(records, voClass);
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
/**
* 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
* @param p MybatisPlus的分页结果
* @param convertor PO到VO的转换函数
* @param 目标VO类型
* @param 原始PO类型
* @return VO的分页对象
*/
public static PageDTO of(Page p, Function
convertor) {
// 1.非空校验
List
records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List vos = records.stream().map(convertor).collect(Collectors.toList());
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
最终,业务层的代码可以简化为:
@Override
public PageDTO queryUserByPage(PageQuery query) {
// 1.构建条件
Page page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, UserVO.class);
}
如果是希望自定义PO到VO的转换过程,可以这样做:
@Override
public PageDTO queryUserByPage(PageQuery query) {
// 1.构建条件
Page page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, user -> {
// 拷贝属性到VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 用户名脱敏
String username = vo.getUsername();
vo.setUsername(username.substring(0, username.length() - 2) + "**");
return vo;
});
}