【设计】设计一个web版的数据库管理平台后端(之五) --借鉴mybatis

前情回顾

之前我们利用4期将了设计一个数据库管理平台的代码精要。我们还讲了mybatis中最重要的几个对象。

现在,我们利用mybatis中的一些思路来优化我们的数据库管理平台。

优化点一:用"注册表(Registry)"替代硬编码的"Switch-Case"(解决"开闭原则")

问题:

每次新增数据库(如达梦、TDSQL)或新增SQL关键词(如UPSERT、MERGE),你都必须修改Configuration类和枚举类的源码。这违反了开闭原则(对扩展开放,对修改关闭)。

借鉴MyBatis(TypeAliasRegistry/MapperRegistry)

java 复制代码
// 1. 定义一个 DialectRegistry(注册表)
public class DialectRegistry {
    private final Map<String, Supplier<Dialect>> registry = new ConcurrentHashMap<>();
    
    public void register(String dbType, Supplier<Dialect> dialectSupplier) {
        registry.put(dbType.toLowerCase(), dialectSupplier);
    }
    
    public Dialect getDialect(String dbType) {
        Supplier<Dialect> supplier = registry.get(dbType.toLowerCase());
        if (supplier == null) {
            throw new UnsupportedOperationException("Unsupported DB: " + dbType);
        }
        return supplier.get();
    }
}

// 2. Configuration 只需持有注册表,并在启动时初始化
public class Configuration {
    private final DialectRegistry dialectRegistry = new DialectRegistry();
    
    public Configuration(DataSource dataSource) {
        // 启动时注册所有已知方言(甚至可以从配置文件动态加载)
        dialectRegistry.register("mysql", MySQLDialect::new);
        dialectRegistry.register("oracle", OracleDialect::new);
        dialectRegistry.register("sqlserver", SQLServerDialect::new);
        // 用户想新增达梦?直接调用 register("dameng", DamengDialect::new) 即可!
    }
    
    public Dialect getDialect() {
        return dialectRegistry.getDialect(this.databaseType);
    }
}

把"变化的数据库类型"变成了"注册表中的一项"。从此,主流程代码(Executor、StatementHandler)一行不改,完美支持无限扩展。

优化点二:用"工厂模式(Factory)"解耦"对象创建"(解决"依赖倒置")

SimpleExecutor 直接 new SimpleStatementHandler(configuration)

SimpleStatementHandler 直接 new MapResultSetHandler()

问题:

Executor 和 StatementHandler 深度绑定了具体实现类。如果想做SQL日志增强、换成批量执行器、或换成自定义结果集处理器(如带加密解密的SecureResultSetHandler),必须修改 SimpleExecutor 的源码。

借鉴MyBatis(DefaultSqlSessionFactory、MapperProxyFactory):

java 复制代码
// 1. 定义工厂接口
public interface StatementHandlerFactory {
    StatementHandler create(Configuration configuration);
}

// 2. 默认工厂生产默认处理器
public class DefaultStatementHandlerFactory implements StatementHandlerFactory {
    @Override
    public StatementHandler create(Configuration configuration) {
        return new SimpleStatementHandler(configuration);
    }
}

// 3. 改造 Executor,依赖工厂而非具体类
public class SimpleExecutor implements Executor {
    private final StatementHandlerFactory statementHandlerFactory;
    
    public SimpleExecutor(Configuration configuration) {
        this.statementHandlerFactory = configuration.getStatementHandlerFactory(); // 从配置拿
    }
    
    @Override
    public List<Map<String, Object>> query(String sql, Object... parameters) {
        StatementHandler handler = statementHandlerFactory.create(configuration);
        return handler.query(sql, parameters);
    }
}

现在如果接入Druid SQL监控,只需写一个 DruidStatementHandlerFactory 并注入Configuration,业务代码一行都不用动。

优化点三:借鉴"MapperMethod"思想,封装"SQL指令"而非散落的"if-else"

SqlSession 暴露了三个独立方法:selectList()、executeDML()、executeDDL()。用户(上层Controller)需要自己判断用哪个。

问题:

如果未来引入"存储过程调用(CALL)"或"批量提交(Batch)",SqlSession接口会无限膨胀(加executeProcedure、executeBatch),变成"大杂烩"。

借鉴MyBatis(MapperMethod 封装命令):

MyBatis的MapperMethod把"方法签名+SQL类型+参数映射"封装成一个对象,execute()内部根据类型走不同分支。

