本文将深入剖析 MyBatis PageHelper 分页插件中 PageInfo 的封装机制,通过实际代码示例展示如何构建优雅的分页响应对象,并分享在复杂业务场景下的最佳实践技巧。
02|PageInfo 的核心价值:为什么需要二次封装?
在实际项目开发中,我们经常会遇到这样的场景:
// 直接返回分页结果的问题
@GetMapping("/users")
public List<User> getUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userService.selectAllUsers();
return users; // 只返回了数据列表,缺少分页信息
}这种方式虽然简单,但前端无法获取总记录数、总页数等关键分页信息。PageInfo 的封装就是为了解决这个痛点,它提供了完整的分页数据包装,让前后端数据交互更加规范。
使用 TRAE IDE 开发时,其智能代码补全功能可以快速生成 PageInfo 的封装代码,大大提升开发效率。
03|PageInfo 数据结构深度解析
PageInfo 是 PageHelper 提供的分页信息封装类,其核心数据结构如下:
public class PageInfo<T> {
// 当前页
private int pageNum;
// 每页数量
private int pageSize;
// 总记录数
private long total;
// 总页数
private int pages;
// 结果集
private List<T> list;
// 是否为第一页
private boolean isFirstPage;
// 是否为最后一页
private boolean isLastPage;
// 导航页码数
private int navigatePages;
// 所有导航页号
private int[] navigatepageNums;
// 导航条上的第一页和最后一页
private int navigateFirstPage;
private int navigateLastPage;
// 前一页和后一页的页号
private int prePage;
private int nextPage;
}在 TRAE IDE 中,通过其强大的代码导航功能,我们可以快速查看 PageInfo 的源码实现,深入理解每个字段的含义和用途。
04|基础封装实践:构建统一分页响应
4.1 创建统一分页响应对象
首先,我们定义一个统一的分页响应对象:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
// 响应码
private int code;
// 响应消息
private String message;
// 分页数据
private PageData<T> data;
@Data
@Builder
public static class PageData<T> {
// 当前页码
private int currentPage;
// 每页条数
private int pageSize;
// 总记录数
private long total;
// 总页数
private int totalPages;
// 数据列表
private List<T> records;
// 是否有下一页
private boolean hasNext;
// 是否有上一页
private boolean hasPrevious;
}
}4.2 封装 PageHelper 工具类
创建一个工具类来简化 PageInfo 的封装过程:
@Slf4j
public class PageHelperUtil {
/**
* 执行分页查询并封装结果
*/
public static <T> PageResult<T> doPage(int pageNum, int pageSize, Supplier<List<T>> queryFunction) {
try {
// 设置分页参数
PageHelper.startPage(pageNum, pageSize);
// 执行查询
List<T> list = queryFunction.get();
// 封装分页信息
PageInfo<T> pageInfo = new PageInfo<>(list);
// 构建响应对象
return buildPageResult(pageInfo);
} catch (Exception e) {
log.error("分页查询失败", e);
throw new BusinessException("分页查询失败");
} finally {
// 清理 ThreadLocal,防止内存泄漏
PageHelper.clearPage();
}
}
/**
* 构建分页响应结果
*/
private static <T> PageResult<T> buildPageResult(PageInfo<T> pageInfo) {
return PageResult.<T>builder()
.code(200)
.message("success")
.data(PageResult.PageData.<T>builder()
.currentPage(pageInfo.getPageNum())
.pageSize(pageInfo.getPageSize())
.total(pageInfo.getTotal())
.totalPages(pageInfo.getPages())
.records(pageInfo.getList())
.hasNext(pageInfo.isHasNextPage())
.hasPrevious(pageInfo.isHasPreviousPage())
.build())
.build();
}
}在 TRAE IDE 中编写这段代码时,其智能提示功能可以自动补全泛型类型和方法调用,减少编码错误。
4.3 实际应用示例
在 Service 层使用封装后的分页工具:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 分页查询用户列表
*/
public PageResult<UserDTO> getUserPageList(int pageNum, int pageSize, UserQueryParam param) {
return PageHelperUtil.doPage(pageNum, pageSize, () -> {
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(param.getUsername())) {
wrapper.like(User::getUsername, param.getUsername());
}
if (StringUtils.isNotBlank(param.getEmail())) {
wrapper.like(User::getEmail, param.getEmail());
}
if (param.getStatus() != null) {
wrapper.eq(User::getStatus, param.getStatus());
}
// 排序
wrapper.orderByDesc(User::getCreateTime);
// 查询数据
List<User> users = userMapper.selectList(wrapper);
// 转换为 DTO
return users.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
});
}
private UserDTO convertToDTO(User user) {
UserDTO dto = new UserDTO();
BeanUtils.copyProperties(user, dto);
return dto;
}
}05|高级封装技巧:应对复杂业务场景
5.1 支持复杂查询条件的分页封装
在实际业务中,我们经常需要处理复杂的查询条件。创建一个支持动态条件的分页封装:
@Slf4j
public class AdvancedPageHelper {
/**
* 支持复杂查询的分页封装
*/
public static <T, P> PageResult<T> doPageWithParams(
int pageNum,
int pageSize,
P queryParam,
Function<P, List<T>> queryFunction) {
try {
PageHelper.startPage(pageNum, pageSize);
List<T> list = queryFunction.apply(queryParam);
PageInfo<T> pageInfo = new PageInfo<>(list);
return buildPageResult(pageInfo);
} catch (Exception e) {
log.error("分页查询失败,参数:{}", JSON.toJSONString(queryParam), e);
throw new BusinessException("分页查询失败");
} finally {
PageHelper.clearPage();
}
}
/**
* 支持排序的分页封装
*/
public static <T> PageResult<T> doPageWithSort(
int pageNum,
int pageSize,
String orderBy,
Supplier<List<T>> queryFunction) {
try {
// 设置排序
if (StringUtils.isNotBlank(orderBy)) {
PageHelper.startPage(pageNum, pageSize, orderBy);
} else {
PageHelper.startPage(pageNum, pageSize);
}
List<T> list = queryFunction.get();
PageInfo<T> pageInfo = new PageInfo<>(list);
return buildPageResult(pageInfo);
} catch (Exception e) {
log.error("分页查询失败", e);
throw new BusinessException("分页查询失败");
} finally {
PageHelper.clearPage();
}
}
}5.2 多表关联查询的分页处理
处理多表关联查询时,我们需要特别注意分页的准确性:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 订单列表查询(包含用户信息和商品详情)
*/
public PageResult<OrderDetailDTO> getOrderDetailPage(int pageNum, int pageSize, OrderQueryParam param) {
return AdvancedPageHelper.doPageWithParams(pageNum, pageSize, param, (queryParam) -> {
// 先查询订单主表
List<Order> orders = orderMapper.selectOrderList(queryParam);
if (CollectionUtils.isEmpty(orders)) {
return Collections.emptyList();
}
// 收集用户ID和商品ID
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Set<Long> productIds = orders.stream().map(Order::getProductId).collect(Collectors.toSet());
// 批量查询用户信息和商品信息
Map<Long, User> userMap = userMapper.selectBatchIds(userIds).stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
Map<Long, Product> productMap = productMapper.selectBatchIds(productIds).stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));
// 组装结果
return orders.stream().map(order -> {
OrderDetailDTO dto = new OrderDetailDTO();
dto.setOrder(order);
dto.setUser(userMap.get(order.getUserId()));
dto.setProduct(productMap.get(order.getProductId()));
return dto;
}).collect(Collectors.toList());
});
}
}在 TRAE IDE 中,其代码分析功能可以帮助我们识别这种复杂查询中的潜在性能问题,并提供优化建议。
5.3 分页缓存优化
对于查询频繁但数据变化不大的场景,可以引入缓存机制:
@Slf4j
@Component
public class CachedPageHelper {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 支持缓存的分页查询
*/
public <T> PageResult<T> doPageWithCache(
String cacheKey,
int pageNum,
int pageSize,
Supplier<List<T>> queryFunction,
long timeout,
TimeUnit unit) {
// 构建缓存key
String key = String.format("%s:%d:%d", cacheKey, pageNum, pageSize);
// 尝试从缓存获取
PageResult<T> cachedResult = (PageResult<T>) redisTemplate.opsForValue().get(key);
if (cachedResult != null) {
log.info("从缓存获取分页数据,key: {}", key);
return cachedResult;
}
// 缓存未命中,查询数据库
PageResult<T> result = PageHelperUtil.doPage(pageNum, pageSize, queryFunction);
// 存入缓存
redisTemplate.opsForValue().set(key, result, timeout, unit);
log.info("分页数据存入缓存,key: {}", key);
return result;
}
/**
* 清除分页缓存
*/
public void clearPageCache(String cacheKeyPrefix) {
Set<String> keys = redisTemplate.keys(cacheKeyPrefix + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
log.info("清除分页缓存,keys: {}", keys);
}
}
}06|前端对接实践:统一分页组件
6.1 创建前端分页响应类型
在 TypeScript 中定义对应的分页响应类型:
// 分页响应统一接口
export interface PageResult<T> {
code: number;
message: string;
data: PageData<T>;
}
// 分页数据接口
export interface PageData<T> {
currentPage: number;
pageSize: number;
total: number;
totalPages: number;
records: T[];
hasNext: boolean;
hasPrevious: boolean;
}
// 分页查询参数接口
export interface PageQuery {
pageNum: number;
pageSize: number;
[key: string]: any;
}6.2 封装统一分页 Hook
使用 React 封装一个通用的分页 Hook:
import { useState, useEffect, useCallback } from 'react';
import { PageResult, PageData, PageQuery } from './types';
interface UsePageOptions<T> {
// 数据获取函数
fetchData: (query: PageQuery) => Promise<PageResult<T>>;
// 默认每页条数
defaultPageSize?: number;
// 是否立即加载
immediate?: boolean;
}
interface UsePageReturn<T> {
// 分页数据
pageData: PageData<T> | null;
// 加载状态
loading: boolean;
// 错误信息
error: string | null;
// 刷新数据
refresh: () => void;
// 跳转到指定页
goToPage: (page: number) => void;
// 修改每页条数
changePageSize: (size: number) => void;
// 修改查询条件
updateQuery: (query: Partial<PageQuery>) => void;
}
export function usePage<T>(options: UsePageOptions<T>): UsePageReturn<T> {
const { fetchData, defaultPageSize = 10, immediate = true } = options;
const [query, setQuery] = useState<PageQuery>({
pageNum: 1,
pageSize: defaultPageSize,
});
const [pageData, setPageData] = useState<PageData<T> | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 加载数据函数
const loadData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await fetchData(query);
if (result.code === 200) {
setPageData(result.data);
} else {
setError(result.message);
}
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
}, [query, fetchData]);
// 刷新数据
const refresh = useCallback(() => {
loadData();
}, [loadData]);
// 跳转到指定页
const goToPage = useCallback((page: number) => {
setQuery(prev => ({ ...prev, pageNum: Math.max(1, page) }));
}, []);
// 修改每页条数
const changePageSize = useCallback((size: number) => {
setQuery(prev => ({ ...prev, pageSize: Math.max(1, size), pageNum: 1 }));
}, []);
// 修改查询条件
const updateQuery = useCallback((newQuery: Partial<PageQuery>) => {
setQuery(prev => ({ ...prev, ...newQuery, pageNum: 1 }));
}, []);
// 初始加载
useEffect(() => {
if (immediate) {
loadData();
}
}, [loadData, immediate]);
// 查询条件变化时重新加载
useEffect(() => {
if (!immediate) return;
loadData();
}, [query, loadData, immediate]);
return {
pageData,
loading,
error,
refresh,
goToPage,
changePageSize,
updateQuery,
};
}6.3 在组件中使用分页 Hook
import React from 'react';
import { Table, Pagination, Input, Button } from 'antd';
import { usePage } from './hooks/usePage';
import { getUserList } from './services/userService';
interface User {
id: number;
username: string;
email: string;
status: number;
}
const UserList: React.FC = () => {
const {
pageData,
loading,
error,
goToPage,
changePageSize,
updateQuery,
} = usePage<User>({
fetchData: getUserList,
defaultPageSize: 20,
});
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '状态', dataIndex: 'status', key: 'status' },
];
const handleSearch = (value: string) => {
updateQuery({ username: value });
};
if (error) {
return <div style={{ color: 'red' }}>错误:{error}</div>;
}
return (
<div>
<div style={{ marginBottom: 16 }}>
<Input.Search
placeholder="搜索用户名"
onSearch={handleSearch}
style={{ width: 200, marginRight: 16 }}
/>
<Button type="primary">新增用户</Button>
</div>
<Table
columns={columns}
dataSource={pageData?.records || []}
loading={loading}
rowKey="id"
pagination={false}
/>
{pageData && (
<Pagination
current={pageData.currentPage}
total={pageData.total}
pageSize={pageData.pageSize}
onChange={goToPage}
onShowSizeChange={(_, size) => changePageSize(size)}
showSizeChanger
showQuickJumper
showTotal={(total) => `共 ${total} 条记录`}
style={{ marginTop: 16, textAlign: 'right' }}
/>
)}
</div>
);
};
export default UserList;07|性能优化与最佳实践
7.1 分页查询性能优化
- 合理使用索引:确保分页查询的排序字段有索引
- 避免深分页:对于大数据量表,考虑使用游标分页
- 延迟关联:先分页查询主表,再批量查询关联数据
/**
* 游标分页示例(适用于深分页场景)
*/
public PageResult<T> cursorPage(Long cursor, int pageSize, Supplier<List<T>> queryFunction) {
// 使用游标进行分页,避免深分页性能问题
List<T> list = queryFunction.apply(cursor, pageSize);
// 构建结果
PageResult.PageData<T> pageData = PageResult.PageData.<T>builder()
.records(list)
.pageSize(pageSize)
.currentPage(cursor == null ? 1 : cursor.intValue())
.build();
return PageResult.<T>builder()
.code(200)
.message("success")
.data(pageData)
.build();
}7.2 内存使用优化
/**
* 大数据量分页导出
*/
public void exportLargeData(OutputStream outputStream, int batchSize) {
// 使用流式查询,避免内存溢出
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE, false)) {
MyMapper mapper = sqlSession.getMapper(MyMapper.class);
int offset = 0;
List<T> batch;
do {
// 分批查询
batch = mapper.selectBatch(offset, batchSize);
// 处理当前批次数据
processBatch(batch, outputStream);
offset += batchSize;
// 清理缓存,释放内存
sqlSession.clearCache();
} while (batch.size() == batchSize);
}
}7.3 分页安全优化
/**
* 分页参数校验
*/
public class PageValidator {
private static final int MAX_PAGE_SIZE = 1000; // 最大每页条数
private static final int DEFAULT_PAGE_SIZE = 10; // 默认每页条数
public static void validatePageParams(int pageNum, int pageSize) {
if (pageNum < 1) {
throw new IllegalArgumentException("页码不能小于1");
}
if (pageSize < 1 || pageSize > MAX_PAGE_SIZE) {
throw new IllegalArgumentException("每页条数必须在1-" + MAX_PAGE_SIZE + "之间");
}
}
public static int normalizePageSize(Integer pageSize) {
return pageSize == null || pageSize < 1 || pageSize > MAX_PAGE_SIZE
? DEFAULT_PAGE_SIZE : pageSize;
}
}08|TRAE IDE 在分页开发中的优势
在整个 PageInfo 封装过程中,TRAE IDE 展现了强大的开发辅助能力:
8.1 智能代码生成
TRAE IDE 的 AI 助手可以根据简单的描述快速生成分页相关的代码模板:
- 输入"创建分页响应对象",自动生成包含完整字段的 PageResult 类
- 输入"封装 PageHelper 工具类",自动生成包含异常处理和缓存的工具类
- 输入"React 分页 Hook",自动生成包含状态管理的自定义 Hook
8.2 代码质量保障
TRAE IDE 提供的实时代码分析功能:
- 性能检测:识别深分页、N+1 查询等性能问题
- 安全检测:发现 SQL 注入风险、内存泄漏隐患
- 规范检查:确保代码符合团队编码规范
8.3 调试与优化
TRAE IDE 的调试功能让分页开发更加高效:
- SQL 日志分析:实时查看 PageHelper 生成的 SQL 语句
- 性能分析:监控分页查询的执行时间和资源消耗
- 内存分析:检测大数据量分页时的内存使用情况
09|总结与思考题
PageHelper 的 PageInfo 封装不仅仅是技术实现,更是前后端协作规范的体现。通过合理的封装设计,我们可以:
- 统一分页响应格式,降低前后端沟通成本
- 提升代码复用性,避免重复造轮子
- 增强系统可维护性,便于后续功能扩展
- 优化查询性能,提供更好的用户体验
思考题:
- 在你的项目中,PageInfo 封装遇到了哪些挑战?是如何解决的?
- 除了文中提到的缓存优化,还有哪些方法可以提升分页查询性能?
- 如何设计一个支持跨数据源的分页查询方案?
本文相关代码示例已上传至 GitHub,欢迎 Star 和贡献:https://github.com/example/pagehelper-demo
使用 TRAE IDE 开发本文示例代码,体验智能编码助手的强大功能!
(此内容由 AI 辅助生成,仅供参考)