登录
后端

PageHelper分页插件中PageInfo的封装实践与技巧

TRAE AI 编程助手

本文将深入剖析 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 分页查询性能优化

  1. 合理使用索引:确保分页查询的排序字段有索引
  2. 避免深分页:对于大数据量表,考虑使用游标分页
  3. 延迟关联:先分页查询主表,再批量查询关联数据
/**
 * 游标分页示例(适用于深分页场景)
 */
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 封装不仅仅是技术实现,更是前后端协作规范的体现。通过合理的封装设计,我们可以:

  1. 统一分页响应格式,降低前后端沟通成本
  2. 提升代码复用性,避免重复造轮子
  3. 增强系统可维护性,便于后续功能扩展
  4. 优化查询性能,提供更好的用户体验

思考题

  1. 在你的项目中,PageInfo 封装遇到了哪些挑战?是如何解决的?
  2. 除了文中提到的缓存优化,还有哪些方法可以提升分页查询性能?
  3. 如何设计一个支持跨数据源的分页查询方案?

本文相关代码示例已上传至 GitHub,欢迎 Star 和贡献:https://github.com/example/pagehelper-demo

使用 TRAE IDE 开发本文示例代码,体验智能编码助手的强大功能!

(此内容由 AI 辅助生成,仅供参考)