Java 面向对象 · 交互课程

数据库编程 JDBC

数据库编程 JDBC

目标:理解程序为什么需要数据库、JDBC 由「接口 + 厂商实现」构成的多态结构;掌握 JDBC 五步时序(加载驱动→建立连接→创建语句→执行 SQL→结果与关闭);会用 Statement/ResultSet 完成增删改查,理解 PreparedStatement 防注入与事务 commit/rollback。

一、为什么需要数据库

二、JDBC 概述:驱动与 DriverManager(接口回顾 · 多态)

JDBC 的核心是一组接口:Driver、Connection、Statement、ResultSet……,具体由数据库厂商实现(MySQL 提供 com.mysql.jdbc.*)。DriverManager 是静态工厂:管理已注册的驱动,并按 URL 前缀挑选合适的驱动去建连。

把模块3 学的多态落到真实 API——JDBC 驱动正是绝佳范例:

一句话:编译期看接口、运行期定实现。换数据库只需换驱动 jar 与 URL,业务代码几乎不用改——这就是接口 + 多态带来的可替换性。

三、JDBC 五步时序

一次完整的 JDBC 操作遵循固定五步:① 加载驱动 → ② 建立连接 → ③ 创建语句 → ④ 执行 SQL → ⑤ 处理结果并关闭。资源释放顺序与创建相反(先关 rs/stmt,再关 conn)。下面按时序逐步高亮每条消息:

交互实验:JDBC 五步时序图(JDBCSequenceDiagram)

接口 · 多态

JDBC 里 Driver / Connection / Statement / ResultSet 都是接口,MySQL 驱动提供具体实现。 下面按「加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 结果与关闭」逐步高亮每条消息, 看调用如何在各对象之间流动、运行期如何落到具体实现。

① 加载驱动② 建立连接③ 创建语句④ 执行 SQL⑤ 结果与关闭
第 1 / 12 步 · ① 加载驱动
Class.forName("com.mysql.jdbc.Driver")—— 触发驱动类的 static 块,把自身登记进 DriverManager
应用程序main()DriverManager静态工厂Driver接口 · com.mysql.jdbc.DriverConnection接口 · ConnectionImplStatement接口 · StatementImplResultSet接口 · ResultSetImplClass.forName

接口与具体实现(多态示例)

接口MySQL 驱动实现作用
Drivercom.mysql.jdbc.Driver连接数据库的入口,负责自注册
ConnectionConnectionImpl一条到数据库的连接会话
StatementStatementImpl执行 SQL、拿结果集的句柄
ResultSetResultSetImpl查询返回的行集,可逐行遍历

经典 JDBC 五步代码

// ① 加载驱动:触发其 static 块完成自注册
Class.forName("com.mysql.jdbc.Driver");

// ② 建立连接:DriverManager 按 URL 选中具体驱动实现(多态)
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/mytest", "root", "");

// ③ 创建语句
Statement stmt = conn.createStatement();

// ④ 执行 SQL
stmt.executeUpdate("insert into guestbook(username,title) values('tom','from tom')");
ResultSet rs = stmt.executeQuery("select * from guestbook");

// ⑤ 处理结果 + 关闭(顺序与创建相反)
while (rs.next()) {
    System.out.println(rs.getInt("id") + " " + rs.getString("username"));
}
rs.close();
stmt.close();
conn.close();

JDBC / 多态要点

  • JDBC 的核心是接口:Driver / Connection / Statement / ResultSet 都是接口,厂商提供具体实现——编译期依赖接口、运行期由 DriverManager 按 URL 选中实现,正是模块3 学的多态落到真实 API;
  • 五步时序:加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 处理结果并关闭;资源释放顺序与创建相反(先 rs/stmt,再 conn);
  • Class.forName 只负责触发驱动类的 static 块完成自注册;JDBC 4.0 起可由 SPI 自动发现而省略,但显式写出更稳妥、便于定位问题;
  • 拼接 SQL 字符串有 SQL 注入风险:应使用 PreparedStatement 预编译 + 占位符 ?,把数据与命令分离(见下一节)。

