Skip to content

第43课:JDBC

🎯 学习目标

  • 理解 JDBC 的作用与核心 API(DriverManager/Connection/Statement/ResultSet)
  • 掌握 PreparedStatement 防止 SQL 注入
  • 掌握事务管理、try-with-resources 释放资源
  • 了解连接池(HikariCP/Druid)与批处理
  • 识别常见陷阱(SQL 注入、资源泄漏、事务)

📖 一、概念讲解:JDBC 是什么

1. JDBC 的角色

JDBC(Java Database Connectivity) 是 Java 访问关系数据库的标准 API。它是一套接口(java.sql),各数据库厂商提供驱动实现。

Java 应用 ──JDBC接口──→ 驱动(MySQL/Oracle/PG)──→ 数据库

JDBC 让 Java 代码用统一 API 操作不同数据库(换驱动即可),是 ORM(MyBatis/JPA)的底层。

2. 核心 API

类/接口作用
DriverManager管理驱动、获取连接
Connection数据库连接(事务、创建 Statement)
Statement执行 SQL(静态)
PreparedStatement预编译 SQL(防注入,推荐)
ResultSet查询结果集
DataSource连接源(连接池基础)

📖 二、基本流程

java
// 1. 加载驱动(JDBC4+ 自动,可省)
Class.forName("com.mysql.cj.jdbc.Driver");

// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC";
try (Connection conn = DriverManager.getConnection(url, "user", "pwd");
     PreparedStatement ps = conn.prepareStatement("SELECT id, name FROM users WHERE id = ?")) {
    // 3. 设置参数
    ps.setInt(1, 100);
    // 4. 执行查询
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            int id = rs.getInt("id");
            String name = rs.getString("name");
        }
    }
}   // 5. 自动关闭(try-with-resources)

关键:Connection/PreparedStatement/ResultSet 都是 AutoCloseable,用 try-with-resources 自动关闭,避免泄漏。


📖 三、PreparedStatement 防注入

java
// ❌ Statement 拼接,SQL 注入风险
String name = request.getParameter("name");   // 用户输入 "a' OR '1'='1"
stmt.execute("SELECT * FROM users WHERE name = '" + name + "'");
// 实际执行:SELECT * FROM users WHERE name = 'a' OR '1'='1'  → 返回所有用户!

// ✅ PreparedStatement 预编译参数化
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, name);   // 参数转义,' 被当作普通字符

原理:PreparedStatement 预编译 SQL 结构(? 占位),参数单独传入并转义,SQL 结构不可被输入改变。永远用 PreparedStatement,别用 Statement 拼接


📖 四、增删改查与事务

java
// 增
try (PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO users(name, age) VALUES(?, ?)")) {
    ps.setString(1, "Alice");
    ps.setInt(2, 25);
    ps.executeUpdate();   // 返回影响行数
}

// 事务
try (Connection conn = dataSource.getConnection()) {
    conn.setAutoCommit(false);   // 开启事务
    try {
        // 多个 SQL 操作
        transfer(conn, from, to, amount);
        conn.commit();            // 提交
    } catch (Exception e) {
        conn.rollback();          // 回滚
    }
}

事务:默认自动提交(每条 SQL 一个事务)。手动事务:setAutoCommit(false) → 操作 → commit/rollback。保证多个操作原子性(如转账)。


📖 五、连接池

每次 getConnection 建物理连接很贵(TCP + 认证)。连接池预建一批连接复用:

java
// HikariCP(推荐,性能好)
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost/test");
config.setUsername("user"); config.setPassword("pwd");
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);

try (Connection conn = ds.getConnection()) { ... }   // 从池借连接,close 归还

原理:池维护连接,getConnection 借出,close 归还(不真关)。避免频繁建连。HikariCP 性能最优,Druid(阿里)监控功能强。


📖 六、批处理

java
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO t(v) VALUES(?)")) {
    for (int v : values) {
        ps.setInt(1, v);
        ps.addBatch();   // 加入批
    }
    ps.executeBatch();   // 一次执行多条
}

