🪟 第20章 Java与SQL Server实现学生成绩管理系统
📄 一句话概括

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 秒实验

  1. 把第②条里的引号去掉,看结果从 0 变成几。 这就是「一个引号之差」。
  2. 把第④条的 ROLLBACK 换成 COMMIT,看班级数怎么变。 体会「commit 是唯一让数据落地的那一句」。
  3. 想一想:如果 rs、ps、conn 只关了 conn,会有什么后果? 连接池里的连接被占着不放,几百次请求之后池子空了,全站卡住。

常见误区

  1. 「WHERE id='?' 这样写也行」。 加引号就变成字符串常量,永远查不到,而且不报错。
  2. 「事务写不写 commit 无所谓」。 不写就等于没执行,而程序还会显示「保存成功」。
  3. 「连接不用关,垃圾回收会处理」。 GC 不保证及时,连接池几轮就空了。 「用了半天就变慢」多半是这个。
自测三问(点开对答案) 1. JDBC 五步是哪五步? —— **加载驱动、建连接、准备语句、执行、关三个对象。** **最容易漏的是最后一步。** 2. 参数顺序反了会怎样? —— **不报错**——**类型不同就查不到,类型相同就查出错的。** **这是 JDBC 最隐蔽的错误。** 3. `setAutoCommit(false)` 之后最容易漏什么? —— **两件:`commit()`,和最后恢复 `setAutoCommit(true)`。** **前者导致数据没进去,后者污染连接池。**

下一篇 20.5,写公共模块: JavaBean 那堆 getter/setter 不是风格,是契约——少一个,页面上那个字段就是空的。

相关笔记