四、CRUD:增删改查

操作SQL用的方法
Create(增)INSERT INTO guestbook(username,title) VALUES('tom','from tom')executeUpdate(sql)
Read(查)SELECT * FROM guestbookexecuteQuery(sql) → ResultSet
Update(改)UPDATE guestbook SET title=? WHERE id=?executeUpdate(sql)
Delete(删)DELETE FROM guestbook WHERE id=?executeUpdate(sql)

下面的实验让你在 guestbook 表上直接试 CRUD:点预设或自己写一条 SQL(INSERT / UPDATE / DELETE / SELECT),点「执行」——新增的行变绿、被改的行变黄、删掉的行红色划掉,受影响行数像 executeUpdate() 一样返回;SELECT 则单独展示结果集。注意 UPDATE / DELETE 必须带 WHERE 条件:

交互实验:CRUD 增删改查实时演示(CRUDVisualizer)

executeUpdate / executeQuery

在内存里的 guestbook(留言板)表上直接试 CRUD:点预设或自己写一条 SQL(INSERT / UPDATE / DELETE / SELECT), 点「执行」——新增行变绿、修改行变黄、删除的行红色划掉,受影响行数像 executeUpdate() 一样返回。

共 3 行 · id 自动编号

guestbook 表(当前状态)

id自动编号userid用户 IDusername昵称title留言标题contents留言内容
1u001tomfrom tom大家好,这是我的第一条留言。
2u002jerryRe: from tom很高兴认识你,tom!
3u003lucy课程很棒JDBC 时序图讲得很清楚。
方法映射:增 / 改 / 删 → executeUpdate(sql)(返回受影响行数,不返回结果集); 查 → executeQuery(sql)(返回 ResultSet,用 rs.next() 逐行读)。
ResultSet rs = stmt.executeQuery("select * from guestbook");
while (rs.next()) {
    System.out.println(rs.getInt("id") + " " + rs.getString("username"));
}
rs.close();

五、预编译语句 PreparedStatement:防 SQL 注入

拼接字符串的风险:若把用户输入直接拼进 SQL——"insert into guestbook(username,title) values('" + user + "', ...)",当 user 来自外部输入且含单引号 ' 时,就能被构造出额外的 SQL(SQL 注入),轻则改数据、重则拖库删库。

PreparedStatement 从机制上避免:先给带占位符 ? 的 SQL 预编译,再用 setString(1, ...) 传值——数据与命令分离,输入不会被当作 SQL 执行:

String sql = "insert into guestbook(username,title) values(?,?)";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, user);    // 参数按 ? 的顺序绑定
ps.setString(2, title);
int n = ps.executeUpdate();   // 返回受影响行数

好处:防注入 + 可复用(同一条 SQL 多次执行)+ 数据库可对预编译语句做优化。

交互实验:预编译语句实验台(PreparedStatementLab)

SQL 模板 SELECT * FROM users WHERE name = ?—— 填一个 name,看绑定版如何把它当数据;再试注入样例,对比拼接版怎样被改变结构。

字符串拼接(危险)

SELECT * FROM users WHERE name = '张三'

正常值下看不出问题——但攻击者输入带引号的值时,结构就被改了。

参数绑定 setString(安全)

SELECT * FROM users WHERE name = ?
    → 参数[0] = "张三"(数据,不是代码)

✓ SQL 结构永远不变:值只是被当作数据字面量传进去——admin' -- 也匹配不到任何行,注入失效。

预编译的三件好处:防注入(结构与数据分离)+ 可复用(同一条 SQL 反复 setParameter 执行)+ 数据库优化(预编译计划缓存)。

六、事务处理:commit / rollback

事务是一组必须「全成功或全失败」的操作。默认每条 SQL 自动提交;con.setAutoCommit(false) 关闭后,多条语句组成一个事务,用 con.commit() 提交、con.rollback() 回滚。

典型场景(转账):A 扣款 + B 加款必须一起成功;若 B 失败,rollback() 让 A 的扣款也撤销,保证数据一致:

