Spring `JdbcTemplate` 無內建分頁支持,大數據量下全量查詢易導致 OOM。本文實現一個通用 `PaginationHelper`,採用「兩條 SQL」策略:COUNT(*) 統計總行數 → 計算總頁數 → `LIMIT/OFFSET` 取當前頁數據,兼容 MySQL、Oracle、PostgreSQL 等主流數據庫。
public class Page<E> {
private int pageNumber; // 當前頁碼 (1-based)
private int pagesAvailable; // 總頁數
private List<E> pageItems = new ArrayList<>(); // 當前頁數據
// getter/setter 省略
}
public class PaginationHelper<E> {
public Page<E> fetchPage(JdbcTemplate jt,
String sqlCountRows, // SELECT COUNT(*) ...
String sqlFetchRows, // SELECT id, name ... ORDER BY ...
Object[] args, // 參數陣列
int pageNo, // 1-based
int pageSize, // 每頁條數
ParameterizedRowMapper<E> rowMapper) {
// 1. 統計總行數
int rowCount = jt.queryForObject(sqlCountRows, args, Integer.class);
// 2. 計算總頁數
int pageCount = rowCount / pageSize;
if (rowCount > pageSize * pageCount) pageCount++;
// 3. 構建 Page 對象
Page<E> page = new Page<>();
page.setPageNumber(pageNo);
page.setPagesAvailable(pageCount);
// 4. 物理分頁查詢(使用 ResultSetExtractor 手動控制游標)
final int startRow = (pageNo - 1) * pageSize;
jt.query(sqlFetchRows, args, new ResultSetExtractor<Page<E>>() {
@Override
public Page<E> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<E> items = page.getPageItems();
int currentRow = 0;
while (rs.next() && currentRow < startRow + pageSize) {
if (currentRow >= startRow) {
items.add(rowMapper.mapRow(rs, currentRow));
}
currentRow++;
}
return page;
}
});
return page;
}
}
public Page<Company> getCompanies(int pageNo, int pageSize) throws SQLException {
PaginationHelper<Company> ph = new PaginationHelper<>();
return ph.fetchPage(
jdbcTemplate,
"SELECT COUNT(*) FROM company WHERE user_id = ? AND deleted = 0 ORDER BY name",
"SELECT id, name FROM company WHERE user_id = ? AND deleted = 0 ORDER BY name",
new Object[]{userId},
pageNo, pageSize,
new ParameterizedRowMapper<Company>() {
@Override
public Company mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Company(rs.getInt(1), rs.getString(2));
}
}
);
}
| 數據庫 | 分頁語法 | sqlFetchRows 寫法 |
|---|---|---|
| MySQL | LIMIT offset, size | SELECT ... LIMIT ?, ? 參數傳 offset, size |
| PostgreSQL | LIMIT size OFFSET offset | SELECT ... LIMIT ? OFFSET ? |
| Oracle 12c+ | OFFSET ... ROWS FETCH NEXT ... ROWS ONLY | SELECT ... OFFSET ? ROWS FETCH NEXT ? ROWS ONLY |
| SQL Server 2012+ | OFFSET ... ROWS FETCH NEXT ... ROWS ONLY | 同 Oracle |
最佳實踐:將分頁 SQL 方言抽象為 `Dialect` 接口,`PaginationHelper` 注入具體實現,實現「寫一次 Java 代碼,多庫部署」。
`PaginationHelper` 以 **兩條 SQL + ResultSetExtractor** 實現通用物理分頁,核心優勢:零侵入業務代碼、支援任意 `RowMapper`、易擴展多方言。生產環境建議結合「總數緩存 + Keyset 分頁」應對高併發大數據場景。