黑马MybatisPlus学习日志
MybatisPlus
想念mp的第xxx天
🌟本文档供入门以及深入学习mybatis-plus用,参考教程黑马程序员MybatisPlus教程
快速入门
入门案例


代码编写
pom文件中导入mp依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
mapper类继承mp提供的BaseMapper类(注意提供泛型来指定要操作的数据对象)

直接在测试类用继承的方法进行增删改查

功能测试
记得在配置文件中将数据库链接和用户密码改成自己的,mapper文件初始忘记添加了@Mapper注解,记得添加即可
顺利运行

常用注解
mp通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
- 默认将类名从驼峰命名转换为下划线命名方式作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表字段名
如果设计的表和类不符合上述规范,可以进行注解手动配置
注解 功能 @TableName("表名") 指定实体类对应操作的数据库表名 @TableId(value="id",type="AUTO") 指定类中主键id和对应数据库中id字段,类型指定id在数据库中是自增还是手动赋值,包括 AUTO,INPUT,ASSIGN_ID(基于雪花算法生成id,默认采用策略)@TableField 用来指定表中普通字段信息
@TableName("tb_user")
public calss User{
@TableId(type=IdType.AUTO)
private Long id;//虽然不用指定字段名,但需要指定主键字段自增长
@TableField("is_married")
private Boolean isMarried;//如果不添加注解,mp会映射到married字段
@TableField("`order`")
private Integer order;//不添加注解会直接导致sql语句报错,需要用转义字符重命名
@TableField(exist=false)
private String address;
}
📑使用@TableField的常用场景
- 成员变量名与数据库字段名不一致
- 成员变量名以is开头,并且是布尔值(mp会自动转成去掉is的下划线命名)
- 成员变量与数据库关键字冲突
- 成员变量不是数据库字段
常用配置
mybatis-plus继承了原生mybatis配置,并新增了其他配置
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 基本包路径
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,需要正确指定路径
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: assign_id # id默认策略为雪花算法生成
update-strategy: not_null # 更新策略:只更新非空字段
核心功能
条件构造器

