MyBatis-Plus分页功能优化:IPage转换与PageDTO增强
前言:分页功能的痛点与挑战
在日常开发中,分页查询是最常见的业务场景之一。然而,传统的分页实现往往面临诸多挑战:
- 序列化问题:分页对象在JSON序列化时产生冗余字段
- 跨服务传输:微服务架构下分页数据在不同服务间传递困难
- 类型转换:不同层之间的分页对象转换繁琐
- 配置复杂性:分页参数配置分散,难以统一管理
MyBatis-Plus作为MyBatis的增强工具,提供了强大的分页功能解决方案。本文将深入探讨IPage接口的设计理念、Page类的实现细节,以及如何通过PageDTO进行功能增强和优化。
IPage接口:分页功能的核心抽象
接口设计概览
IPage接口定义了分页功能的核心契约,提供了丰富的方法来支持各种分页场景:
public interface IPage<T> extends Serializable {
// 排序信息
List<OrderItem> orders();
// SQL优化配置
boolean optimizeCountSql();
boolean optimizeJoinOfCountSql();
// 分页计算
long offset();
long getPages();
// 数据操作
List<T> getRecords();
IPage<T> setRecords(List<T> records);
// 分页参数
long getTotal();
IPage<T> setTotal(long total);
long getSize();
IPage<T> setSize(long size);
long getCurrent();
IPage<T> setCurrent(long current);
// 类型转换
<R> IPage<R> convert(Function<? super T, ? extends R> mapper);
}
核心功能详解
1. 分页计算能力
// 计算偏移量
default long offset() {
long current = getCurrent();
if (current <= 1L) {
return 0L;
}
return Math.max((current - 1) * getSize(), 0L);
}
// 计算总页数
default long getPages() {
if (getSize() == 0) {
return 0L;
}
long pages = getTotal() / getSize();
if (getTotal() % getSize() != 0) {
pages++;
}
return pages;
}
2. 类型安全转换
@SuppressWarnings("unchecked")
default <R> IPage<R> convert(Function<? super T, ? extends R> mapper) {
List<R> collect = this.getRecords().stream().map(mapper).collect(toList());
return ((IPage<R>) this).setRecords(collect);
}
Page类:IPage接口的标准实现
类结构设计
Page类提供了IPage接口的完整实现,支持丰富的配置选项:
public class Page<T> implements IPage<T> {
// 核心数据字段
private List<T> records = Collections.emptyList();
private long total = 0;
private long size = 10;
private long current = 1;
// 配置选项
private List<OrderItem> orders = new ArrayList<>();
private boolean optimizeCountSql = true;
private boolean searchCount = true;
private boolean optimizeJoinOfCountSql = true;
private Long maxLimit;
private String countId;
}
使用示例
基础分页查询
// 创建分页对象
Page<User> page = new Page<>(1, 10);
page.addOrder(OrderItem.asc("create_time"));
// 执行分页查询
IPage<User> result = userMapper.selectPage(page,
Wrappers.<User>lambdaQuery()
.eq(User::getStatus, 1)
);
// 处理结果
List<User> users = result.getRecords();
long total = result.getTotal();
long pages = result.getPages();
高级配置选项
// 自定义分页配置
Page<User> page = new Page<>(1, 20, 0, false);
page.setOptimizeCountSql(false); // 禁用COUNT SQL优化
page.setSearchCount(false); // 不进行COUNT查询
page.setMaxLimit(100L); // 设置最大分页限制
page.setCountId("customCountQuery"); // 指定COUNT查询ID
PageDTO:解决跨服务传输难题
设计背景与问题
在微服务架构中,分页对象在不同服务间传输时面临以下问题:
- 序列化冗余:Page对象包含大量内部状态字段
- 版本兼容:服务间版本不一致导致反序列化失败
- 配置泄露:内部配置信息不应暴露给外部服务
PageDTO实现方案
PageDTO继承自Page类,主要提供Getter方法用于序列化:
public class PageDTO<T> extends Page<T> {
// 提供Getter方法用于序列化
public String getCountId() {
return super.countId();
}
public Long getMaxLimit() {
return super.maxLimit();
}
public List<OrderItem> getOrders() {
return super.orders();
}
public boolean isOptimizeCountSql() {
return super.optimizeCountSql();
}
public boolean isSearchCount() {
return super.searchCount();
}
public boolean isOptimizeJoinOfCountSql() {
return super.optimizeJoinOfCountSql();
}
}
使用场景对比
传统方式的问题
// 服务A:返回Page对象
public IPage<User> getUsers(int page, int size) {
Page<User> pageParam = new Page<>(page, size);
return userMapper.selectPage(pageParam, Wrappers.emptyWrapper());
}
// 服务B:接收Page对象(可能序列化失败)
public void processUsers(IPage<User> users) {
// 反序列化可能失败,因为Page包含内部状态
}
PageDTO解决方案
// 服务A:返回PageDTO对象
public PageDTO<User> getUsers(int page, int size) {
Page<User> pageParam = new Page<>(page, size);
IPage<User> result = userMapper.selectPage(pageParam, Wrappers.emptyWrapper());
// 转换为PageDTO进行传输
PageDTO<User> dto = new PageDTO<>();
dto.setRecords(result.getRecords());
dto.setTotal(result.getTotal());
dto.setCurrent(result.getCurrent());
dto.setSize(result.getSize());
return dto;
}
// 服务B:安全接收PageDTO
public void processUsers(PageDTO<User> users) {
// 安全反序列化,只包含必要字段
List<User> userList = users.getRecords();
long total = users.getTotal();
}
高级优化技巧
1. 分页参数统一管理
/**
* 分页参数构建器
*/
public class PageBuilder {
public static <T> Page<T> buildPage(int pageNum, int pageSize) {
return new Page<>(pageNum, pageSize);
}
public static <T> Page<T> buildPage(int pageNum, int pageSize,
String orderBy, boolean ascending) {
Page<T> page = new Page<>(pageNum, pageSize);
if (StringUtils.isNotBlank(orderBy)) {
page.addOrder(ascending ?
OrderItem.asc(orderBy) : OrderItem.desc(orderBy));
}
return page;
}
public static <T> PageDTO<T> toDTO(IPage<T> page) {
PageDTO<T> dto = new PageDTO<>();
dto.setRecords(page.getRecords());
dto.setTotal(page.getTotal());
dto.setCurrent(page.getCurrent());
dto.setSize(page.getSize());
dto.setOrders(new ArrayList<>(page.orders()));
return dto;
}
}
2. 类型安全转换工具
/**
* 分页数据转换工具
*/
public class PageConverter {
public static <T, R> IPage<R> convert(IPage<T> source,
Function<T, R> converter) {
return source.convert(converter);
}
public static <T, R> PageDTO<R> convertToDTO(IPage<T> source,
Function<T, R> converter) {
IPage<R> converted = source.convert(converter);
PageDTO<R> dto = new PageDTO<>();
dto.setRecords(converted.getRecords());
dto.setTotal(converted.getTotal());
dto.setCurrent(converted.getCurrent());
dto.setSize(converted.getSize());
return dto;
}
}
3. 响应对象封装
/**
* 统一分页响应对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
private long current;
private long size;
private long total;
private long pages;
private List<T> records;
private List<OrderItem> orders;
public static <T> PageResponse<T> of(IPage<T> page) {
return new PageResponse<>(
page.getCurrent(),
page.getSize(),
page.getTotal(),
page.getPages(),
page.getRecords(),
new ArrayList<>(page.orders())
);
}
public static <T> PageResponse<T> of(PageDTO<T> pageDTO) {
return new PageResponse<>(
pageDTO.getCurrent(),
pageDTO.getSize(),
pageDTO.getTotal(),
pageDTO.getPages(),
pageDTO.getRecords(),
new ArrayList<>(pageDTO.getOrders())
);
}
}
实战应用案例
案例1:电商商品分页查询
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductMapper productMapper;
/**
* 商品分页查询
*/
public PageResponse<ProductVO> getProducts(ProductQuery query) {
// 构建分页参数
Page<Product> page = PageBuilder.buildPage(
query.getPageNum(),
query.getPageSize(),
query.getOrderBy(),
query.isAscending()
);
// 执行查询
IPage<Product> result = productMapper.selectPage(page,
Wrappers.<Product>lambdaQuery()
.eq(Product::getStatus, 1)
.like(StringUtils.isNotBlank(query.getKeyword()),
Product::getName, query.getKeyword())
.eq(query.getCategoryId() != null,
Product::getCategoryId, query.getCategoryId())
);
// 转换为VO并返回
return PageResponse.of(
PageConverter.convertToDTO(result, this::toVO)
);
}
private ProductVO toVO(Product product) {
return ProductVO.builder()
.id(product.getId())
.name(product.getName())
.price(product.getPrice())
.image(product.getImage())
.build();
}
}
案例2:微服务间的分页数据传输
// 商品服务
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public ResponseEntity<PageResponse<ProductVO>> getProducts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
PageResponse<ProductVO> response = productService.getProducts(page, size);
return ResponseEntity.ok(response);
}
}
// 订单服务(调用商品服务)
@Service
@RequiredArgsConstructor
public class OrderService {
private final RestTemplate restTemplate;
public PageResponse<ProductVO> getAvailableProducts(int page, int size) {
String url = "http://product-service/products?page={page}&size={size}";
// 调用商品服务获取分页数据
ResponseEntity<PageResponse<ProductVO>> response = restTemplate.exchange(
url, HttpMethod.GET, null,
new ParameterizedTypeReference<PageResponse<ProductVO>>() {},
page, size
);
return response.getBody();
}
}
性能优化建议
1. COUNT查询优化
// 在明确知道不需要总数时禁用COUNT查询
Page<User> page = new Page<>(1, 10, false);
// 或者根据业务场景动态决定
public IPage<User> getUsers(boolean needTotal) {
Page<User> page = new Page<>(1, 10, needTotal);
return userMapper.selectPage(page, Wrappers.emptyWrapper());
}
2. 分页大小限制
// 防止过大的分页请求
public Page<User> createSafePage(int pageNum, int pageSize) {
int safeSize = Math.min(pageSize, 100); // 最大100条
return new Page<>(Math.max(pageNum, 1), safeSize);
}
3. 缓存策略
// 使用Redis缓存分页结果
@Cacheable(value = "users", key = "#pageNum + '-' + #pageSize")
public PageResponse<UserVO> getUsers(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
IPage<User> result = userMapper.selectPage(page, Wrappers.emptyWrapper());
return PageResponse.of(result.convert(this::toVO));
}
总结
MyBatis-Plus的分页功能通过IPage接口、Page类和PageDTO的协同工作,提供了强大而灵活的分页解决方案。关键优化点包括:
- 接口设计:IPage提供了统一的分页契约,支持各种分页场景
- 实现完善:Page类提供了丰富的配置选项和工具方法
- 传输优化:PageDTO解决了跨服务传输的序列化问题
- 类型安全:内置的convert方法支持安全的类型转换
通过合理的架构设计和最佳实践,可以构建出高性能、易维护的分页系统,满足各种复杂的业务需求。
在实际项目中,建议:
- 使用PageDTO进行跨服务数据传输
- 封装统一的分页工具类
- 实现类型安全的转换机制
- 根据业务场景优化COUNT查询
- 设置合理的分页大小限制
这些优化措施将显著提升系统的稳定性、性能和可维护性。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