批量插入用 addBatch + executeBatch,一次网络往返插多条,远快于逐条 executeUpdate。


⚠️ 七、常见陷阱

陷阱1:SQL 注入

用 Statement 拼接用户输入 → 注入。永远 PreparedStatement 参数化。

陷阱2:资源泄漏

Connection/Statement/ResultSet 不关闭 → 连接泄漏,连接池耗尽。try-with-resources。

陷阱3:事务未提交

setAutoCommit(false) 后忘记 commit/rollback,连接归还池时事务状态未清理,影响后续。确保 commit 或 rollback。

陷阱4:连接池配置不当

池太大浪费资源,太小请求等待。按并发量调(如最大连接数 = 并发数 × 单请求持有时长)。

陷阱5:N+1 查询

循环里单条查询(查 N 次单条)而非批量。用 JOIN 或 IN 批量查。


🆚 八、Java vs C / ORM

特性CJava JDBCORM
数据库访问C API(mysql_real_query)JDBC 接口MyBatis/JPA
驱动厂商 C 库厂商 JDBC 驱动基于 JDBC
参数化手动转义PreparedStatement自动

对 C 程序员:JDBC 对应 C 的数据库客户端库(libmysql),但用接口+驱动解耦,换数据库只换驱动。MyBatis/JPA 在 JDBC 上封装(SQL 映射/对象关系),减少样板代码。


💡 九、最佳实践

  1. 永远 PreparedStatement,防注入。
  2. try-with-resources 释放连接/语句/结果集。
  3. 用连接池(HikariCP),别裸 DriverManager。
  4. 手动事务保证原子性,异常 rollback。
  5. 批量用 addBatch,别循环单条。
  6. 避免 N+1,用 JOIN/IN 批量。
  7. 生产用 MyBatis/JPA,减少 JDBC 样板。

📝 练习预告

完成 练习/Ex43_JDBC.java 中的 6 道题:

  1. 连接与查询(PreparedStatement)
  2. 防注入对比(Statement vs PreparedStatement)
  3. 增删改(executeUpdate)
  4. 事务管理(转账)
  5. 连接池使用
  6. 综合:批处理插入

完成后对比 答案/Sol43.java,查看逐行讲解与多解法。


📖 十、事务隔离级别

事务不只是 commitrollback,还涉及隔离级别。

常见并发问题:

text
脏读:读到其他事务未提交的数据。
不可重复读:同一事务内两次读取同一行,结果不同。
幻读:同一事务内两次范围查询,行数不同。

JDBC 设置隔离级别:

java
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

常见级别:

隔离级别说明常见数据库
READ_UNCOMMITTED可能脏读很少使用
READ_COMMITTED只能读已提交Oracle 默认,PostgreSQL 默认
REPEATABLE_READ同事务重复读一致MySQL InnoDB 默认
SERIALIZABLE串行化,最严格性能最低

选择建议:

text
普通业务从数据库默认隔离级别开始。
转账、库存、订单状态变更要结合锁和唯一约束。
不要为了“更安全”直接 SERIALIZABLE,可能严重降低并发。
真正的并发正确性通常依赖事务、锁、唯一索引、版本号共同保证。

📖 十一、连接池配置要点

连接池不是越大越好。

关键参数:

text
maximumPoolSize:最大连接数。
minimumIdle:最小空闲连接。
connectionTimeout:获取连接最大等待时间。
idleTimeout:空闲连接多久回收。
maxLifetime:连接最大生命周期。
leakDetectionThreshold:连接泄漏检测阈值。

HikariCP 示例:

java
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/app");
config.setUsername("app");
config.setPassword("secret");
config.setMaximumPoolSize(20);
config.setConnectionTimeout(3000);
config.setMaxLifetime(30 * 60 * 1000L);
config.setLeakDetectionThreshold(10_000);

估算思路:

text
如果数据库最多承受 200 个连接,不能每个服务实例都配 200。
需要按实例数平摊连接数。
如果单次请求持有连接时间很长,连接池会更容易耗尽。
慢 SQL 会间接拖垮连接池。

