说说 JDBC 的执行步骤?

#Java#面试八股

说说 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
prepareStatementStatementHandler.prepare()
setXxxParameterHandler + TypeHandler
executeStatementHandler.update/query + Executor
rs.next + getXxxResultSetHandler + 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 框架都是在这条流水线上加缓存、加映射、加事务代理,而真正和数据库打交道的动作,一步都没少。

笔记加载中…