Appearance
第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
| 特性 | C | Java JDBC | ORM |
|---|---|---|---|
| 数据库访问 | C API(mysql_real_query) | JDBC 接口 | MyBatis/JPA |
| 驱动 | 厂商 C 库 | 厂商 JDBC 驱动 | 基于 JDBC |
| 参数化 | 手动转义 | PreparedStatement | 自动 |
对 C 程序员:JDBC 对应 C 的数据库客户端库(libmysql),但用接口+驱动解耦,换数据库只换驱动。MyBatis/JPA 在 JDBC 上封装(SQL 映射/对象关系),减少样板代码。
💡 九、最佳实践
- 永远 PreparedStatement,防注入。
- try-with-resources 释放连接/语句/结果集。
- 用连接池(HikariCP),别裸 DriverManager。
- 手动事务保证原子性,异常 rollback。
- 批量用 addBatch,别循环单条。
- 避免 N+1,用 JOIN/IN 批量。
- 生产用 MyBatis/JPA,减少 JDBC 样板。
📝 练习预告
完成 练习/Ex43_JDBC.java 中的 6 道题:
- 连接与查询(PreparedStatement)
- 防注入对比(Statement vs PreparedStatement)
- 增删改(executeUpdate)
- 事务管理(转账)
- 连接池使用
- 综合:批处理插入
完成后对比 答案/Sol43.java,查看逐行讲解与多解法。
📖 十、事务隔离级别
事务不只是 commit 和 rollback,还涉及隔离级别。
常见并发问题:
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 | 网络或数据库不可达 | 网络、端口、数据库状态 |
SQLSyntaxErrorException | SQL 语法错误 | 打印最终 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、缓存