排查连接池耗尽时看:

text
活跃连接数是否接近最大值。
等待连接的线程数。
慢 SQL 是否增多。
是否有连接泄漏。
事务是否长时间不提交。

🧪 十二、结果集映射实践

JDBC 查询的结果是 ResultSet,业务代码通常需要映射成对象。

示例:

java
public User mapUser(ResultSet rs) throws SQLException {
    User user = new User();
    user.setId(rs.getLong("id"));
    user.setName(rs.getString("name"));
    user.setAge(rs.getInt("age"));
    user.setCreatedAt(rs.getTimestamp("created_at").toInstant());
    return user;
}

注意点:

text
列名和对象字段名要明确对应。
timestamp、date、time 类型要统一时区策略。
getInt 读取 null 会返回 0,需要用 wasNull 判断。
金额不要用 double,使用 BigDecimal。
大字段不要一次性全部读入内存。

判断 nullable int:

java
int age = rs.getInt("age");
Integer nullableAge = rs.wasNull() ? null : age;

这也是 MyBatis、JPA 等框架替你处理的样板代码。


🛡 十三、SQL 注入的白名单场景

PreparedStatement 可以参数化值,但不能参数化表名、列名、排序方向。

错误示例:

java
String orderBy = request.getParameter("orderBy");
String sql = "SELECT * FROM users ORDER BY " + orderBy;

正确做法是白名单:

java
private static final Map<String, String> ORDER_COLUMNS = Map.of(
    "name", "name",
    "createdAt", "created_at",
    "id", "id"
);

String column = ORDER_COLUMNS.getOrDefault(input, "id");
String sql = "SELECT * FROM users ORDER BY " + column + " DESC";

原则:

text
值用 ? 参数化。
结构用白名单。
不要把用户输入直接拼进 SQL。

🧪 十四、大结果集与分页

一次查询过多数据会带来问题:

text
数据库扫描压力大。
网络传输慢。
应用内存暴涨。
ResultSet 长时间占用连接。

常见处理方式:

text
分页查询。
游标或 fetchSize。
按主键分批处理。
异步任务导出大数据。

按主键分批示例:

java
long lastId = 0;
int size = 1000;
while (true) {
    List<User> users = queryUsersAfterId(lastId, size);
    if (users.isEmpty()) {
        break;
    }
    for (User user : users) {
        handle(user);
        lastId = user.getId();
    }
}

相比 OFFSET 深分页,按主键游标在大表中通常更稳定。


🛠 十五、JDBC 排查清单

数据库访问异常时按下面顺序定位:

text
JDBC URL 是否正确。
驱动版本是否匹配数据库版本。
用户名密码是否正确。
网络和端口是否可达。
连接池是否耗尽。
SQL 是否慢。
事务是否未提交。
是否存在锁等待。
是否出现 SQL 注入风险。
ResultSet 是否正确关闭。

常见异常:

异常含义排查方向
SQLException: Access denied认证失败用户名、密码、权限
Communications link failure网络或数据库不可达网络、端口、数据库状态
SQLSyntaxErrorExceptionSQL 语法错误打印最终 SQL 和参数
Lock wait timeout锁等待超时事务、索引、并发更新
Connection is not available连接池取不到连接慢 SQL、泄漏、池太小

✅ 十六、掌握标准

学完本课后,应能做到:

text
能用 PreparedStatement 完成 CRUD。
能用 try-with-resources 正确关闭资源。
能手动控制事务并处理 rollback。
能解释 SQL 注入和参数化原理。
能配置并理解连接池关键参数。
能识别 N+1、慢 SQL、连接泄漏问题。
能处理 ResultSet 到对象的类型映射。
能知道什么时候应升级到 MyBatis 或 JPA。

JDBC 是数据库访问的地基。即使日常使用 MyBatis 或 JPA,也需要理解它,才能排查连接池、事务和 SQL 执行问题。


🎓 下一步

  • 第44课:MyBatis — SQL 映射、动态 SQL、缓存