2.4 JDBC的诞生
从各数据库各说各话走到 JDBC 统一接口,用谈判、工厂、驱动、参数和事务追踪一次安全查询。
学习目标
- 能沿配置、工厂、DataSource、Connection、PreparedStatement 和 ResultSet 追踪一次 JDBC 查询
- 能用统一接口、面向接口编程、简单工厂和工厂方法解释应用如何隔离具体数据库驱动
- 能在正常、边界和单一故障样本中定位参数注入、连接泄漏、驱动不匹配和事务提交错误
2.4 JDBC的诞生
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.4 JDBC的诞生。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
数据库厂商各自拥有协议、类型和连接方式,应用如果直接调用每家 API,就会被实现细节锁住。JDBC 的关键不是“把所有数据库变成一样”,而是定义一组稳定的接口和责任:驱动负责翻译,DataSource 负责获得连接,Connection 负责事务边界,PreparedStatement 负责参数化语句,ResultSet 负责游标式读取。应用依赖接口,具体驱动由配置和工厂选择。
三个会让 JDBC 抽象失真的陷阱
七个目录节点到接口证据
谈判
↡应用与数据库厂商围绕连接、语句、结果、类型和错误共同约定一套可调用的最小契约。不是让所有数据库失去特色,而是确定应用必须依赖的共同能力。接口名称、参数类型、资源关闭和异常传播要稳定;方言能力则通过扩展或明确降级表达。
统一接口
↡用 Driver/DataSource、Connection、PreparedStatement 和 ResultSet 等抽象承载关系数据库访问的共同调用形状。让业务代码不必直接知道每家驱动的类名。但接口调用仍有状态:连接可能在事务中,语句可能已关闭,结果集必须按游标读取。把接口当成无状态函数会隐藏资源和时序错误。
面向接口编程
↡业务逻辑依赖 JDBC 接口和自己的端口,而不是依赖某个数据库驱动的具体类,从而可以替换实现并隔离测试。把驱动选择放在组合根或工厂边界。测试可以注入假的 DataSource,生产环境注入真实池;替换实现仍要验证 SQL 方言、事务和错误语义。
简单工厂
↡读取受控配置并创建某一种 DataSource、ConnectionProvider 或驱动适配对象的集中选择点。适合把“选择哪种实现”从业务代码中移走。它不应把任意类名和凭据交给外部输入,也不应在每次查询中随意创建连接;配置校验、池生命周期和失败诊断属于工厂合同。
数据驱动
↡由具体数据库驱动把 JDBC 调用翻译成目标数据库协议、类型和错误的实现层。是真正面对数据库差异的地方。驱动版本、数据库版本、字符编码、时区、TLS 和错误映射都可能影响行为;应用要保存驱动身份和连接参数摘要,不能把接口层日志当成全部事实。
工厂方法
↡把创建连接、语句或数据源的步骤延迟到可替换的创建方法,使调用方只依赖抽象且可注入不同生命周期管理。适合区分单元测试、池化生产连接、只读连接和事务连接。创建策略改变时,必须重新验证关闭、超时、隔离和回滚;“能 new 出对象”不等于生命周期合同成立。
2.4 JDBC的诞生
2.4 JDBC的诞生的结果是一条可替换但不失真的访问链:共同接口负责形状,工厂负责选择,驱动负责翻译,连接负责事务,参数化语句负责输入边界,结果集负责读取。抽象减少耦合,却把驱动、资源和事务的验证责任变得更明确。
最小 JDBC 访问合同
interface QueryPort {
List<String> findNames(DataSource dataSource, String prefix) throws SQLException;
}
final class JdbcQuery implements QueryPort {
public List<String> findNames(DataSource dataSource, String prefix) throws SQLException {
String sql = "SELECT name FROM account WHERE name LIKE ? ORDER BY name";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, prefix + "%");
try (ResultSet result = statement.executeQuery()) {
List<String> names = new ArrayList<>();
while (result.next()) names.add(result.getString("name"));
return names;
}
}
}
}这里的 ? 是输入边界,try-with-resources 是资源边界,DataSource 是实现替换边界。若要执行多条写入,先明确 setAutoCommit(false)、提交、回滚和异常传播;查询成功不自动意味着业务事务已经完成。
四步复核一次 JDBC 调用
1. 谈判:固定共同能力和数据库差异
列出目标数据库、驱动版本、支持的 SQL、类型、隔离级别和错误映射。把必须跨库的能力放进接口,把方言能力标成显式扩展。
Lab
JDBC 访问边界实验
只改变一个输入、资源或事务条件,观察接口调用如何分叉。
输入作为参数绑定,不改变 SQL 结构
prepare(sql) → bind(prefix) → execute → close result/statement/connection
判定
accept:查询与资源合同成立
当前样本:参数化查询;保存驱动、参数、借还、事务和异常记录。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 驱动、配置、参数和资源生命周期匹配 | 查询结果正确,资源归还 | 驱动、SQL 模板、参数类型、池状态 |
| 边界 | 空值、大结果、超时或事务冲突 | 明确结果/异常,连接仍可复用 | 行数、游标、超时、事务状态 |
| 单一故障 | 驱动断开、参数非法或提交失败 | 拒绝/回滚且关闭资源 | 异常映射、借还记录、重放结果 |
故障诊断:先分清接口、驱动和资源层
- 配置层:检查工厂选择、URL、驱动版本、凭据来源和最小权限;没有连接前不要把问题归咎于 SQL。
- 接口层:检查参数类型、绑定顺序、SQL 模板、游标和关闭顺序;PreparedStatement 仍需要正确的类型与语义。
- 驱动/数据库层:检查方言、字符集、时区、隔离级别、错误码和服务器日志;接口异常只是入口。
- 资源/事务层:检查连接借出与归还、自动提交、提交/回滚和重试;连接泄漏与数据错误要分开定位。
如果同一业务在替身 DataSource 成功、真实驱动失败,先比较驱动和数据库合同;如果查询结果正确但池耗尽,先找关闭路径;如果写入重复,先找自动提交与重试幂等性。JDBC 通过必须同时覆盖输入、实现和生命周期。
最小 JDBC 证据包与反例
证据包包含工厂配置摘要、DataSource/驱动身份、连接池参数、SQL 模板、参数类型与脱敏值、结果行数、游标状态、事务模式、提交/回滚、异常映射、借出/归还和重试记录。不要把密码或完整敏感值写入日志,但要保留能区分一次请求的 ID。
反例一是把用户输入拼进 SQL 导致注入或类型错误;反例二是异常路径没有关闭 ResultSet/Statement/Connection,压力测试后池被耗尽;反例三是自动提交与业务重试共同造成重复写入。每次只改一个边界并从干净连接池重放。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 谈判
应用与数据库厂商共同确定连接、语句、结果、类型和错误的最小契约。
- 统一接口
用 JDBC 抽象承载不同关系数据库共同调用形状与资源状态。
- 面向接口编程
业务依赖接口并在组合根注入具体数据源或驱动实现。
- 简单工厂
根据受控配置集中选择并创建 DataSource 或连接适配对象。
- 数据驱动
把 JDBC 调用翻译为目标数据库协议、类型和错误的具体实现。
- 工厂方法
延迟和替换创建连接、语句或数据源的生命周期策略。
练习
练习
问题 1: 为什么 PreparedStatement 比字符串拼接更适合作为 JDBC 输入边界?还需要检查什么?
问题 2: 连接池在高压下耗尽,但数据库查询都返回正确。你会先看哪些记录?
问题 3: 同一套接口换了数据库驱动后,事务隔离和错误码表现不同。如何保持面向接口编程不自欺?
本页小结
2.4 JDBC的诞生的关键不是“一个接口解决所有数据库问题”,而是把谈判、统一接口、接口编程、工厂、驱动、参数和事务边界分开验证。完成标准是能追踪一次安全查询,解释驱动差异,并在异常、池耗尽和重试场景下证明资源与数据都能恢复。