示例
基于SelectWrapper
@Test
public void testQueryWrapper(){
Wrapper<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);
}
@Test
public void testUpdateByQueryMapper(){
//1.要更新的数据
User user = new User();
user.setBalance(2000);
//2.更新条件
Wrapper<User> wrapper = new QueryWrapper<User>().eq("username","Jack");
//3.执行更新
userMapper.update(user,wrapper);
}
基于UpdateMapper
不必像上述那样先对实体类进行更改再传入update方法中,直接通过wrapper对数据进行修改
@Test
public void testUpdateByQueryMapper(){
//1.更新条件
List<Long> ids = List.of(1L, 2L, 3L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>().setSql("balance=balance-100").in("id", ids);
//2.执行更新
userMapper.update(null,wrapper);
}
基于LambdaWrapper
通过函数式编程将写死的字段替换成通过反射获取类中对应字段,避免了硬编码,非常优雅,推荐使用
@Test
public void testLambdaQueryMapper(){
//1.选择条件
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().select(User::getId,User::getUsername,User::getBalance).like(User::getUsername,"o").ge(User::getBalance,1000);
//2.执行查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
自定义SQL
利用mp的wrapper构建完复杂的where条件语句之后,可以自己定义sql语句中剩下的部分
虽然上述wrapper可以通过setSql实现自定义sql,但不可避免会将sql语句写入service层当中,并不符合将sql语句定义到mapper层的规范,因此
示例

利用wrapper构建复杂的where条件,然后自己定义sql语句中剩下的部分
-
基于wrapper构建条件
List<Long> ids = List.of(1L,2L,4L); int amount = 200; LambdaQueryWrappper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId,ids); //自定义sql方法进行调用 userMapper.updateBalanceByIds(wrapper,amount); -
在mapper方法参数中用param注解声明wrapper变量名称,必须是
ew//Mapper void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper,@Param("amount") int amount); -
自定义sql,并使用wrapper中的条件
<update id="updaeBalanceByIds"> update tb_user set balance = balance - #{amount} ${ew.customSqlSegment} </update>
Service接口
IService接口基本用法
正所谓一时继承一时爽,一直继承一直爽,service代码也可以通过继承service接口得到简化
使用机制
mp提供了默认的实现了IService的ServiceImpl类,只需要接口继承IService,实现类继承UserServiceImpl即可
//Service接口
public interface UserService extends IService<User> {
}
//Service实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}
需要指定service对应的mapper泛型和操作实体类泛型
直接调用service自带的save方法即可快速实现插入操作
@SpringBootTest
class UserServiceImplTest {
@Autowired
private UserService userService;
@Test
public void testSaveUser(){
User user = new User();
user.setId(6L);
user.setUsername("Lilith");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userService.save(user);
}
}
IService开发基础业务接口
基于Restful实现下列接口

准备工作
导入maven依赖
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置swagger配置
# application.yml
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: zhanghuyi@itcast.cn
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
导入vo,dto和query实体类

代码开发
新增用户接口
@RestController
@RequestMapping("/users")
@Api(tags = "用户管理")
@Slf4j
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
@ApiOperation("新增用户")
public void save(@RequestBody UserFormDTO userFormDTO) {
log.info("saveUser");
//将DTO转换为PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
//调用service新增用户
userService.save(user);
}
}
📑
@RequiredArgsConstructor:lombok注解,只会自动构造器加载必要的字段,如final修饰字段,由于spring不推荐使用@Autowired字段注入,因此可以通过对注入的bean用final修饰来达到自动注入的目的ℹ️这里beanutil是hutools这个依赖包的工具类
其他简单接口
/**
* 删除用户
*/
@PostMapping("{id}")
@ApiOperation("删除用户")
public void delete(Long id) {
log.info("deleteUser");
//调用service删除用户
userService.removeById(id);
}
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 1.查询用户
User user = userService.getById(userId);
// 2.处理vo
return BeanUtil.copyProperties(user, UserVO.class);
}
@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
// 1.查询用户
List<User> users = userService.listByIds(ids);
// 2.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
一句service代码也没有写,mp是真的nb啊
复杂业务Servie接口实现
//Controller
/**
* 扣减用户余额
* @param id
* @param money
*/
@ApiOperation("扣减用户余额")
@PutMapping("/{id}/deduction/{money}")
public void deductMoneyById(
@ApiParam("用户id") @PathVariable("id") Long id,
@ApiParam("扣减金额") @PathVariable("money") Integer money) {
userService.deductBalance(id,money);
}
//Service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
/**
* 扣减用户余额
* @param id
* @param money
*/
@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.deductBalance(id,money);
}
}
//Mapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Update("update user set balance = balance - #{money} where id = #{id}")
void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
❗ 在进行业务逻辑判断的时候,推荐进行反向判断,如当用户不存在或不可用的时候,编写内部逻辑,这样可以有效避免if的嵌套
IService的Lambda语法
查询

//Controller
/**
* 根据复杂条件查询用户接口
* @param userQuery
* @return
*/
@GetMapping("/list")
@ApiOperation("根据复杂条件查询用户接口")
public List<UserVO> queryUsers(UserQuery userQuery) {
// 1.查询用户
List<User> users = userService.queryUsers(userQuery);
// 2.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
//ServiceImpl
@Override
public List<User> queryUsers(UserQuery userQuery) {
return lambdaQuery()
.like(User::getUsername, userQuery.getName())
.eq(userQuery.getStatus() != null, User::getStatus, userQuery.getStatus())
.gt(userQuery.getMinBalance() != null, User::getBalance, userQuery.getMinBalance())
.lt(userQuery.getMaxBalance() != null, User::getBalance, userQuery.getMaxBalance())
.list();
}
非常优雅的一行流代码,条件清晰,没有硬编码
更新

/**
* 扣减用户余额
* @param id
* @param money
*/
@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("余额不足");
}
// //扣减金额
// baseMapper.deductBalance(id,money);
//lambda方式实现
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();
}
上述使用lambda表达式执行过程中存在并发问题,比如两个线程同时查询到相同的余额,进行同样的扣减金额,会导致只扣减成功一份的钱,因此需要在执行更新操作之前判断余额在语句执行期间有没有改变,即乐观锁,至于乐观锁是什么,往后继续学习就能逐渐了解了
IService批量新增
不同批处理方案的对比
for循环遍历执行调用service单个插入方法
-> 每一次遍历都是发送一次网络请求给数据库,最耗时
默认采用mp基于预编译的批处理,一次性将多个数据批量传给service发送
-> 减少了网络请求次数,但是mysql底层还是一条一条执行插入语句
通过mysql设置rewriteBatchedStatements=true配置,让mysql底层实现将多个插入语句拼接为一条插入语句批量添加
-> 效率最高
通过传统mybatis的for each动态sql,同样效率最高,但耗时费力
mysql进行相关配置

