【设计】设计一个web版的数据库管理平台后端(之三) -- 多数据库通用分页

前情回顾

〖设计〗设计一个web版的数据库管理平台后端精要

〖设计〗设计一个web版的数据库管理平台后端(之二)

设计一个分页插件之二〖基于mybatis实现分页插件〗

设计一个分页插件

#架构回顾

〖设计〗设计一个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实例,不会互相干扰。

相关推荐
Rick19931 小时前
mysql联合索引经典实例
java·数据库·mysql
anew___1 小时前
《数据库原理》精要解读(七)—— 数据库设计:从蓝图到现实的系统工程
数据库·oracle
独隅1 小时前
MySQL 接入不同 AI 大模型进行数据管理的全面指南(MySQL + AI)
数据库·人工智能·mysql
go不是csgo1 小时前
GORM 上手:一个 main.go 跑通 Go 数据库增删改查
jvm·数据库·golang
lld9510272 小时前
(一)云回测:量化策略上线前的必经之路
java·服务器·数据库
Old Uncle Tom2 小时前
Harness Engineering 综述
java·开发语言·数据库
疯狂打码的少年3 小时前
Cache的三种映射方式(直接/全相联/组相联)
linux·服务器·数据库·笔记
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL备份与恢复Day14(2026年)
数据库·后端·mysql
渣渣盟3 小时前
MySQL DDL操作全解析:从入门到精通,包含索引视图分区表等全操作解析
大数据·数据库·mysql