← Backend / Java

23_Java JDBC与事务

DataSource、参数化查询、JDBC 事务、资源关闭和并发冲突。

Java JDBC 与事务

学习目标:不依赖 ORM 也能解释连接、参数化 SQL、事务、资源关闭和并发冲突。

1. JDBC 的对象职责

DataSource 提供连接,Connection 表示一次数据库会话,PreparedStatement 持有带参数占位符的 SQL,ResultSet 逐行读取结果。真实服务通常通过连接池提供 DataSource;不要每个请求都手工配置驱动和新建物理连接。

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import javax.sql.DataSource;

class NoteRepository {
    private final DataSource dataSource;
    NoteRepository(DataSource dataSource) { this.dataSource = dataSource; }

    Optional<String> findTitle(long id, long ownerId) throws SQLException {
        String sql = "SELECT title FROM notes WHERE id = ? AND owner_id = ?";
        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setLong(1, id);
            statement.setLong(2, ownerId);
            try (ResultSet rows = statement.executeQuery()) {
                return rows.next()
                        ? Optional.of(rows.getString("title"))
                        : Optional.empty();
            }
        }
    }
}

? 绑定的是值,不能用来替换表名或排序关键字;动态排序字段必须由服务端白名单选择。查询同时限定 owner_id 是逐资源授权的一部分,但调用前仍须确认 ownerId 来自已认证身份,而非客户端任意传入。

2. 手工事务边界

JDBC 连接默认通常为自动提交;多条写入要同成同败时,关闭自动提交并明确提交或回滚。连接属于资源,事务属于一次业务动作,不应把连接作为全局共享变量。

try (Connection connection = dataSource.getConnection()) {
    connection.setAutoCommit(false);
    try {
        // 使用同一个 connection 执行所有属于该事务的 SQL。
        insertNote(connection, ownerId, title);
        insertAudit(connection, ownerId, "create-note");
        connection.commit();
    } catch (SQLException | RuntimeException failure) {
        try { connection.rollback(); }
        catch (SQLException rollbackFailure) { failure.addSuppressed(rollbackFailure); }
        throw failure;
    }
}

这里的 insertNote、insertAudit 是需自行实现的方法,展示事务结构而非完整可运行类。连接池归还连接时的自动提交状态、隔离级别重置由池管理,仍应按所用连接池文档确认。Spring 托管事务时,应让同一事务中的仓储操作使用受框架管理的连接,不要另开连接绕过事务。

3. 约束、冲突与隔离

参数化查询防注入,唯一约束防并发重复,外键保证引用完整性,事务保证一组操作的原子性;它们解决不同问题。先查“邮箱不存在”再插入仍有竞态,必须依赖数据库唯一约束并处理冲突。编辑冲突可在更新条件中带版本号,受影响行数为 0 时返回业务冲突或未找到。

隔离级别与锁行为由数据库决定,不要以为 setAutoCommit(false) 就能阻止所有并发问题。为长查询设置超时;缩短事务内的外部网络调用,避免占用连接和锁。

4. 实验与参考

准备隔离数据库:运行迁移、插入两个用户与各自笔记,测试用户 A 无法读用户 B 的 ID;再故意让审计插入失败,验证笔记写入也回滚。