工厂模式实战:让数据库连接与多数据源切换更优雅

发布时间:2026/10/10 18:56:15
工厂模式实战:让数据库连接与多数据源切换更优雅
做后端开发这些年数据库连接这块的需求几乎每个项目都会遇到尤其是那种需要同时支持多种数据库的系统——本地开发用 SQLite测试环境切 MySQL生产环境上了 Oracle 或者 PostgreSQL。如果每次切换都去改一大片业务代码里的连接逻辑那基本就是给自己找罪受。工厂模式Factory Pattern解决的核心问题恰恰就是让调用方不关心具体连的是哪个数据库只需要说给我一个连接至于这个连接是从 MySQL 拉来的还是从达梦拉来的交给工厂去决定。这篇内容适合正在学习设计模式的开发者也适合在项目中需要做多数据源切换的团队参考。我会从最基础的直连痛点说起把简单工厂、工厂方法、抽象工厂三种实现方式串起来附上 Java 的完整代码示例和参数配置细节最后分享我在实际迁移数据库时踩过的坑和排查思路。1. 为什么数据库连接这一层需要工厂模式1.1 传统直连方式的痛点先看一个没有工厂模式的裸奔写法。很多同学刚接触 JDBC 时直接在业务代码里这么干public ListUser getUsers() { Connection conn DriverManager.getConnection( jdbc:mysql://127.0.0.1:3306/shop, root, 123456 ); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM users); // 处理结果集... }单看这一小段确实没什么但把它放到真实项目里问题立刻暴露。第一连接字符串散落在各个业务方法里哪天数据库从 MySQL 换成 PostgreSQL你得全局搜索替换多少个文件第二连接创建逻辑和业务查询逻辑耦合在一起一个业务方法既要管查数据又要管建连接违背了单一职责原则。第三如果项目要求同时支持多种数据库难道每个业务方法里都写一堆 if-else 来判断当前环境该用哪个数据库你可能会说我们项目固定用 MySQL不存在这种切换需求。 但真实场景往往是开发环境用 H2 做快速测试测试环境切 MySQL生产环境用 Oracle或者客户有国产化要求需要支持达梦DM8、人大金仓这类数据库。就算当下不换给系统预留一个扩展点也是合理的架构决策。1.2 工厂模式解决的核心问题工厂模式的价值就是把创建对象和使用对象分离开。放在数据库连接的场景里调用方依赖的是一个抽象的IDatabaseConnection接口而不是具体的MySQLConnection类。这种依赖倒置带来的好处非常直接业务代码里不再出现驱动加载、URL 拼接、时区设置这些细节代码干净不少新增数据库类型时只需要新增一个实现类加上工厂里的一个映射分支业务方完全感知不到变化所有连接的创建逻辑集中在一处管理后面想加连接池、加监控、加重试机制只改工厂这一层就行一句话总结工厂模式把变化圈在了一个可控的类里让稳定的调用方代码得以安身。这比把变化散落在几十个业务方法里要安全得多。2. 三种工厂模式的选型思路2.1 简单工厂模式一个工厂管所有数据库简单工厂并不是 GoF 定义的正式设计模式但它绝对是应用最广的一种。做法很直白一个DatabaseConnectionFactory类通过传入的参数判断来创建不同类型的数据库连接对象。对于数据库类型固定且数量不多的项目这个方案最简单直接也很容易理解。2.2 工厂方法模式每种数据库一个工厂工厂方法模式把创建动作进一步下沉到子类。具体来说先定义IDatabaseFactory接口然后 MySQL 有MySQLFactoryPostgreSQL 有PostgreSQLFactory每个工厂只负责创建自己对应的连接对象。这种设计适合数据库种类多、同一个数据库还要分主库从库、读写分离等不同配置的场景。2.3 抽象工厂模式数据库产品簇的统一管理抽象工厂用于创建一系列相关的对象。在 JDBC 场景里连接、语句、结果集本身就是一套产品簇——MySQL 有 MySQL 的 Connection、Statement、ResultSetOracle 有 Oracle 的对应实现。不过 JDBC 标准已经帮我们封装好了这一层抽象日常开发里抽象工厂在数据库连接层的独立需求不算多。真正会用到抽象工厂的往往是自研 ORM 框架或者连接管理中间件需要统一管理连接、事务、元数据等多种对象的时候。这三种模式不是互斥关系。我在真实项目里的做法通常是外层用简单工厂做快速创建内层组合工厂方法来应对同一类型多配置的需求。下面具体拆解实现过程。3. 核心代码实现与参数解析3.1 定义数据库连接抽象接口先定义统一接口这是整个模式的地基public interface IDatabaseConnection { void connect(String host, int port, String dbName, String user, String password); void query(String sql); void close(); }接口尽量精简只暴露业务真正关心的三个动作。你可能会问为什么不用 JDBC 自带的java.sql.Connection因为 JDBC 的 Connection 接口太宽了直接暴露给业务方容易让他们绕过统一封装把各种方言 SQL 直接塞进来。包一层自己的接口相当于在数据库和业务代码之间立了一道纪律线。3.2 实现具体数据库连接类以 MySQL 实现为例public class MySQLConnection implements IDatabaseConnection { private static final String DRIVER com.mysql.cj.jdbc.Driver; private Connection connection; Override public void connect(String host, int port, String dbName, String user, String password) { try { Class.forName(DRIVER); String url String.format( jdbc:mysql://%s:%d/%s?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8, host, port, dbName ); this.connection DriverManager.getConnection(url, user, password); } catch (ClassNotFoundException e) { throw new DatabaseConnectException(MySQL driver not found, check dependency, e); } catch (SQLException e) { throw new DatabaseConnectException(MySQL connection failed: url, e); } } Override public void query(String sql) { try (Statement stmt connection.createStatement()) { try (ResultSet rs stmt.executeQuery(sql)) { // 这里处理结果集建议业务方自行传入 RowMapper System.out.println(MySQL query executed successfully); } } catch (SQLException e) { throw new DatabaseQueryException(MySQL query failed: sql, e); } } Override public void close() { try { if (connection ! null !connection.isClosed()) { connection.close(); } } catch (SQLException e) { // 记录日志即可关闭连接失败通常不影响主流程 } } }几个关键参数值得单独说明serverTimezoneAsia/ShanghaiMySQL 8.0 之后时区校验变得严格不设置的话容易报The server time zone value Öйú±ê׼ʱ¼ä这类的乱码时区错误这个坑我踩过一次印象很深刻。characterEncodingutf8避免中文乱码的约定俗成配置加了它之后插入和查询中文基本不会出问题。useSSLfalse本地开发和内网环境基本不需要 SSL但生产环境要根据安全要求决定是否开启。PostgreSQL 的实现结构完全一样只是驱动类和 URL 格式不同Class.forName(org.postgresql.Driver); String url String.format(jdbc:postgresql://%s:%d/%s, host, port, dbName);Oracle 稍微特殊一点它走thin模式Class.forName(oracle.jdbc.OracleDriver); String url String.format(jdbc:oracle:thin:%s:%d:%s, host, port, dbName);注意Oracle 的 URL 格式里dbName是 SID 的概念不是数据库名。这是 Oracle 连接里最常见的坑之一。至于国产数据库达梦DM8它的 JDBC 驱动类是dm.jdbc.driver.DmDriverURL 格式是jdbc:dm://host:port只要实现了IDatabaseConnection接口接入方式和 MySQL 完全一致——这正是 JDBC 抽象层设计得好时更换数据库对业务代码几乎透明的典型例证。3.3 用简单工厂统一管理创建过程接口和实现类都准备好了下一步是写工厂本身public class DatabaseConnectionFactory { public static IDatabaseConnection createConnection(String dbType) { switch (dbType.toLowerCase()) { case mysql: return new MySQLConnection(); case postgresql: case pg: return new PostgreSQLConnection(); case oracle: return new OracleConnection(); case dm8: case dameng: return new DM8Connection(); default: throw new UnsupportedDatabaseTypeException(Unsupported database type: dbType); } } }这里有两个设计细节值得注意。第一参数统一转小写再判断这样调用方传MySQL、mysql还是MYSQL都能走同一个分支减少大小写带来的心智负担。第二默认分支抛出UnsupportedDatabaseTypeException自定义异常而不是返回 null。返回 null 的后果是调用方还得写if (connection null)判空而抛异常能更快暴露配置问题让错误在源头就暴露出来。3.4 客户端调用示例业务方使用时代码变得非常干净IDatabaseConnection connection DatabaseConnectionFactory.createConnection(mysql); try { connection.connect(127.0.0.1, 3306, shop, root, 123456); connection.query(SELECT * FROM users); } finally { connection.close(); }如果哪一天需要切换成 PostgreSQL只需要改createConnection的参数其他业务代码一行都不用动。这就是工厂模式带来的最直观的好处——变化的成本被压缩到了一行代码。4. 从简单工厂到工厂方法应对复杂配置的演进4.1 为什么有时候简单工厂不够用简单工厂解决了基本的多数据库创建问题但你如果遇到这种需求MySQL 有主库和从库两组配置PostgreSQL 有读写分离的多个地址简单工厂就开始吃力了。因为createConnection(String type)只有类型这一个维度表达不了同一类型但不同配置的差异。我在实际项目中就碰到过一个场景一个服务要同时连接两个不同的 MySQL 实例一个存业务数据一个存日志数据。如果只按类型创建两个MySQLConnection对象没有任何区别业务却要从不同的库读写。显然光靠 type 不够了。4.2 工厂方法模式的接入方式把工厂升级成接口每种数据库一个工厂类每个工厂内部可以完全掌控自己连接的配置方式public interface IDatabaseFactory { IDatabaseConnection createConnection(); String getType(); }MySQL 工厂public class MySQLFactory implements IDatabaseFactory { private final String host; private final int port; private final String dbName; private final String user; private final String password; public MySQLFactory(MysqlConfig config) { this.host config.getHost(); this.port config.getPort(); this.dbName config.getDbName(); this.user config.getUser(); this.password config.getPassword(); } Override public IDatabaseConnection createConnection() { MySQLConnection conn new MySQLConnection(); conn.connect(host, port, dbName, user, password); return conn; } Override public String getType() { return mysql; } }如果喜欢更简洁的写法可以结合函数式接口用一个 Map 来管理工厂的注册MapString, IDatabaseFactory factories new HashMap(); factories.put(mysql, new MySQLFactory(mysqlConfig)); factories.put(postgresql, new PostgreSQLFactory(pgConfig)); factories.put(oracle, new OracleFactory(oracleConfig)); IDatabaseConnection conn factories.get(dbType).createConnection();这实际上把工厂做成了一个注册表模式每个实现的配置在初始化时注入运行时按 key 取出工厂再创建连接。好处是新增数据库类型时你只需要在初始化工厂表的位置加一行注册代码具体调用方完全感知不到新增这件事。4.3 工厂方法模式的代价与边界工厂方法的代价是类数量增加了。之前一个工厂类管所有类型现在每种数据库都有独立的工厂类。如果数据库类型只有两三种用工厂方法可能有点过度设计。我的判断标准很简单项目只连一种数据库不需要工厂模式直接用连接池就好项目同时支持 2-3 种数据库简单工厂完全够用项目支持 3 种以上数据库且有同类型多实例、多配置的需求请上工厂方法写代码和买东西一个道理够用就好不要为了设计模式而设计模式。5. 常见问题与排查技巧实录5.1 驱动类加载失败的排查工厂模式封装好之后最容易遇到的就是ClassNotFoundException。这个异常不外乎两个原因一是缺少依赖比如pom.xml里没加mysql-connector-java的坐标二是驱动类名写错了。我遇到过 MySQL 8.0 混用老驱动类com.mysql.jdbc.Driver的情况8.x 驱动虽然保留了兼容类但会打出警告而且某些场景下行为有差异。排查思路很简单看异常栈里Class.forName那一层的类名把驱动类名和依赖目录下的实际类路径对照确认一下。提示JDBC 4.0 之后驱动可以通过META-INF/services自动注册Class.forName其实不是强制的。但写上它有一个好处驱动未引入时报错会更直接不会等到DriverManager.getConnection时才给出晦涩的No suitable driver错误。5.2 连接层出不穷导致超时的解决用工厂模式直连数据库还有一个容易忽略的问题每次createConnection都会创建一个物理连接高频业务下连接数会被打满数据库端会报Too many connections。这其实不是工厂模式本身的问题而是创建方式的问题。解决思路很清楚工厂返回的不应该是裸连接而应该从连接池里取出来的代理连接。实际做法是给工厂注入一个连接池数据源例如 HikariCPpublic class MySQLConnection implements IDatabaseConnection { private HikariDataSource dataSource; Override public void connect(String host, int port, String dbName, String user, String password) { HikariConfig config new HikariConfig(); config.setJdbcUrl(String.format(jdbc:mysql://%s:%d/%s, host, port, dbName)); config.setUsername(user); config.setPassword(password); config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource new HikariDataSource(config); } Override public void query(String sql) { try (Connection conn dataSource.getConnection()) { // 从池中获取连接执行查询 } } }这样改造后业务代码的调用方式基本不变但底层连接是复用的。连接池的maximumPoolSize不是越大越好我一般先设 10再根据压测时的active连接数和wait时间逐步调整。连接池大小如果顶到数据库的max_connections上限基本就是在给整个系统埋雷。5.3 工厂模式下的 SQL 方言适配工厂模式解决了连接的创建问题但没解决SQL 方言的差异问题。MySQL 分页用LIMIT ?Oracle 用ROWNUM或者FETCH FIRST ? ROWS ONLYPostgreSQL 用的是LIMIT ... OFFSET ...。如果业务代码里直接拼 SQL换数据库时照样炸。我在工厂模式的项目里习惯给IDatabaseConnection增加一个方言相关的方法public interface IDatabaseConnection { void connect(String host, int port, String dbName, String user, String password); void query(String sql); String buildPageSql(String sql, int offset, int limit); void close(); }每个数据库实现类自己负责方言// MySQL 实现 public String buildPageSql(String sql, int offset, int limit) { return sql LIMIT offset , limit; } // Oracle 实现 public String buildPageSql(String sql, int offset, int limit) { return SELECT * FROM (SELECT t.*, ROWNUM rn FROM ( sql ) t WHERE ROWNUM (offset limit) ) WHERE rn offset; }这样连接创建和SQL 方言两个变化点都收进了各自的实现类里。业务层写一套代码换数据库之后分页、日期函数这类差异都由底层兜住。注意这不是让你把所有 SQL 都塞进底层实现里而是把方言易变点抽象成接口方法避免业务代码被某一种数据库绑架。5.4 连接泄漏的排查记录工厂模式封装以后最容易出现的隐性 bug 是资源泄漏。query方法里用 try-with-resources 处理Statement和ResultSet问题不大但有些同学把Connection也放在方法里自动关闭这会把连接和业务动作搅在一起——业务方拿到的通道被底层方法给关了下次调用直接报连接不可用。我遇到的典型场景是用工厂返回的IDatabaseConnection业务方调了几次query之后忘了调close()数据库连接数悄悄攀升。后来排查时发现这个忘了 close的问题在代码 review 阶段根本看不出来。我的经验是不要只看代码里有没有close要看连接池指标。active一直上涨而idle稳定在低位基本都是连接泄漏。在这个问题上花点时间把工厂层的生命周期管理理顺比事后补漏要省心得多。6. 再往前一步工厂模式与配置中心联动6.1 配置驱动型的工厂设计最后聊聊生产环境里常见的一层演进工厂本身不硬编码类型判断而是从配置中心读取数据库配置。前面简单工厂的switch (dbType)在数据库类型固定时够用但如果你在一个插件化系统里想实现新加一个数据库驱动包配置中心加一份配置系统立刻多一种数据源支持这个 switch 就会成为瓶颈。一个更灵活的工厂设计是结合反射或 SPI 机制public class DynamicDatabaseFactory { private static MapString, IDatabaseConnection pool new ConcurrentHashMap(); public static IDatabaseConnection createConnection(DatabaseConfig config) { String type config.getType(); return pool.computeIfAbsent(type, key - { String className config.getClassName(); try { Class? clazz Class.forName(className); return (IDatabaseConnection) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new DatabaseConnectException(Failed to create connection instance: className, e); } }); } }这个写法把新数据库接入变成了配置驱动只要新数据库类实现了IDatabaseConnection接口把类名填到配置中心系统就能动态加载。不需要改工厂代码也不需要重新发布应用。当变化变得频繁且不可预知时把判断逻辑从工厂里彻底抽出去是比写一堆 case 分支更理性的选择。6.2 与 Spring 生态结合的实际方案如果你用的是 Spring Boot直接在Configuration类里把工厂注册成 Bean 就好Configuration public class DatabaseFactoryConfig { Bean public IDatabaseConnection mysqlConnection() { return new MySQLConnection(); } Bean public IDatabaseConnection postgresqlConnection() { return new PostgreSQLConnection(); } Bean public DatabaseConnectionFactory connectionFactory( ListIDatabaseConnection connections) { MapString, IDatabaseConnection map new HashMap(); for (IDatabaseConnection conn : connections) { map.put(conn.getType(), conn); } return new DatabaseConnectionFactory(map); } }这里 Spring 会把所有IDatabaseConnection类型的 Bean 自动收集进List并注入工厂只需要把 List 转成 Map。以后新增一种数据库加一个Bean方法就完事工厂和业务代码都保持不动。这个思路在微服务多数据源场景下非常实用我在两个生产项目里都是这么落地的扩展起来很顺畅。我个人在实际项目里的体会是工厂模式在数据库连接层的价值不在于我用了一个设计模式这个名头而在于它逼着你先定义好接口再把变化限制在实现类里。换数据库不是一个高频操作但正因为不频繁每次切换的时候才更需要一个清晰的扩展点。如果你刚开始重构多数据源代码我建议不要一上来就堆抽象工厂先写一个简单工厂跑通全链路等真的出现了同类型多配置的需求再演进到工厂方法。最后记得一件事工厂层只做创建这一件事千万别把 SQL 解析、事务管理这些也塞进去——一个膨胀的工厂比不用任何模式还要难维护。