JDBC 就五步:加载驱动、连、发、取、关——全书所有数据库代码都是这五步的变体。
问号占位符按位置对,顺序反了不报错、只是查不到东西,甚至查出错的。
setAutoCommit(false) 之后一定要在 finally 里恢复成 true,
否则连接一还池就带着脏状态。
前面三章都是「数据库怎么设计」,这一节第一次讲「程序怎么连数据库」。
桌面那边用的是 ADO,Web 这边用的是 JDBC——名字不同,五步一样。
教材 20.4 分三小节:主要技术、JDBC 连接 SQL Server、结果集与事务。
「结果集与事务」那一小节最容易看漏——
setAutoCommit 这一句,是全章唯一一个「不写就出隐性 bug」的地方。
沙箱没有 JDK、没有 SQL Server 驱动,所以这一节的 Java 原码全部标「不执行」,
对应的行为用 Python 的 sqlite3 模块逐条复刻并真跑——
复刻的是「参数怎么绑、结果怎么取、事务怎么开」,不是「类名怎么拼」。
一、五步走,只有 SQL 和参数在变
| 步 | 做什么 | 最容易漏的地方 |
|---|---|---|
| 1 | Class.forName(驱动) |
新版 JDBC 可以省略,老代码必须写 |
| 2 | DriverManager.getConnection(url) |
连接串里带库名 |
| 3 | prepareStatement(sql) |
用 ? 不用拼接 |
| 4 | executeQuery / executeUpdate |
选错方法会抛异常 |
| 5 | rs.close(); ps.close(); conn.close(); |
三个都要关,顺序反的 |
五步里最容易漏的是最后一步「关」。
ResultSet、Statement、Connection 三个都要关,而且顺序是反的(先开的后关)。
不关的后果不是「报错」,是「连接池慢慢耗尽」。
表现是「系统用了半天以后越来越慢,重启就好」——
这个现象特别有欺骗性,很多人以为是数据库性能问题,其实是自己漏了 close。
现代写法用 try-with-resources,括号里的对象自动关——
但教材那版 Java 可能不支持,所以手写 finally 也行。关键是「一定要有关的地方」。
二、问号占位符:按位置对,不按名字
| 写法 | 对不对 | 为什么 |
|---|---|---|
WHERE stu_id=? AND subject=? |
对 | 两个 ? 按顺序绑 |
WHERE stu_id='?' |
错 | 加了引号就变成字符串常量 |
WHERE stu_id=? 绑 setString |
能跑但危险 | 类型隐式转换,可能走不了索引 |
第二种错法很常见:把 ? 用引号包起来,以为像模板占位符。
结果 SQL 变成「字段等于一个问号字符」,永远查不到,而且不报错。
第三种是「能跑但危险」。 类型不匹配时数据库会隐式转换,
转换之后索引可能用不上——数据量一大就慢,而你不会想到是这一行的问题。
三、三种执行方法:选错会抛异常
| 方法 | 用来干什么 | 返回什么 |
|---|---|---|
executeQuery |
SELECT |
ResultSet |
executeUpdate |
INSERT/UPDATE/DELETE |
影响行数 |
execute |
不确定是什么语句时 | boolean(有没有结果集) |
executeUpdate 返回的影响行数是「免费的自检」。
UPDATE ... WHERE id=? 返回 0,说明这一行不存在或者值没变——
一句 if (n == 0) 提示「记录不存在」,就能挡住很多「保存成功但其实没保存」。
这一点在 JSP 项目里特别重要,因为页面刷新后用户看不出「到底存没存」。
一句影响行数判断,比任何提示文案都可靠。
四、事务:默认是「每条语句自动提交」
| 设置 | 含义 | 忘了会怎样 |
|---|---|---|
setAutoCommit(true)(默认) |
每条语句各自提交 | — |
setAutoCommit(false) |
要手动 commit |
不写 commit 就等于没执行 |
恢复 true |
还池前复位 | 下一个请求拿到「不自动提交」的连接 |
「自动提交」是 JDBC 的默认值,也是最容易被忽略的默认值。
它意味着你不写 commit 也是「提交了」——
所以第一次写事务的人常常以为「我明明没提交,怎么数据进去了」。
反过来更坑:setAutoCommit(false) 之后忘了 commit,数据就真的没进去,
而且程序不报错——用户看到「保存成功」,实际什么都没存。
恢复 setAutoCommit(true) 这一步,教材里经常省略。
桌面程序里问题不大(连接用完就关),Web 项目里问题很大——
因为连接是还回池子、给下一个请求用的。
五、动手 Demo:JDBC 的四个真相,逐个验一遍
这一段把 JDBC 里四个「不报错但出错」的点逐个跑出来:
参数顺序反、占位符加引号、executeUpdate 返回 0、自动提交的默认值。
四条里三条都是「程序不崩,结果是错的」——这才是 JDBC 真正难的地方。
# ① 参数顺序反:不报错,只是查不到
ok = c.execute('SELECT score FROM tb_score'
' WHERE stu_id = ? AND subject = ?', (1, '数学')).fetchone()
bad = c.execute('SELECT score FROM tb_score'
' WHERE stu_id = ? AND subject = ?', ('数学', 1)).fetchone()
print('顺序对:', ok, ' 顺序反:', bad)
# ② 占位符加了引号:永远查不到
n = c.execute("SELECT COUNT(*) FROM tb_student WHERE sex = '?'",
()).fetchone()[0]
print('加引号:', n, '行')
# ③ executeUpdate 返回 0:值没变或行不存在
print('改存在的:', c.execute('UPDATE tb_student SET sname=?'
' WHERE sno=?', ('张一鸣', '20260101')).rowcount)
print('改不存在的:', c.execute('UPDATE tb_student SET sname=?'
' WHERE sno=?', ('张三', '99999999')).rowcount)
# ④ 自动提交:不 commit 就没进去
c.execute('BEGIN')
c.execute("INSERT INTO tb_class (cname, grade) VALUES ('T','2026')")
c.execute('ROLLBACK')
print('回滚后班级数:', c.execute('SELECT COUNT(*) FROM tb_class').fetchone()[0])
实跑输出:
--------------------------------------------------------------------
关键技术:JDBC 的四个真相
--------------------------------------------------------------------
① 五步走:加载驱动、连、发、取、关
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
| String url = "jdbc:sqlserver://localhost:1433;databaseName=db_score";
| Connection conn = DriverManager.getConnection(url, "sa", "你的密码");
| PreparedStatement ps = conn.prepareStatement(
| "SELECT sno, sname FROM tb_student WHERE class_id = ?");
| ps.setInt(1, 1);
| ResultSet rs = ps.executeQuery();
| while (rs.next()) {
| System.out.println(rs.getString("sno") + rs.getString("sname"));
| }
| rs.close(); ps.close(); conn.close();
这五步全书反复出现,只有 SQL 和参数在变。
五步里最容易漏的是最后一步「关」。
ResultSet、Statement、Connection 三个都要关,而且顺序是反的(先开的后关)。
不关的后果不是「报错」,是「连接池慢慢耗尽」——表现为「系统用了半天以后越来越慢,重启就好」。
这个现象特别有欺骗性,很多人以为是数据库性能问题。
② 问号占位符是按位置对的,不是按名字
--------------------------------------------------------------------
ps = conn.prepareStatement(
"SELECT * FROM tb_score WHERE stu_id = ? AND subject = ?")
ps.setInt(1, stuId); // 第一个 ? 是 stu_id
ps.setString(2, subject); // 第二个 ? 是 subject
参数顺序对 → 92.0
参数顺序反 → 查不到(类型不匹配,条件永不成立)
这是 JDBC 最容易出的错,而且不报错。
setInt 和 setString 的顺序一反,SQL 照样执行,只是查不到东西。
更麻烦的是:如果两列类型一样(比如两个都是字符串),顺序反了照样能查出结果——只是结果是错的。
这种错最难发现,因为「有数据」比「没数据」更像正常。
对策只有一条:占位符超过两个时,把 SQL 和 set 写在挨着的两行,中间别插别的代码。
隔了十行再看,谁都对不上。
③ 三种执行方法,别用错
--------------------------------------------------------------------
| 方法 | 用来干什么 | 返回什么 |
|---|---|---|
| executeQuery | SELECT | ResultSet |
| executeUpdate | INSERT/UPDATE/DELETE | 影响行数 |
| execute | 不确定是什么语句时 | boolean(有没有结果集) |
executeUpdate 返回的影响行数是「免费的自检」。
UPDATE ... WHERE id=? 返回 0,说明这一行不存在或者值没变——一句 if (n == 0) 提示「记录不存在」,
就能挡住很多「保存成功但其实没保存」。
这一点在 JSP 项目里特别重要,因为页面刷新后用户看不出「到底存没存」。
一句影响行数判断,比任何提示文案都可靠。
④ 事务:JDBC 默认是「每条语句自动提交」
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| conn.setAutoCommit(false); // 关掉自动提交
| try {
| ps1.executeUpdate(); // 第一条
| ps2.executeUpdate(); // 第二条
| conn.commit(); // 一起提交
| } catch (Exception e) {
| conn.rollback(); // 一起回滚
| } finally {
| conn.setAutoCommit(true); // 恢复,否则连接池里的连接状态是脏的
| }
最后那句 setAutoCommit(true) 经常被忘,连接一还池就带着「不自动提交」的状态。
「自动提交」是 JDBC 的默认值,也是最容易被忽略的默认值。
它意味着你不写 commit 也是「提交了」——所以第一次写事务的人常常以为「我明明没提交,
怎么数据进去了」。
恢复 setAutoCommit(true) 这一步,教材里经常省略。
在桌面程序里省略问题不大(连接用完就关),在 Web 项目里问题很大——因为连接是还回池子、
给下一个请求用的。
第②条最值得看:加引号的问号不是占位符,是一个普通字符串。
SQL 变成「性别等于问号字符」,永远查不到,而程序一句错都不报。
这个错在真实项目里活得特别久,因为「查不到」经常被当成「没数据」。
第③条两个结果一个是 1 一个是 0。 0 就是那句「记录不存在」的来源——
注意也可能是「值没变」,所以提示语写「可能已被删除」,别写「一定不存在」。
第④条演示的是「回滚之后数没变」,等价于 JDBC 里忘 commit 的效果。
SQLite 的 BEGIN/COMMIT 对应 JDBC 的 setAutoCommit(false)/commit(),
语义一样,写法不同。
30 秒实验
- 把第②条里的引号去掉,看结果从 0 变成几。 这就是「一个引号之差」。
- 把第④条的
ROLLBACK换成COMMIT,看班级数怎么变。 体会「commit 是唯一让数据落地的那一句」。 - 想一想:如果
rs、ps、conn只关了conn,会有什么后果? 连接池里的连接被占着不放,几百次请求之后池子空了,全站卡住。
常见误区
- 「
WHERE id='?'这样写也行」。 加引号就变成字符串常量,永远查不到,而且不报错。 - 「事务写不写
commit无所谓」。 不写就等于没执行,而程序还会显示「保存成功」。 - 「连接不用关,垃圾回收会处理」。 GC 不保证及时,连接池几轮就空了。 「用了半天就变慢」多半是这个。
自测三问(点开对答案)
1. JDBC 五步是哪五步? —— **加载驱动、建连接、准备语句、执行、关三个对象。** **最容易漏的是最后一步。** 2. 参数顺序反了会怎样? —— **不报错**——**类型不同就查不到,类型相同就查出错的。** **这是 JDBC 最隐蔽的错误。** 3. `setAutoCommit(false)` 之后最容易漏什么? —— **两件:`commit()`,和最后恢复 `setAutoCommit(true)`。** **前者导致数据没进去,后者污染连接池。**下一篇 20.5,写公共模块: JavaBean 那堆 getter/setter 不是风格,是契约——少一个,页面上那个字段就是空的。
- ☐ (本篇完,下一篇 20.5 公共模块设计)