url: jdbc:mysql://localhost:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=TRUE
插入10w条数据测试,上述三种方案耗时
- 210秒
- 26秒
- 6秒
扩展功能
代码生成
在使用mp的时候往往需要进行很多重复性的代码工作,例如创建实体类,创建对应的service层,mapper层等等
可以通过在项目中导入代码生成器相关依赖和相关代码来使用该功能,但是明显不怎么方便,因此这里可以采用idea插件,这里使用的是MybatisX,不是老师选择的MybatisPlus插件(因为自己在使用的时候出现了故障)


可以看到自动生成了实体类,mapper层接口和xml,service层接口和实现类

静态工具

mp提供了
Db工具类,里面封装了大量的静态方法(IService中的方法不是静态的,因此没有被封装到里面)和之前直接通过继承方式使用的方法不同的是,需要提供实体类的字节码来告诉工具类操作的实体类对象(对于save和update这种直接传入对象的当然就不必了)
❓为什么需要另提供一个工具类实现已有的方法
-> 可以避免循环依赖的发生,例如一个业务需要查询两张表的信息,传统的实现需要对应的两个service层互相注入对方的service类实现方法,而如果使用Db提供的静态方法则不必注入就可以实现功能,避免了互相依赖的情况发生
案例

需求1
导入AddressVO(在资料中提供,之前已经导入过了)
在UserVO添加用户列表字段
@ApiModelProperty("用户收货地址")
private List<AddressVO> addressList;
改造UserController的根据id查询用户方法
/**
* 根据id查询用户
* @param userId
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询用户及收货地址")
public UserVO queryUserById(@PathVariable("id") Long userId){
return userService.queryUserAndAddressById(userId);
}
//ServiceImpl
@Override
public UserVO queryUserAndAddressById(Long userId) {
//查询用户
User user = getById(userId);
if(user == null || user.getStatus() == 2){
throw new RuntimeException("用户不存在或已被禁用");
}
//查询地址
List<Address> addressList = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, userId)
.list();
//封装vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
if(CollUtil.isEmpty(addressList)){
userVO.setAddressList(BeanUtil.copyToList(addressList, AddressVO.class));
}
return userVO;
}
需求2
//UserController
/**
* 根据id集合查询用户
* @param ids
* @return
*/
@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
return userService.queryUserAndAddressByIds(ids);
}
//ServiceImpl
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
//查询用户
List<User> users = lambdaQuery()
.in(User::getId, ids)
.list();
if(CollUtil.isEmpty(users)){
throw new RuntimeException("用户不存在");
}
//查询地址
//获取用户id集合(传递过来的id可能不存在,因此要重新查询)
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Address> addressList = Db.lambdaQuery(Address.class)
.in(Address::getUserId, userIds)
.list();
Map<Long, List<AddressVO>> addressMap = new HashMap<>();
//地址转换vo和分组
if (CollUtil.isNotEmpty(addressList)) {
//转换vo
List<AddressVO> addressVOList = BeanUtil.copyToList(addressList, AddressVO.class);
//梳理地址集合,相同用户的地址放到一起(分组)
addressMap = addressVOList.stream()
.collect(Collectors.groupingBy(AddressVO::getUserId));
}
//转换用户Vo
List<UserVO> userVOList = BeanUtil.copyToList(users, UserVO.class);
//封装地址
for(UserVO userVO : userVOList){
userVO.setAddressList(addressMap.get(userVO.getId()));
}
return userVOList;
}
采用一口气查询所有id的全部地址,然后后续在java代码中进行分组再封装到userVO集合中,相比遍历每个id分别查询对应地址集合效率要高许多
这里写了两个循环分别处理userVO转换和封装地址,养成一个循环只负责一件事情的好习惯
功能测试顺利

