简介:这份实验报告面向高校数据库课程学习者与Java JDBC初学者,聚焦吉林大学《数据库应用程序开发》课程的完整实验过程,帮助读者理解Java程序与DB2数据库交互的典型场景。报告围绕JDBC连接建立、Statement与PreparedStatement使用、ResultSet结果集遍历、事务提交控制及SQL异常处理展开,并记录了权限授予、对象名称不匹配、数值溢出与事务未提交等调试思路,还涉及GUI改造与单行、多行、子查询插入等扩展任务。资源包共1个doc文件,约1.76MB,内容以实验步骤、错误分析与运行截图为主,便于对照复现实验环境。目前已有902人学习,适合需要完成同类数据库实验或梳理JDBC编程要点的读者参考。
1. 从一份实验报告里拆出的 JDBC 实战骨架
翻到这份《数据库应用程序开发》实验报告时,我第一反应不是“这有什么可讲的”,而是它把 JDBC 从连库到事务、从控制台到 GUI、从单行插入到子查询插入的完整链路都走了一遍。八个任务串起来,正好是一条数据库应用开发的最小闭环:连库、查询、更新、事务、界面、异常、结果集操作。对刚接触 JDBC 的开发者来说,这是一份能照着跑通的练习骨架;对熟手来说,它暴露的权限、别名、事务提交、数值溢出这些坑,恰恰是真实项目里最容易翻车的地方。下面我按“能干什么、怎么跑、怎么改”的顺序,把这份报告拆成可复现的工程笔记。
2. 实验任务全景:八个任务对应的 JDBC 能力矩阵
2.1 任务清单与技术点映射
这份报告的价值在于任务设计有递进关系,不是八个孤立练习。任务 1 到任务 3 打基础,任务 4 到任务 8 做界面和健壮性。先把映射关系理清楚,后面跑的时候才知道每个文件在练什么。
| 任务 | 程序文件 | 核心 JDBC 能力 | 关键类/方法 |
|---|---|---|---|
| 任务 1 | P171 片段补全 | 权限授予、对象名限定 | GRANT、schema 前缀 |
| 任务 2 | labstaff.java | 连接、Statement、ResultSet 遍历 | DriverManager、createStatement、next() |
| 任务 3 | labupdate.java | PreparedStatement、事务控制、异常码 | setAutoCommit、executeUpdate、getErrorCode |
| 任务 4 | labupdate GUI 版 | 输入输出对话框化 | JOptionPane |
| 任务 5 | TEMPL 插入程序 | 单行/多行/子查询插入 | executeUpdate、INSERT…SELECT |
| 任务 6 | 空值显示改造 | 结果集空值处理 | wasNull()、getString |
| 任务 7 | 异常处理改造 | 约束/类型/溢出异常捕获 | SQLException 分类处理 |
| 任务 8 | labstaff1 + 任意行列修改 | 可滚动结果集、SQLWarning | TYPE_SCROLL_INSENSITIVE、getWarnings |
这张表建议贴在显示器边上。跑任何一个任务之前,先确认它依赖的前置任务是否已经跑通,比如任务 5 的插入程序依赖任务 1 里 TEMPL 相关对象的权限和命名,任务 8 的可滚动结果集又依赖任务 2 的连接方式。
2.2 运行环境与依赖准备
报告里用的是 DB2 的 SAMPLE 数据库,驱动类是COM.ibm.db2.jdbc.app.DB2Driver,这是 DB2 的本地 JDBC 驱动(type 2)。放到今天的环境里跑,有两条路:一是继续用 DB2 本地驱动,需要装 DB2 客户端并配置好实例;二是换成 DB2 的通用 JDBC 驱动(type 4),驱动类变成com.ibm.db2.jcc.DB2Driver,连接串也要相应调整。我一般会选第二条,少一层本地库依赖,容器里也能跑。
环境准备的关键步骤:
# 1. 确认 DB2 实例已启动,SAMPLE 库存在 db2start db2 list db directory # 2. 编译时把驱动 jar 放进 classpath javac -cp ".:db2jcc.jar" labstaff.java # 3. 运行时同样带上驱动 java -cp ".:db2jcc.jar" labstaff参数说明:-cp后面的.表示当前目录,db2jcc.jar是 DB2 通用驱动的 jar 包名,实际文件名可能带版本号,按你本地下载的为准。如果用的是本地驱动,jar 名通常是db2java.zip里的类,classpath 写法不同。这一步最容易翻车的地方是驱动 jar 没放对位置,报ClassNotFoundException,先检查 jar 路径再怀疑代码。
提示:报告里连接串写的是
JDBC:db2:sample,大小写不敏感但建议统一写成jdbc:db2:sample,避免某些驱动版本解析异常。
3. 核心代码拆解:从连库到事务提交的完整链路
3.1 连接与查询:labstaff.java 的最小闭环
任务 2 的代码是整份报告的地基,它把 JDBC 查询的四步走完了:加载驱动、建连接、执行查询、遍历结果集。报告里给出的片段是填空式的,我把它补成能直接跑的完整版本。
import java.sql.*; public class labstaff { public static void main(String[] args) { Connection sample = null; Statement stmt = null; ResultSet rs = null; try { // 1. 加载 DB2 驱动 Class.forName("COM.ibm.db2.jdbc.app.DB2Driver"); // 2. 建立连接,三个参数分别是 URL、用户名、密码 sample = DriverManager.getConnection( "jdbc:db2:sample", "db2admin", "db2admin"); // 3. 创建 Statement 对象 stmt = sample.createStatement(); // 4. 执行查询,注意表名带 schema 前缀 JLU rs = stmt.executeQuery( "select NAME, JOB, SALARY from JLU.staff Where ID = 10"); // 5. 移动到第一行并判断是否有数据 boolean more = rs.next(); while (more) { System.out.println(rs.getString("NAME") + " | " + rs.getString("JOB") + " | " + rs.getDouble("SALARY")); more = rs.next(); } } catch (ClassNotFoundException e) { System.out.println("驱动类未找到:" + e.getMessage()); } catch (SQLException e) { System.out.println("SQL 错误码:" + e.getErrorCode() + " 状态:" + e.getSQLState() + " 消息:" + e.getMessage()); } finally { // 关闭顺序与打开顺序相反 try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (sample != null) sample.close(); } catch (SQLException ignored) {} } } }逻辑说明:Class.forName触发驱动类静态块注册到 DriverManager,这一步在 JDBC 4.0 之后可以省略,但报告环境里显式写更稳。executeQuery只用于 SELECT,返回 ResultSet;next()第一次调用把游标移到第一行,返回 false 表示结果集为空。参数说明:连接串里的sample是数据库名,db2admin是报告里用的账号,实际环境按你的实例改。表名JLU.staff里的JLU是 schema,报告任务 1 的报错“TEMPL 是未定义名称”就是因为漏了 schema 前缀,这个坑后面避坑章节还会提。
3.2 更新与事务:labupdate.java 的提交开关
任务 3 是整份报告里信息密度最高的部分,它把 PreparedStatement、手动事务、异常码三件事揉在一起。报告里明确写了setAutoCommit(false),然后执行更新,最后发现“成功修改但没生效”,原因是事务没提交。这段代码我按报告描述还原如下。
import java.sql.*; public class labupdate { public static void main(String[] args) { Connection sample = null; PreparedStatement pstmt = null; try { Class.forName("COM.ibm.db2.jdbc.app.DB2Driver"); sample = DriverManager.getConnection( "jdbc:db2:sample", "db2admin", "db2admin"); // 关闭自动提交,开启手动事务 sample.setAutoCommit(false); String sqlstmt = "update JLU.staff set salary = salary * ? where dept = ?"; pstmt = sample.prepareStatement(sqlstmt); // 第一个参数是放大倍数,第二个是部门号 pstmt.setDouble(1, 1.01); pstmt.setInt(2, 20); int updateCount = pstmt.executeUpdate(); System.out.println("受影响行数:" + updateCount); // 关键:手动事务必须显式提交,否则连接关闭时回滚 sample.commit(); } catch (SQLException e) { // 出错时回滚,避免半截事务 try { if (sample != null) sample.rollback(); } catch (SQLException ignored) {} System.out.println("SQLCode=" + e.getErrorCode() + " SQLState=" + e.getSQLState() + " Message=" + e.getMessage()); } catch (ClassNotFoundException e) { System.out.println("驱动未找到:" + e.getMessage()); } finally { try { if (pstmt != null) pstmt.close(); } catch (SQLException ignored) {} try { if (sample != null) sample.close(); } catch (SQLException ignored) {} } } }逻辑说明:setAutoCommit(false)之后,每条 DML 都在一个事务里,executeUpdate返回受影响行数,但数据还没落盘,必须commit()。报告里“输入 20 或 30 执行成功但数据库没变化”就是漏了提交,把false改回true能生效,本质是自动提交帮你补了这一步。参数说明:setDouble(1, 1.01)对应 SQL 里第一个?,setInt(2, 20)对应第二个?,参数下标从 1 开始,这是 JDBC 的硬规矩,写成 0 会抛异常。异常处理里getErrorCode拿的是数据库厂商错误码,getSQLState是标准五字符状态码,两个一起看才能定位是约束冲突还是类型不匹配。
3.3 溢出与零行更新:报告里两个值得复现的现象
报告里有一段很实在的调试记录:把 salary 乘 2 时输入 15 和 20 都溢出,乘 1.01 都不溢出,证明是数值放大后超出字段精度。另一个现象是输入 16 时返回“修改了 0 行”,查库发现确实没有 dept=16 的记录。这两个现象对应两类问题:数值溢出和零行更新。
数值溢出的根因是salary字段的 DECIMAL 精度固定,乘一个大系数后结果超出精度范围,DB2 抛类型转换溢出。复现方法很简单,把倍数调大再执行,看SQLException的 SQLState 是不是以22开头(数据异常类)。零行更新不是错误,executeUpdate返回 0 只说明 WHERE 条件没命中,代码层面要区分“执行成功但没数据”和“执行失败”,前者不该走异常分支。
注意:判断更新是否真正生效,不能只看有没有抛异常,要看
executeUpdate的返回值和提交后的实际查询结果,两个都对上才算数。
4. GUI 化与插入改造:JOptionPane 和三种插入方式
4.1 任务 4:把控制台输入输出换成对话框
任务 4 要求把 labupdate 改成 GUI 形式,输入和输出都走JOptionPane。这个改造看着简单,但它是后面任务 5 到任务 8 的界面基础。核心就两个方法:showInputDialog拿输入,showMessageDialog显示结果。
import javax.swing.JOptionPane; import java.sql.*; public class labupdateGUI { public static void main(String[] args) { String deptStr = JOptionPane.showInputDialog("请输入部门号:"); if (deptStr == null) return; // 用户点了取消 int mydeptno; try { mydeptno = Integer.parseInt(deptStr.trim()); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "部门号必须是整数"); return; } Connection sample = null; PreparedStatement pstmt = null; try { Class.forName("COM.ibm.db2.jdbc.app.DB2Driver"); sample = DriverManager.getConnection( "jdbc:db2:sample", "db2admin", "db2admin"); sample.setAutoCommit(false); pstmt = sample.prepareStatement( "update JLU.staff set salary = salary * 1.01 where dept = ?"); pstmt.setInt(1, mydeptno); int count = pstmt.executeUpdate(); sample.commit(); JOptionPane.showMessageDialog(null, "受影响行数:" + count); } catch (SQLException e) { try { if (sample != null) sample.rollback(); } catch (SQLException ignored) {} JOptionPane.showMessageDialog(null, "SQLState=" + e.getSQLState() + "\n" + e.getMessage()); } catch (ClassNotFoundException e) { JOptionPane.showMessageDialog(null, "驱动未找到"); } finally { try { if (pstmt != null) pstmt.close(); } catch (SQLException ignored) {} try { if (sample != null) sample.close(); } catch (SQLException ignored) {} } } }逻辑说明:showInputDialog返回 null 表示用户取消,必须判空,否则parseInt直接抛 NPE。trim()是为了去掉用户误输入的空格,这个细节报告没写但实际跑的时候很容易踩。参数说明:showMessageDialog第一个参数传 null 表示对话框居中于屏幕,传组件则相对组件居中,实验场景用 null 就够。
4.2 任务 5:单行、多行、子查询三种插入
任务 5 要求向 TEMPL 表插入数据,界面支持三种方式。单行插入用一条 INSERT,多行插入用批量addBatch,子查询插入用INSERT INTO … SELECT。三种方式对应三种不同的executeUpdate用法,值得分开写。
// 方式一:单行插入 String sql1 = "insert into JLU.templ(id, name, salary) values(?, ?, ?)"; PreparedStatement p1 = conn.prepareStatement(sql1); p1.setInt(1, 101); p1.setString(2, "张三"); p1.setDouble(3, 5000.0); p1.executeUpdate(); // 方式二:多行插入,批量提交 String sql2 = "insert into JLU.templ(id, name, salary) values(?, ?, ?)"; PreparedStatement p2 = conn.prepareStatement(sql2); for (int i = 0; i < rows.length; i++) { p2.setInt(1, rows[i].id); p2.setString(2, rows[i].name); p2.setDouble(3, rows[i].salary); p2.addBatch(); // 加入批次,不立即执行 } int[] results = p2.executeBatch(); // 一次性提交所有批次 // 方式三:通过子查询插入 String sql3 = "insert into JLU.templ(id, name, salary) " + "select id, name, salary from JLU.staff where dept = ?"; PreparedStatement p3 = conn.prepareStatement(sql3); p3.setInt(1, 20); p3.executeUpdate();逻辑说明:批量插入的addBatch只是把参数攒起来,executeBatch才真正发往数据库,返回的 int 数组每个元素对应一条语句的影响行数。子查询插入的列数和类型必须与目标表匹配,报告里任务 5 的报错“无法创建 jlu.templ 表”是因为之前给 employee 建过别名 templ,别名和表名冲突,删掉别名才能建表。参数说明:批量插入时如果中途某条失败,默认行为是抛异常并停止,需要部分成功的话要配合setAutoCommit(false)和逐条捕获。
4.3 任务 6 到任务 8:空值、异常、任意行列修改
任务 6 要求结果集里为空的列显示“空”。JDBC 里判断空值不能直接getString后判 null,因为数据库 NULL 和 Java null 之间还有一层wasNull()。
String name = rs.getString("NAME"); if (rs.wasNull()) { name = "空"; }wasNull()必须在调用 getter 之后立刻调用,它反映的是“上一次读取的列是否为 SQL NULL”。这个顺序错了就永远拿到 false,是新手常翻的车。
任务 7 要求所有操纵类错误以对话框提示,包括违反约束、类型不匹配、溢出、非法符号。做法是在catch (SQLException e)里按 SQLState 分类,统一走JOptionPane。任务 8 要求对结果集任意行任意列修改,这需要可滚动结果集:
Statement stmt = conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet rs = stmt.executeQuery("select id, name, salary from JLU.staff"); rs.absolute(3); // 定位到第 3 行 rs.updateDouble("SALARY", 8000.0); rs.updateRow(); // 提交这一行的修改参数说明:TYPE_SCROLL_INSENSITIVE表示游标可前后滚动但不感知他人并发修改,CONCUR_UPDATABLE表示结果集可更新。absolute(n)定位到第 n 行,updateRow()把缓存中的修改写回数据库。报告里任务 8 还要求处理 SQLWarning,用stmt.getWarnings()拿警告链,遍历getNextWarning()逐条显示。
5. 避坑与排查:报告里踩过的坑和没写透的坑
5.1 权限与对象名:两个最高频的启动失败
现象:执行查询报“db2admin 没有访问 EMPLOYEE 的权限”。原因:表的所有者是 JLU 用户,db2admin 没有被授权。解决:用 JLU 登录执行GRANT SELECT ON JLU.EMPLOYEE TO db2admin,按需给 SELECT/INSERT/UPDATE/DELETE。这个坑的本质是 DB2 的 schema 隔离,跨用户访问必须显式授权。
现象:报“DB2ADMIN.TEMPL 是一个未定义的名称”。原因:代码里写的是TEMPL,没带 schema,DB2 默认按当前用户 schema 解析,找不到。解决:把代码里的表名改成JLU.TEMPL。血泪经验是,只要涉及多 schema,所有表名都带前缀,别指望默认解析。
5.2 事务提交:改成功了但数据没变
现象:executeUpdate返回大于 0,程序不报错,但查库数据没变。原因:setAutoCommit(false)之后没有commit(),连接关闭时事务回滚。解决:手动事务必须显式提交,异常分支里补rollback()。这个坑报告里写得很清楚,但实际项目里换个框架、换个连接池,自动提交的默认值可能又变了,每次换环境都要确认一遍。
5.3 别名与表名冲突:建表失败
现象:创建JLU.TEMPL表时报错。原因:之前给EMPLOYEE建过别名TEMPL,别名占用了这个名字。解决:DROP ALIAS TEMPL之后再建表。别名和表在同一个命名空间里,这个设计容易让人误判,建表前先查一下SYSCAT.TABLES和SYSCAT.ALIASES有没有重名。
5.4 数值溢出与零行更新:两个容易误判的返回
现象:更新 salary 时报类型转换溢出。原因:放大后的数值超出字段 DECIMAL 精度。解决:调小放大系数,或先查字段精度再决定倍数。现象:executeUpdate返回 0。原因:WHERE 条件没命中任何行。解决:先select count(*)确认数据存在,再执行更新。这两个现象都不该直接当异常处理,前者是数据问题,后者是业务逻辑问题,分开对待。
5.5 驱动类与连接串不匹配
现象:ClassNotFoundException或连接被拒。原因:驱动 jar 没进 classpath,或驱动类型和连接串不匹配(本地驱动 vs 通用驱动)。解决:确认 jar 路径,本地驱动用COM.ibm.db2.jdbc.app.DB2Driver,通用驱动用com.ibm.db2.jcc.DB2Driver,连接串前缀统一小写jdbc:db2:。换驱动时这两处要一起改,只改一处必翻车。
6. 二次开发改造:把实验代码变成可复用的小工具
这份报告的代码是练习性质,方法全堆在 main 里,连接参数硬编码,异常处理粗糙。要拿它做二次开发的起点,我一般会做三件事:抽连接工具类、抽 DAO、把异常分类做成统一提示。下面给一个最小改造示例,把连接管理从业务代码里剥出来。
public class DBUtil { private static final String URL = "jdbc:db2:sample"; private static final String USER = "db2admin"; private static final String PWD = "db2admin"; static { try { Class.forName("com.ibm.db2.jcc.DB2Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("DB2 驱动加载失败"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }改造后的 DAO 只关心 SQL 和参数,连接和关闭交给工具类。验证方法很直接:把原来 labstaff 的查询逻辑搬进一个StaffDao.findByDept(int dept)方法,跑一遍结果和原程序一致,再故意传一个不存在的部门号,确认返回空列表而不是抛异常。异常分类这块,我习惯按 SQLState 前缀分流:23开头是约束冲突,22开头是数据异常(含溢出和类型不匹配),42开头是语法或对象不存在,分别给不同提示文案。
还有一个值得做的改造是把任务 5 的三种插入方式封装成一个insert(InsertMode mode, List<Row> rows)方法,用枚举区分单行、批量、子查询。这样界面层只负责收集参数,插入逻辑收敛到一处,后面加第四种插入方式不用动界面代码。改造时注意批量插入的批次大小,一次攒几千条再executeBatch容易撑爆内存,我一般按 500 条一批分段提交。
从那以后我每次碰 JDBC 项目,不管是实验代码还是生产代码,都强制先确认三件事:驱动和连接串是否匹配、事务提交开关在哪、表名有没有带 schema。这三件事任何一件没对齐,后面调半天都是白费。希望这份拆解帮到你,少走几个我当年走过的弯路。
本文还有配套的精品资源,点击获取