说说 JDBC 的执行步骤?
结论先行:JDBC 的固定流程是 加载驱动 → 建立连接 → 创建 Statement/PreparedStatement → 设置参数 → 执行 → 处理 ResultSet → 释放资源。MyBatis、Hibernate 这些框架并没有绕开它,只是把这条流水线封装进了 Executor、StatementHandler。
1. 完整流程
① Class.forName / SPI 自动加载 Driver
▼
② DriverManager.getConnection(url, user, pwd) ── 或 DataSource.getConnection()
▼
③ conn.prepareStatement(sql) ── 预编译,返回 ps 与参数元数据
▼
④ ps.setXxx(1, value) ── 按占位符顺序绑定
▼
⑤ ps.executeQuery() / executeUpdate()
▼
⑥ ResultSet rs.next() 逐行读取,rs.getXxx(column)
▼
⑦ 关闭顺序:ResultSet → Statement → Connection(try-with-resources 自动)
2. 每一步的作用
| 步骤 | 关键点 | 常见错误 |
|---|---|---|
| 加载驱动 | JDBC 4 起 SPI 自动发现 META-INF/services/java.sql.Driver,Class.forName 可省 | 多驱动 jar 冲突 |
| 获取连接 | 生产必须走连接池(HikariCP、Druid),DriverManager 每次新建 TCP 连接 | 忘记关闭导致连接泄漏 |
| 创建语句 | PreparedStatement 预编译并缓存执行计划 | 用 Statement 拼字符串导致注入 |
| 绑定参数 | 下标从 1 开始,类型由 setXxx 决定 | 下标越界、setString 传 null 丢类型 |
| 执行 | executeQuery 返回结果集,executeUpdate 返回影响行数 | 混用导致返回 false 或异常 |
| 读取结果 | 必须 next() 才能取第一行,列名大小写依赖驱动 | 忘记 next() 导致 SQLException |
| 释放资源 | 逆序关闭,Connection 归还池而非真正关闭 | 只关 Connection 不关 Statement |
3. 一段最小可运行代码
public User findById(long id) throws SQLException {
String sql = "SELECT id, name, created_at FROM t_user WHERE id = ?";
// try-with-resources:按关闭顺序声明,异常时也会逆序释放
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id); // 参数从 1 开始
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null; // 必须显式判断是否为空
}
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
return user;
}
}
}
4. 写操作与批量
public void batchInsert(List<Order> orders) throws SQLException {
String sql = "INSERT INTO t_order (user_id, amount, status) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false); // 开启事务
for (Order o : orders) {
ps.setLong(1, o.getUserId());
ps.setBigDecimal(2, o.getAmount());
ps.setInt(3, o.getStatus());
ps.addBatch(); // 只加批,不发送
}
ps.executeBatch(); // 一次发送
conn.commit();
} catch (SQLException e) {
// 连接关闭时未提交事务会自动回滚,但显式 rollback 更清晰
throw e;
}
}
MySQL 上要让 addBatch 真正合并成一条多值 SQL,还需要在 URL 加 rewriteBatchedStatements=true。
5. 与 MyBatis 的对应关系
| JDBC 步骤 | MyBatis 对应 |
|---|---|
| 加载驱动、建连接 | Environment 的 DataSource + TransactionFactory |
prepareStatement | StatementHandler.prepare() |
setXxx | ParameterHandler + TypeHandler |
execute | StatementHandler.update/query + Executor |
rs.next + getXxx | ResultSetHandler + TypeHandler |
| 关闭资源 | Executor.close()、SqlSession.close() |
理解了 JDBC,MyBatis 的源码就只剩下「什么时候打开、什么时候关闭、参数怎么拿」这些调度问题。
6. 常见追问
追问一:Statement 和 PreparedStatement 有什么区别?
Statement 每次执行都要把 SQL 文本发过去并重新解析,且有注入风险;PreparedStatement 预编译、可复用执行计划、参数绑定防注入。除动态 DDL 外都应使用后者。
追问二:连接池为什么必要?
建立 TCP 连接 + 认证 + 会话初始化是毫秒级开销,高 QPS 下会拖垮数据库。连接池复用连接,同时限制最大连接数保护数据库。常见参数:maximumPoolSize、connectionTimeout、idleTimeout。
追问三:execute()、executeQuery()、executeUpdate() 怎么选?
executeQuery 只用于 SELECT,返回 ResultSet;executeUpdate 用于 INSERT/UPDATE/DELETE/DDL,返回行数;execute 两者皆可,返回 boolean 表示是否有结果集,再分别用 getResultSet/getUpdateCount 取。
追问四:事务是 JDBC 还是数据库的?
事务由数据库实现,JDBC 通过 setAutoCommit(false)、commit()、rollback() 控制边界。连接池会做「事务状态清理」,避免归还的连接带着未提交事务。
追问五:ResultSet 默认只能向前读吗?
默认 TYPE_FORWARD_ONLY,只能 next() 前进。需要滚动或改数据时用 createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE),但代价是驱动要在客户端缓存结果集。
小结
JDBC 的七步是一条不能跳的流水线:连、编、绑、执、读、关。所有 ORM 框架都是在这条流水线上加缓存、加映射、加事务代理,而真正和数据库打交道的动作,一步都没少。