java 复制代码
// 1. 封装一个 SqlCommand,包含类型和SQL
public class SqlCommand {
    private final SqlCommandType type; // SELECT, DML, DDL, PROCEDURE
    private final String sql;
    
    public Object execute(SqlSession session, Object[] params) {
        switch (type) {
            case SELECT: return session.selectList(sql, params);
            case DML: return session.executeDML(sql, params);
            case DDL: return session.executeDDL(sql, params);
            // 新增CALL时,只改这里,不改SqlSession接口
        }
    }
}

// 2. SqlSession只保留一个通用入口
public class SqlSession {
    public Object execute(SqlCommand command, Object... parameters) {
        return command.execute(this, parameters);
    }
}

将"变化的SQL类型"转化为"可枚举的命令对象",业务侧只需要构造SqlCommand传给SqlSession,完全无需感知底层是查还是改。

优化点四:引入"拦截器(Interceptor)"机制,抽离"横切关注点"

当前代码:

SimpleStatementHandler.query()里,直接包含stmt.setQueryTimeout()和stmt.setMaxRows(),日志打印、性能监控、SQL审计全部要硬编码进去。

问题:

随着需求增加(慢SQL报警、SQL黑名单拦截、数据脱敏),StatementHandler会越来越臃肿,变成"上帝类"。

借鉴MyBatis(Interceptor链 + 动态代理):

虽然平台不需要像MyBatis那样用动态代理那么重,但可以引入"责任链模式"预处理/后处理SQL。

java 复制代码
// 1. 定义拦截器接口
public interface SqlInterceptor {
    void beforeExecute(String sql, Object[] params);
    void afterExecute(String sql, Object result, long costMs);
}

// 2. SimpleStatementHandler 注入拦截器列表
public class SimpleStatementHandler {
    private List<SqlInterceptor> interceptors;
    
    @Override
    public List<Map<String, Object>> query(String sql, Object... parameters) {
        for (SqlInterceptor interceptor : interceptors) {
            interceptor.beforeExecute(sql, parameters);
        }
        long start = System.currentTimeMillis();
        // ... 执行逻辑 ...
        long cost = System.currentTimeMillis() - start;
        for (SqlInterceptor interceptor : interceptors) {
            interceptor.afterExecute(sql, result, cost);
        }
        return result;
    }
}

现在,慢SQL日志、权限校验、数据脱敏全部以"插件"形式挂在拦截器链上。主流程SimpleStatementHandler永远只负责执行SQL,不关心任何旁路逻辑。新增审计需求时,只需要新增一个拦截器类并在启动时注册即可。

总结

当前代码 抽象目标(MyBatis方式) 架构收益
switch硬编码创建方言 DialectRegistry 注册表 开闭原则:新增DB零修改
new SimpleStatementHandler() StatementHandlerFactory 工厂 依赖倒置:可随意替换执行策略
SqlSession有多个executeXxx()方法 SqlCommand统一命令封装 单一职责:接口永不膨胀
日志/监控与执行逻辑混杂 SqlInterceptor 拦截器链 非侵入式扩展:主流程永葆纯洁
相关推荐
东风破_7 小时前
danci 2:创建的单词书到底存在哪里?从 Supabase 一路理解 ORM、Drizzle 和 RLS
数据库·后端·node.js
橙子家8 小时前
OSS 文件上传的几个风险点和解决方案
数据库
2601_962066499 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库
愤怒的苹果ext10 小时前
MySQL Shell备份恢复数据库
数据库·mysql·备份恢复·mysqlsh
数字新视界12 小时前
DCIM管理系统的技术架构与部署模式详解
数据库·物联网·数据中心·数据中心基础设施管理·dcim管理系统
何以解忧,唯有..12 小时前
Pydantic 介绍与使用:Python 数据校验的现代方案
数据库·python·microsoft
这个DBA有点耶13 小时前
数据库容灾进入“秒级时代”:同城双活架构原理、关键技术选型与落地实践
数据库·架构·dba
2601_9620748114 小时前
大数据-264 实时数仓 - Canal MySQL的binlog研究 存储目录 变动信息 配置MySQL
大数据·数据库·mysql
2601_9620738114 小时前
大数据-240 离线数仓 - 广告业务 测试 ADS层数据加载 DataX数据导出到 MySQL
大数据·数据库·mysql