con.setAutoCommit(false);
try {
    // ... 多条 SQL(如转账的「扣款」「加款」两步)
    con.commit();      // 全部成功 → 提交
} catch (SQLException e) {
    con.rollback();    // 任一步失败 → 回滚到事务开始前的状态
}

注意两层数据:事务未提交前,变更只有本事务自己看得见(pending),其他查询看到的仍是旧的已提交数据;commit() 之后两者才一致。下面的实验用「A 转给 B 300 元」逐步走查两种结局——全部成功就 commit、中途失败就 rollback:

交互实验:事务 commit / rollback 对比(TransactionDemo)

setAutoCommit · commit/rollback

转账场景:A(Tom,1000 元)转给 B(Jerry,500 元)300 元——A 扣款 + B 加款必须全成功或全失败。 选一个场景,用「下一步」逐步走查:注意已提交数据与事务内部状态两层数据何时一致、何时不同。

事务进行中(IN_TX)第 1 / 4 步 · 扣款、加款都执行成功,con.commit() 让变更永久生效

已提交数据(其他查询可见)

账号户主余额(元)
ATom1000
BJerry500

事务内部状态(pending,未提交)

账号户主余额(元)
ATom1000
BJerry500
已提交总额:1500 元 · 事务内总额:1500 元(commit / rollback 后总额恢复原值)

事务走查日志

  1. 1con.setAutoCommit(false)关闭自动提交,事务开始:此后每条变更都是「未提交(pending)」的,只有本事务自己看得见

commit vs rollback:最终已提交数据

✅ 全部成功 → commit()

Tom700 元
Jerry800 元

❌ 中途失败 → rollback()

Tom1000 元
Jerry500 元

经典写法:try / catch + commit / rollback

con.setAutoCommit(false);
try {
    // op1: A 扣款 300;op2: B 加款 300(多条 SQL)
    con.commit();      // 全部成功 → 提交
} catch (SQLException e) {
    con.rollback();    // 任一步失败 → 回滚到事务开始前的状态
} finally {
    con.setAutoCommit(true); // 恢复自动提交(建议)
}

事务要点

  • 事务是一组「全成功或全失败」的操作(原子性):转账的扣款 + 加款必须一起生效,不能只做一半;
  • 默认每条 SQL 自动提交;setAutoCommit(false) 关闭后,多条语句组成一个事务;
  • commit() 让 pending 变更永久生效;rollback() 丢弃全部未提交变更,恢复到事务开始前的状态;
  • 事务未提交前,变更只有本事务自己看得见——其他查询看到的仍是旧的已提交数据;
  • 最佳实践:catch 到异常 → rollback();finally 里恢复自动提交。

七、总结

思考题

  1. 为什么说 JDBC 是「多态」的绝佳范例?如果把 MySQL 换成 Oracle,业务代码需要改哪些地方、哪些不用改?
  2. 为什么资源释放顺序要与创建相反?如果先 con.close() 再调用 rs.next() 会发生什么?
  3. 用字符串拼接 SQL 有什么风险?PreparedStatement 是从哪个环节避免这个风险的?

学习小测验

学习小测验

Q1

在 JDBC 中,DriverManager.getConnection(...) 返回的 Connection 对象,其实际类型由什么决定?

Q2

调用 Class.forName("com.mysql.jdbc.Driver") 的主要作用是( )

Q3

执行 SELECT 查询并返回一个 ResultSet 的方法是( )

Q4

对 guestbook 表执行 INSERT / UPDATE / DELETE 时,应调用( )

Q5

下列能从根本上防止 SQL 注入的做法是( )

Q6

JDBC 中释放资源的正确顺序是( )

Q7

guestbook 表里只有 id=1、2、3 三行,执行 stmt.executeUpdate("update guestbook set title='x' where id=9") 的返回值是( )

Q8

对 UPDATE / DELETE 语句,为什么强烈建议带上 WHERE 条件?

Q9

执行 con.setAutoCommit(false) 之后,紧接着执行的 UPDATE 语句会怎样?

Q10

转账事务里 A 扣款成功、B 加款时抛出异常,正确的处理是( )