数据库编程 JDBC
数据库编程 JDBC
目标:理解程序为什么需要数据库、JDBC 由「接口 + 厂商实现」构成的多态结构;掌握 JDBC 五步时序(加载驱动→建立连接→创建语句→执行 SQL→结果与关闭);会用
Statement/ResultSet完成增删改查,理解PreparedStatement防注入与事务commit/rollback。
一、为什么需要数据库
- 数据要持久化:内存里的数据随进程结束而消失;用户留言、订单、账号等必须落到磁盘上的数据库,重启后仍在;
- 关系数据库用表组织数据:一行 = 一条记录,一列 = 一个字段。以本课程的
guestbook(留言板)表为例——id / userid / username / title / contents五列,每条留言就是一行; - Java 通过 JDBC 访问数据库:JDBC(Java Database Connectivity)是一组统一 API,屏蔽不同数据库厂商的差异——一套代码可对接 MySQL、Oracle、PostgreSQL。
二、JDBC 概述:驱动与 DriverManager(接口回顾 · 多态)
JDBC 的核心是一组接口:Driver、Connection、Statement、ResultSet……,具体由数据库厂商实现(MySQL 提供 com.mysql.jdbc.*)。DriverManager 是静态工厂:管理已注册的驱动,并按 URL 前缀挑选合适的驱动去建连。
把模块3 学的多态落到真实 API——JDBC 驱动正是绝佳范例:
Driver是接口,MySQL 的com.mysql.jdbc.Driver实现它,并在自己的static块里调用DriverManager.registerDriver(new Driver())完成自注册;DriverManager.getConnection(...)声明返回Connection(接口),但运行期才由具体驱动返回真正的实现对象ConnectionImpl;conn.createStatement()返回Statement(接口)→ 实际是StatementImpl;stmt.executeQuery(...)返回ResultSet(接口)→ 实际是ResultSetImpl。
一句话:编译期看接口、运行期定实现。换数据库只需换驱动 jar 与 URL,业务代码几乎不用改——这就是接口 + 多态带来的可替换性。
三、JDBC 五步时序
一次完整的 JDBC 操作遵循固定五步:① 加载驱动 → ② 建立连接 → ③ 创建语句 → ④ 执行 SQL → ⑤ 处理结果并关闭。资源释放顺序与创建相反(先关 rs/stmt,再关 conn)。下面按时序逐步高亮每条消息:
交互实验:JDBC 五步时序图(JDBCSequenceDiagram)
接口 · 多态JDBC 里 Driver / Connection / Statement / ResultSet 都是接口,MySQL 驱动提供具体实现。 下面按「加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 结果与关闭」逐步高亮每条消息, 看调用如何在各对象之间流动、运行期如何落到具体实现。
接口与具体实现(多态示例)
| 接口 | MySQL 驱动实现 | 作用 |
|---|---|---|
| Driver | com.mysql.jdbc.Driver | 连接数据库的入口,负责自注册 |
| Connection | ConnectionImpl | 一条到数据库的连接会话 |
| Statement | StatementImpl | 执行 SQL、拿结果集的句柄 |
| ResultSet | ResultSetImpl | 查询返回的行集,可逐行遍历 |
经典 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 guestbook | executeQuery(sql) → ResultSet |
| Update(改) | UPDATE guestbook SET title=? WHERE id=? | executeUpdate(sql) |
| Delete(删) | DELETE FROM guestbook WHERE id=? | executeUpdate(sql) |
- 增删改用
executeUpdate(sql):返回受影响的行数(>0表示成功),不返回结果集; - 查询用
executeQuery(sql):返回一个ResultSet,用rs.next()逐行推进、rs.getString("username")/rs.getInt("id")按列名取值。
下面的实验让你在 guestbook 表上直接试 CRUD:点预设或自己写一条 SQL(INSERT / UPDATE / DELETE / SELECT),点「执行」——新增的行变绿、被改的行变黄、删掉的行红色划掉,受影响行数像 executeUpdate() 一样返回;SELECT 则单独展示结果集。注意 UPDATE / DELETE 必须带 WHERE 条件:
交互实验:CRUD 增删改查实时演示(CRUDVisualizer)
executeUpdate / executeQuery在内存里的 guestbook(留言板)表上直接试 CRUD:点预设或自己写一条 SQL(INSERT / UPDATE / DELETE / SELECT), 点「执行」——新增行变绿、修改行变黄、删除的行红色划掉,受影响行数像 executeUpdate() 一样返回。
guestbook 表(当前状态)
| id自动编号 | userid用户 ID | username昵称 | title留言标题 | contents留言内容 |
|---|---|---|---|---|
| 1 | u001 | tom | from tom | 大家好,这是我的第一条留言。 |
| 2 | u002 | jerry | Re: from tom | 很高兴认识你,tom! |
| 3 | u003 | lucy | 课程很棒 | 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 加款必须全成功或全失败。 选一个场景,用「下一步」逐步走查:注意已提交数据与事务内部状态两层数据何时一致、何时不同。
已提交数据(其他查询可见)
| 账号 | 户主 | 余额(元) |
|---|---|---|
| A | Tom | 1000 |
| B | Jerry | 500 |
事务内部状态(pending,未提交)
| 账号 | 户主 | 余额(元) |
|---|---|---|
| A | Tom | 1000 |
| B | Jerry | 500 |
事务走查日志
- 1con.setAutoCommit(false)关闭自动提交,事务开始:此后每条变更都是「未提交(pending)」的,只有本事务自己看得见
commit vs rollback:最终已提交数据
✅ 全部成功 → commit()
| Tom | 700 元 |
| Jerry | 800 元 |
❌ 中途失败 → rollback()
| Tom | 1000 元 |
| Jerry | 500 元 |
经典写法: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 里恢复自动提交。
七、总结
- JDBC = 一组接口 + 厂商实现;
DriverManager按 URL 在运行期选中具体驱动——多态落到真实 API; - 五步时序固定:加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 结果与关闭;资源释放顺序与创建相反;
- 防注入用 PreparedStatement(预编译 +
?占位符);数据一致性靠事务(commit/rollback)。
思考题
- 为什么说 JDBC 是「多态」的绝佳范例?如果把 MySQL 换成 Oracle,业务代码需要改哪些地方、哪些不用改?
- 为什么资源释放顺序要与创建相反?如果先
con.close()再调用rs.next()会发生什么? - 用字符串拼接 SQL 有什么风险?
PreparedStatement是从哪个环节避免这个风险的?
学习小测验
学习小测验
在 JDBC 中,DriverManager.getConnection(...) 返回的 Connection 对象,其实际类型由什么决定?
调用 Class.forName("com.mysql.jdbc.Driver") 的主要作用是( )
执行 SELECT 查询并返回一个 ResultSet 的方法是( )
对 guestbook 表执行 INSERT / UPDATE / DELETE 时,应调用( )
下列能从根本上防止 SQL 注入的做法是( )
JDBC 中释放资源的正确顺序是( )
guestbook 表里只有 id=1、2、3 三行,执行 stmt.executeUpdate("update guestbook set title='x' where id=9") 的返回值是( )
对 UPDATE / DELETE 语句,为什么强烈建议带上 WHERE 条件?
执行 con.setAutoCommit(false) 之后,紧接着执行的 UPDATE 语句会怎样?
转账事务里 A 扣款成功、B 加款时抛出异常,正确的处理是( )