前情回顾
#架构回顾
在〖设计〗设计一个web版的数据库管理平台后端(之二)这篇文章中,我们仿照MyBatis设计了一套轻量级的数据库访问框架。核心分层如下:
-
SqlSession:对外提供查询入口,负责创建Executor。
-
Executor:执行器,负责SQL的调度执行。
-
StatementHandler:语句处理器,负责SQL的准备、参数设置和执行。
-
ResultSetHandler:结果集处理器,负责将ResultSet转换为List<Map<String, Object>>。
-
Configuration:持有数据源等全局配置。
这个架构非常灵活,而且不依赖于任何预定义的实体类或Mapper接口,完美适配"用户输入任意SQL"的场景。
今天我们要做的,就是在保留原有非分页查询能力的基础上,为这套架构增加多数据库通用的物理分页功能。
设计方案
我们延续已有的分层思想,将分页职责合理地分配到各个组件中:
| 组件 | 新增职责 |
|---|---|
| Configuration | 识别当前连接的数据库类型,并提供对应的方言(Dialect) 实例 |
| StatementHandler | 根据方言生成COUNT SQL和分页SQL,并分别执行这两类查询 |
| Executor | 编排分页流程:先调用StatementHandler获得总记录数,再调用获得分页数据,最后组装PageResult |
| SqlSession | 对外提供selectListWithPagination方法,接收原始SQL和分页参数 |
整个流程如下:
text
用户调用 SqlSession.selectListWithPagination(sql, pageNum, pageSize, params)
│
▼
Executor.queryWithPagination(sql, pageParam, params)
│
├── 1. 从Configuration获取Dialect
├── 2. 调用StatementHandler.executeCount() → 得到total
└── 3. 调用StatementHandler.executePaged() → 得到rows
│
▼
StatementHandler内部:
├── dialect.getCountSql(sql) → 执行COUNT查询
└── dialect.getPagedSql(sql, offset, limit) → 执行分页查询
│
▼
ResultSetHandler.handleResultSets(rs) → 转换为List<Map>
│
▼
返回 PageResult(total, rows)
核心代码实现
1. 分页参数与结果模型
java
// 分页请求参数
public class PageParam {
private int pageNum; // 当前页码,从1开始
private int pageSize; // 每页记录数
public PageParam(int pageNum, int pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
}
public int getOffset() {
return (pageNum - 1) * pageSize;
}
// getter/setter 省略
}
// 分页响应结果
public class PageResult<T> {
private long total; // 总记录数
private List<T> rows; // 当前页数据
// getter/setter 省略
}
2. 数据库类型枚举与方言接口
我们采用策略模式,每种数据库实现自己的分页SQL生成逻辑。
java
// 数据库类型枚举
public enum DatabaseType {
MYSQL, ORACLE, SQLSERVER, POSTGRESQL, UNKNOWN;
public static DatabaseType fromJdbcUrl(String jdbcUrl) {
if (jdbcUrl == null) return UNKNOWN;
String lower = jdbcUrl.toLowerCase();
if (lower.contains(":mysql:")) return MYSQL;
if (lower.contains(":oracle:")) return ORACLE;
if (lower.contains(":sqlserver:")) return SQLSERVER;
if (lower.contains(":postgresql:")) return POSTGRESQL;
return UNKNOWN;
}
}
// 方言接口
public interface Dialect {
String getCountSql(String originalSql);
String getPagedSql(String originalSql, int offset, int limit);
}
// MySQL / PostgreSQL 方言
public class MySQLDialect implements Dialect {
@Override
public String getCountSql(String originalSql) {
return "SELECT COUNT(1) FROM (" + originalSql + ") TEMP_COUNT_TABLE";
}
@Override
public String getPagedSql(String originalSql, int offset, int limit) {
return originalSql + " LIMIT " + offset + ", " + limit;
}
}
// Oracle 方言(12c以前)
public class OracleDialect implements Dialect {
@Override
public String getCountSql(String originalSql) {
return "SELECT COUNT(1) FROM (" + originalSql + ")";
}
@Override
public String getPagedSql(String originalSql, int offset, int limit) {
int endRow = offset + limit;
return "SELECT * FROM (SELECT TMP_.*, ROWNUM ROWNUM_ FROM ("
+ originalSql + ") TMP_ WHERE ROWNUM <= " + endRow
+ ") WHERE ROWNUM_ > " + offset;
}
}
// SQL Server 方言(2012+)
public class SQLServerDialect implements Dialect {
@Override
public String getCountSql(String originalSql) {
return "SELECT COUNT(1) FROM (" + originalSql + ") TEMP_COUNT_TABLE";
}
@Override
public String getPagedSql(String originalSql, int offset, int limit) {
return originalSql + " OFFSET " + offset + " ROWS FETCH NEXT " + limit + " ROWS ONLY";
}
}
如果需要支持其他数据库(如DB2、H2),只需新增一个Dialect实现类即可。
3. 增强 Configuration(配置中心)
在原有的Configuration中加入数据库类型识别和方言获取能力。注意:由于每个用户的数据源不同,数据库类型可能在运行时才能确定,因此我们在构造Configuration时从数据源连接URL中解析。
java
public class Configuration {
private DataSource dataSource;
private boolean cacheEnabled = false;
private DatabaseType databaseType; // 新增
private Dialect dialect; // 新增
public Configuration(DataSource dataSource) {
this.dataSource = dataSource;
this.databaseType = detectDatabaseType();
this.dialect = createDialect();
}
private DatabaseType detectDatabaseType() {
try (Connection conn = dataSource.getConnection()) {
String url = conn.getMetaData().getURL();
return DatabaseType.fromJdbcUrl(url);
} catch (SQLException e) {
return DatabaseType.UNKNOWN;
}
}
private Dialect createDialect() {
switch (databaseType) {
case MYSQL:
case POSTGRESQL:
return new MySQLDialect();
case ORACLE:
return new OracleDialect();
case SQLSERVER:
return new SQLServerDialect();
default:
throw new UnsupportedOperationException("Unsupported database: " + databaseType);
}
}
public Dialect getDialect() { return dialect; }
// 原有getter/setter保持不变...
}
``
## 4. 增强 StatementHandler(语句处理器)
StatementHandler是分页SQL改写的核心。我们在原有query方法之外,新增两个方法:executeCount和executePaged。
```java
public interface StatementHandler {
// 普通查询(原有)
List<Map<String, Object>> query(String sql, Object... parameters) throws SQLException;
// 执行COUNT查询,返回总记录数
long executeCount(String sql, Object... parameters) throws SQLException;
// 执行分页查询,返回当前页数据
List<Map<String, Object>> executePaged(String sql, int offset, int limit, Object... parameters) throws SQLException;
}
public class SimpleStatementHandler implements StatementHandler {
private final Configuration configuration;
private final ResultSetHandler resultSetHandler;
public SimpleStatementHandler(Configuration configuration) {
this.configuration = configuration;
this.resultSetHandler = new MapResultSetHandler();
}
// 普通查询实现(原有逻辑,略)
@Override
public List<Map<String, Object>> query(String sql, Object... parameters) throws SQLException {
// ... 保持不变 ...
}
@Override
public long executeCount(String sql, Object... parameters) throws SQLException {
Dialect dialect = configuration.getDialect();
String countSql = dialect.getCountSql(sql);
return executeCountQuery(countSql, parameters);
}
@Override
public List<Map<String, Object>> executePaged(String sql, int offset, int limit, Object... parameters) throws SQLException {
Dialect dialect = configuration.getDialect();
String pagedSql = dialect.getPagedSql(sql, offset, limit);
return executeQuery(pagedSql, parameters);
}
private long executeCountQuery(String countSql, Object... parameters) throws SQLException {
try (Connection conn = configuration.getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(countSql)) {
for (int i = 0; i < parameters.length; i++) {
stmt.setObject(i + 1, parameters[i]);
}
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getLong(1);
}
return 0L;
}
}
}
private List<Map<String, Object>> executeQuery(String pagedSql, Object... parameters) throws SQLException {
try (Connection conn = configuration.getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(pagedSql)) {
for (int i = 0; i < parameters.length; i++) {
stmt.setObject(i + 1, parameters[i]);
}
try (ResultSet rs = stmt.executeQuery()) {
return resultSetHandler.handleResultSets(rs);
}
}
}
}
5. 增强 Executor(执行器)
Executor负责编排分页流程:先获取总记录数,再获取分页数据,最后组装PageResult。
java
public interface Executor {
List<Map<String, Object>> query(String sql, Object... parameters);
PageResult<Map<String, Object>> queryWithPagination(String sql, PageParam pageParam, Object... parameters);
void close();
}
public class SimpleExecutor implements Executor {
private final Configuration configuration;
private StatementHandler statementHandler;
public SimpleExecutor(Configuration configuration) {
this.configuration = configuration;
this.statementHandler = new SimpleStatementHandler(configuration);
}
@Override
public List<Map<String, Object>> query(String sql, Object... parameters) {
try {
return statementHandler.query(sql, parameters);
} catch (SQLException e) {
throw new RuntimeException("Error executing query", e);
}
}
@Override
public PageResult<Map<String, Object>> queryWithPagination(String sql, PageParam pageParam, Object... parameters) {
try {
long total = statementHandler.executeCount(sql, parameters);
List<Map<String, Object>> rows = statementHandler.executePaged(
sql, pageParam.getOffset(), pageParam.getPageSize(), parameters
);
PageResult<Map<String, Object>> result = new PageResult<>();
result.setTotal(total);
result.setRows(rows);
return result;
} catch (SQLException e) {
throw new RuntimeException("Error executing paginated query", e);
}
}
@Override
public void close() {
// 释放资源(如有)
}
}
6. 增强 SqlSession(对外API)
最后,在SqlSession中提供一个便利的分页查询方法。
java
public class SqlSession {
private final Configuration configuration;
private final Executor executor;
public SqlSession(Configuration configuration) {
this.configuration = configuration;
this.executor = new SimpleExecutor(configuration);
}
// 普通查询
public List<Map<String, Object>> selectList(String sql, Object... parameters) {
return executor.query(sql, parameters);
}
// 分页查询
public PageResult<Map<String, Object>> selectListWithPagination(String sql, int pageNum, int pageSize, Object... parameters) {
PageParam pageParam = new PageParam(pageNum, pageSize);
return executor.queryWithPagination(sql, pageParam, parameters);
}
public void close() {
executor.close();
}
}
使用示例
java
// 1. 创建数据源(用户自己的数据库连接池)
DataSource dataSource = ...;
Configuration configuration = new Configuration(dataSource);
// 2. 执行分页查询
try (SqlSession sqlSession = new SqlSession(configuration)) {
String sql = "SELECT id, name, create_time FROM user ORDER BY create_time DESC";
PageResult<Map<String, Object>> page = sqlSession.selectListWithPagination(sql, 2, 10);
System.out.println("总记录数: " + page.getTotal());
for (Map<String, Object> row : page.getRows()) {
System.out.println(row);
}
}
结果:
json
{
"total": 105,
"rows": [
{"id": 11, "name": "张三", "create_time": "2025-01-01"},
{"id": 12, "name": "李四", "create_time": "2025-01-02"}
// ... 共10条
]
}
如果用户连接的是Oracle数据库,同样的代码会自动使用Oracle分页语法,无需任何修改!
总结
1. 高扩展性
新增一个Dialect实现类。
在Configuration.createDialect()中添加一个case分支。
无需修改任何其他代码。
2. 线程安全
Configuration、Dialect、StatementHandler等均为无状态或只读状态,多个线程可以安全共享。每个SqlSession持有独立的Executor实例,不会互相干扰。