需求三就不做了,之前敲了挺多代码了,偷懒一下下
逻辑删除
例如淘宝购物中删除订单,就是一种
逻辑删除,,即基于代码模拟删除,实质上数据库并不会删除这条数据,只需要为数据设置一个字段用于标记是否被删除,所以逻辑删除本质上是更新操作
如果添加了逻辑删除的话,意味着每一条查询和修改语句都需要判断该数据是否被删除,如果手动实现的话会非常繁琐,因此可以直接在mp进行相关配置
mybatis-plus: global-config: db-config: logic-delete-field: deleted # 配置逻辑删除字段
可以看到发送sql语句的时候自动添加了条件判断

逻辑删除本身也有一定的问题
- 会导致数据库表垃圾数据越来越多,英系那个查询效率
- sql中全部需要对逻辑删除字段做判断,英系那个查询效率
因此老师个人并不推荐这种方法,可以通过将删除的数据迁移到类似回收站的另一个表中而不影响主表查询
枚举处理器
例如status状态用不同数字存储,可以用枚举类罗列出来
相应地,存储字段应当使用枚举类类型,而不是整形
(相较于实体类存储常量的方式,枚举类在进行比较的时候可以使用
==,而不必使用equal方法)
但是当初在数据库设计的status依然使用数字存储,这就涉及到了数据库类型和java类型的转换,这种转换是用过mybatis的
TypeHandler来实现的
但原版枚举类转换支持的并不太好,因此使用mp提供的
MybatisEnumTypeHandler(mp还提供了一个AbstractJsonTypeHandler用于json类型)
配置全局枚举处理器
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
配置枚举类并在枚举类上要转成相应类型的字段通过注解告诉mp
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结")
;
@EnumValue
@JsonValue
private final int value;//数据库真正存储的值
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
@JsonValue是SpringMVC的注解,用于指定返回前端的json数据的value显示什么字段的值,默认返回枚举项名字,即
NORMAL或FROZEN
修改User的status字段为UserStatus枚举类类型(可以通过重构中的类型迁移来实现)
将UserServiceImpl中对应数字替换成枚举类
修改UserVO的status字段类型
JSON处理器
用来解决Json数据和java类型之间的转换
例如user表中存储的info字段,里面存储的是json格式数据,原始使用java的string类型进行接收,但如果想要获取info中的某一个属性,需要进行繁琐的转换,因此可以用实体类来接收这个字段
mp的json处理器并不支持全局配置,因此只能通过注解方式单个配置
创建UserInfo实体类对象
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
改用UserInfo对象接收info字段,并设置autoResultMap=true
@Data
@TableName(autoResultMap = true)
public class User {
/**
* 用户id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 注册手机号
*/
private String phone;
/**
* 详细信息
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
/**
* 使用状态(1正常 2冻结)
*/
private UserStatus status;
/**
* 账户余额
*/
private Integer balance;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
ResultMap是用于映射数据库查询结果的一种配置方式。它定义了如何将数据库查询结果中的列映射到 Java 对象的属性上
修改对应的UserVO字段
插件功能
mp提供了多种实用插件
分页插件
入门使用
首先注册插件(本质上是一个拦截器)
//config.MybatisConfig
//mybatis总的拦截器配置类
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//创建分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor((DbType.MYSQL));
paginationInnerInterceptor.setMaxLimit(1000L);
//添加分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
接着就可以使用分页API了(就在IService提供的方法中)
@Test
public void testPageQuery(){
int pageNo =1,pageSize = 2;
//准备分页条件
Page<User> page = Page.of(pageNo, pageSize);
page.addOrder(new OrderItem("balance",true));//按照balance字段升序排列
page.addOrder(new OrderItem("id",true));
//分页查询
Page<User> queriedPage = (Page<User>) Db.page(page,User.class);
//解析
long total = queriedPage.getTotal();
System.out.println("total = "+ total);
long pages = queriedPage.getPages();
System.out.println("pages = "+ pages);
List<User> users = queriedPage.getRecords();
users.forEach(System.out::println);
}
如果测试报错无法加载userMapper.xml的话,直接把xml里面动态sql语句都注释掉,保留外部的mapper 标签即可(我也不清楚为什么,不过反正xml动态sql也不会再用了,注释掉也没什么影响)
通用分页实体
创建PageQuery通用实体类
@Data
@ApiModel(description = "分页查询实体类")
public class Pagequery{
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("页面大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
因为分页查询很常用,虽然userQuery中包含了重复的字段,还是建议另创建一个新的实体类
然后让UserQuery继承Pagequery即可
然后定义返回结果封装类PageDTO
@Data
@ApiModel(descption = "分页结果")
public class PageDTO<T>{
@ApiModelProperty("总条数")
private Integer total;
@ApiModelProperty("总页数")
private Integer pages;
@ApiModelProperty("记录集合")
private List<T> list;
}
虽然之前苍穹外卖项目结果返回前端,所以叫做VO,但是PageQuery结果往往是用于提供给多种不同服务(尤其在微服务项目当中),因此推荐使用DTO这个命名
实现业务逻辑
//Controller
/**
* 分页查询用户
* @param query
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询用户")
public PageDTO<UserVO> queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
//Serivce
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
//构建page
Page<User> page = Page.of(query.getPageNo(),query.getPageSize());
//排序条件
if(StrUtil.isNotBlank(query.getSortBy())){
page.addOrder(new OrderItem(query.getSortBy(),query.getIsAsc()));
}else{
//默认按照更新时间排序
page.addOrder(new OrderItem("update_time",false));
}
String name = query.getName();
Integer status = query.getStatus();
Page<User> queriedPage = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status) .gt(query.getMinBalance() != null,User::getBalance,query.getMinBalance())
.lt(query.getMinBalance() != null,User::getBalance,query.getMaxBalance())
.page(page);
//获取当页数据
List<User> records =
CollUtil.isEmpty(queriedPage.getRecords()) ?
Collections.emptyList() :
queriedPage.getRecords();
List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);
//封装VO结果
PageDTO<UserVO> dto = PageDTO.<UserVO>builder()
.total(queriedPage.getTotal())
.pages(queriedPage.getPages())
.list(userVOList)
.build();
return dto;
}
这里使用了lombok提供的builder方法,需要在PageDTO类上添加@Builder注解,同时注意使用builder方法时泛型添加的位置
通用分页实体与mp转换
在进行分页查询的时候,对于创建page,创建排序,获取page数据这部分代码是重复的,因此可以封装成一个实体类

创建page和设置order部分封装在PageQuery类中
@Data
public class PageQuery {
private Integer pageNo = 1;
private Integer pageSize = 5;
private String sortBy;
private Boolean isAsc = true;
public <T> Page<T> toMpPage(OrderItem ... orders){
// 1.分页条件
Page<T> p = Page.of(pageNo, pageSize);
// 2.排序条件
// 2.1.先看前端有没有传排序字段
if (StrUtil.isNotBlank(sortBy)) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
// 2.2.再看有没有手动指定排序字段
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);
}
}
然后将封装查询返回的page结果代码封装到pageDTO中
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PageDTO<V> {
private Long total;
private Long pages;
private List<V> list;
/**
* 返回空分页结果
* @param p MybatisPlus的分页结果
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> empty(Page<P> p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为 VO分页结果
* @param p MybatisPlus的分页结果
* @param voClass 目标VO类型的字节码
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> 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 <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
方法传入泛型无法自动通过泛型获取该类的字节码,因此需要调用者传入对应类的字节码作为另一个参数
传入lambda函数(Function Convertor)可以自定义PO到VO的规则,用stream流map映射
最后简化的代码,优雅,非常的优雅
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
//构建page(没有排序条件时,默认按照更新时间降序排序)
Page<User> page = StrUtil.isNotBlank(query.getSortBy()) ? query.toMpPage() : query.toMpPageDefaultSortByUpdateTimeDesc();
//分页查询
Page<User> queriedPage = lambdaQuery()
.like(query.getName() != null, User::getUsername, query.getName())
.eq(query.getStatus() != null, User::getStatus, query.getStatus())
.gt(query.getMinBalance() != null,User::getBalance,query.getMinBalance())
.lt(query.getMinBalance() != null,User::getBalance,query.getMaxBalance())
.page(page);
//获取当页数据
return PageDTO.of(queriedPage,
user ->{
//拷贝基础属性
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
//用户名模糊处理
userVO.setUsername(StrUtil.hide(userVO.getUsername(),1,userVO.getUsername().length() - 1));
//返回
return userVO;
});
}
功能测试正常










浙公网安备 33010602011771号