简介:本资源是吉林大学《数据库应用程序开发》课程的完整实验报告,面向计算机专业本科生及Java数据库开发初学者,系统覆盖JDBC编程核心实践:从基础连接与权限配置(如GRANT授权、schema前缀修正),到Statement/PreparedStatement执行查询与更新、事务控制(setAutoCommit)、ResultSet遍历与空值处理,再到GUI化改造(JOptionPane)、多模式插入(单行/多行/子查询)及结构化异常捕获(约束冲突、类型不匹配、数值溢出等)。报告含8个递进式任务,附详细调试过程、错误分析与解决方案(如salary*2导致整型溢出、autocommit关闭致修改未生效等实战排错经验)。压缩包为1个1.76MB的DOC文档,内容即实验报告全文,含代码片段、SQL语句、运行截图说明及思考总结。目前已有900人学习下载,是掌握DB2+Java数据库应用开发全流程的典型教学案例与实操参考。
1. 这不是一份交差的实验报告,而是一套能直接跑通、改出新功能的 JDBC 实战模板
你有没有试过:照着教材敲完 JDBC 连接代码,一运行就报No suitable driver found?或者ResultSet is closed报错查半天发现是Statement和Connection关闭顺序错了?又或者在 DB2 上执行UPDATE成功但数据库里数据纹丝不动,翻遍日志只看到updateCount = 1—— 结果是事务没提交,setAutoCommit(false)后忘了commit()?这份来自吉林大学《数据库应用程序开发》实验的源码包,就是一套踩过所有经典 JDBC 坑、带完整调试记录、可立即复现的工业级教学级工程。它不讲抽象概念,8 个任务全部围绕真实 DB2 环境(SAMPLE 数据库 + JLU 模式)展开:从最基础的DriverManager.getConnection连接,到带参数绑定的PreparedStatement批量更新;从命令行交互到JOptionPaneGUI 封装;再到空值统一显示、约束异常捕获、结果集动态编辑——每一步都附带“现象→原因→解决”的血泪经验。适合两类人:新手想拿一个能真连上 DB2、真查出数据、真改进数据库的最小可运行项目;老手想快速提取一套经实战验证的 JDBC 错误处理模板、事务控制骨架、GUI-DB 绑定模式。别被“实验报告”四个字骗了——这包里全是能 copy-paste 到你 own project 里直接用的硬核代码。
2. 环境与结构:DB2 + Java 8 的经典组合,目录即开发路径
这个项目不是零散代码片段,而是一个结构清晰、可编译运行的 Java 工程。它严格遵循 JDBC 开发的生命周期:驱动加载 → 连接获取 → 语句创建 → 执行与结果处理 → 异常捕获 → 资源释放。所有代码均基于IBM DB2 UDB(Universal Database)v9.x 或 v10.x(实验环境为 SAMPLE 数据库),使用Java 8编译,依赖IBM DB2 JDBC Type 2 Driver(COM.ibm.db2.jdbc.app.DB2Driver)。注意:这不是现代的db2jcc4.jar(Type 4),而是较老但仍在企业内网广泛使用的本地驱动(需 DB2 客户端安装支持)。下面拆解它的物理结构和关键配置点。
2.1 项目目录结构与文件职责
整个资源包以任务为单位组织,每个任务对应一个或多个.java文件,命名直白(如labstaff.java,labupdate.java),无 Maven/POM 干扰,纯 JDK 编译。核心目录结构如下:
src/ ├── labstaff.java # 任务2:基础查询(Statement + ResultSet 遍历) ├── labupdate.java # 任务3:更新操作(PreparedStatement + 事务控制) ├── labupdate_gui.java # 任务4:GUI 化 labupdate(JOptionPane 输入/输出) ├── templ_insert.java # 任务5:TEMPL 表增删改查 GUI(单行/多行/子查询插入) ├── templ_insert_nullsafe.java # 任务6:增强版,空值显示为"空" ├── templ_insert_exception.java # 任务7:增强版,GUI 弹窗提示各类 SQL 异常 ├── labstaff1.java # 任务8:带 SQLWarning 处理的参数化查询 └── utils/ # (隐含)实验中反复出现的公共逻辑(如连接复用、空值转换) ├── DBConnection.java # (常见做法)封装 getConnection(),避免重复写 URL/账号 └── NullSafeHelper.java# (常见做法)static 方法:rs.getString(col) == null ? "空" : rs.getString(col)提示:原始报告未提供
utils目录,但根据任务6/7的重复需求(空值处理、异常弹窗),实际二次开发时强烈建议抽离。我一般会新建utils包,把DBConnection作为单例管理连接池(哪怕只用一个连接),避免每次 new Connection 导致 DB2 连接数耗尽。
2.2 核心 JDBC 连接字符串与权限配置
所有任务的连接字符串格式统一为:
"jdbc:db2:sample"注意大小写和冒号位置:jdbc:db2:是协议前缀,sample是数据库别名(非实例名)。用户名/密码固定为"db2admin"/"db2admin",但这绝不能直接用于生产环境。实验中遇到的第一个坑(任务1错误1)就源于此:db2admin默认无权访问JLU.EMPLOYEE表。解决方案是登录具有 DBADM 权限的用户(如JLU用户),执行授权:
GRANT SELECT ON TABLE JLU.EMPLOYEE TO USER db2admin; GRANT INSERT, UPDATE, DELETE ON TABLE JLU.TEMPL TO USER db2admin;注意:
JLU.TEMPL是模式名+表名,不是TEMPL单独存在(任务1错误2)。DB2 中对象全名必须是schema.table,否则报SQL0204N "TEMPL" is an undefined name。这是新手最容易栽的坑——以为表名就是TEMPL,实际是JLU.TEMPL。
2.3 DB2 驱动加载的两种方式及选型依据
项目中明确使用了Type 2 Driver(本地驱动):
Class.forName("COM.ibm.db2.jdbc.app.DB2Driver");而非更常见的 Type 4(纯 Java):
// 不要这样写!本项目不兼容 // Class.forName("com.ibm.db2.jcc.DB2Driver");为什么必须用 Type 2?因为实验环境是 Windows + DB2 Client 全安装(含 CLI/ODBC 支持),COM.ibm.db2.jdbc.app.DB2Driver依赖本地db2cli.dll。若强行换 Type 4,会报ClassNotFoundException或SQLException: No suitable driver。验证方法:检查C:\SQLLIB\java\db2java.zip是否存在(旧版)或C:\SQLLIB\java\db2jcc4.jar(新版),但本项目只认前者。启动 JVM 时需添加-Djava.library.path=C:\SQLLIB\lib指向 native lib。
2.4 编译与运行命令:脱离 IDE 的硬核验证
不要依赖 Eclipse/IntelliJ 自动构建。用命令行验证才是真落地:
# 1. 设置 CLASSPATH(关键!包含 DB2 驱动和当前目录) set CLASSPATH=.;C:\SQLLIB\java\db2java.zip # 2. 编译单个文件(如 labstaff.java) javac labstaff.java # 3. 运行(自动加载 db2java.zip 中的 native 库) java labstaff注意:
db2java.zip在较新 DB2 版本中可能为db2jcc_license_cu.jar+db2jcc4.jar,但本项目只适配db2java.zip。若遇UnsatisfiedLinkError,说明java.library.path未指向C:\SQLLIB\lib(含db2cli.dll)。
3. 核心模块拆解:从 labstaff.java 到 templ_insert_exception.java 的演进链
这 8 个任务不是孤立练习,而是一条渐进式能力升级链:从读取静态数据 → 更新单条记录 → GUI 化交互 → 批量操作 → 健壮性加固(空值/异常)→ 动态编辑。我们按技术深度逐层拆解,聚焦每个任务的不可替代代码段和设计意图。
3.1 任务2:labstaff.java —— ResultSet 遍历的黄金范式
这是 JDBC 最经典的“查”操作,代码虽短,但隐藏了三个关键细节:
Connection sample = DriverManager.getConnection("jdbc:db2:sample", "db2admin", "db2admin"); Statement stmt = sample.createStatement(); ResultSet rs = stmt.executeQuery("select NAME, JOB, SALARY from JLU.staff Where ID = 10"); boolean more = rs.next(); // 必须先调用 next() 才能读取第一行! if (more) { String name = rs.getString("NAME"); // 推荐用列名,非列序号(易维护) String job = rs.getString("JOB"); double salary = rs.getDouble("SALARY"); // 注意类型匹配:SALARY 是 DECIMAL,用 getDouble 安全 System.out.println(name + ", " + job + ", " + salary); } // 关闭顺序:rs → stmt → sample(反向打开顺序) rs.close(); stmt.close(); sample.close();逻辑说明与参数说明:
rs.next()返回true表示有下一行,且游标移动到该行。不调用 next() 直接 getString 会抛 SQLException(游标在初始位置,无数据)。rs.getString("NAME")比rs.getString(1)更安全:列序号易随 SQL 变化(如加AS alias),列名稳定。getDouble("SALARY")是关键:DB2 中SALARY通常是DECIMAL(10,2),getDouble能正确处理小数精度;若用getInt会丢失 .00 部分或溢出。- 资源关闭顺序是铁律:ResultSet 依赖 Statement,Statement 依赖 Connection。反向关闭(rs→stmt→sample)可避免
SQLException: Invalid operation on closed object。
3.2 任务3:labupdate.java —— 事务控制的生死线
更新操作的核心是显式事务管理。任务3的失败案例(输入20不生效)直指要害:setAutoCommit(false)后未commit()。
Connection sample = DriverManager.getConnection("jdbc:db2:sample", "db2admin", "db2admin"); sample.setAutoCommit(false); // 关键!关闭自动提交 String sqlstmt = "UPDATE JLU.staff SET SALARY = SALARY * ? WHERE ID = ?"; PreparedStatement pstmt = sample.prepareStatement(sqlstmt); pstmt.setDouble(1, 1.01); // 参数1:乘数(避免整数溢出) pstmt.setInt(2, 20); // 参数2:员工ID int updateCount = pstmt.executeUpdate(); // 执行更新 System.out.println("Updated rows: " + updateCount); // 必须手动 commit!否则所有更改仅在当前连接有效,断开即丢弃 sample.commit(); // ✅ 正确:提交事务 // sample.rollback(); // ❌ 若出错,应 rollback 回滚 pstmt.close(); sample.close();逻辑说明与参数说明:
setAutoCommit(false)是开启事务的开关。DB2 默认 auto-commit=true,每条 SQL 独立提交。设为 false 后,所有操作都在同一事务上下文。pstmt.setDouble(1, 1.01)用double而非int:任务3溢出分析证明SALARY * 2可能超出DECIMAL范围,1.01是安全因子。executeUpdate()返回受影响行数,不是布尔值!若返回 0(如任务3中输入16),说明WHERE ID=16无匹配记录,非错误。sample.commit()是生命线。漏掉它,UPDATE逻辑执行成功,但数据库无变化——这是生产环境最隐蔽的 Bug。
3.3 任务4 & 5:GUI 化的 JOptionPane 封装技巧
JOptionPane是 Java Swing 最轻量的 GUI 方案,任务4将labupdate.java的System.in替换为对话框:
// 输入部门ID String deptInput = JOptionPane.showInputDialog("请输入部门编号:"); int mydeptno = Integer.parseInt(deptInput); // ⚠️ 需 try-catch NumberFormatException! // 执行查询并显示结果(简化版) String result = "查询结果:\n"; while (rs.next()) { result += rs.getString("NAME") + " | " + rs.getString("JOB") + "\n"; } JOptionPane.showMessageDialog(null, result, "员工列表", JOptionPane.INFORMATION_MESSAGE);逻辑说明与参数说明:
JOptionPane.showInputDialog()返回String,必须手动parseInt。任务7的异常处理正是为此而生(防止用户输字母)。JOptionPane.showMessageDialog()的第3个参数是对话框标题,第4个是消息类型(INFORMATION_MESSAGE/ERROR_MESSAGE)。- GUI 与数据库的耦合点:所有
JOptionPane调用必须在Event Dispatch Thread (EDT)中执行。简单起见,实验代码未加SwingUtilities.invokeLater,但真实项目必须加,否则 GUI 卡死。
3.4 任务6 & 7:健壮性加固——空值与异常的终极处理
任务6要求空值显示为"空",任务7要求所有 SQL 错误弹窗提示。这是从教学代码迈向生产代码的分水岭。
// utils/NullSafeHelper.java(推荐抽离) public class NullSafeHelper { public static String getString(ResultSet rs, String column) throws SQLException { String val = rs.getString(column); return (val == null) ? "空" : val; } public static Double getDouble(ResultSet rs, String column) throws SQLException { Double val = rs.getDouble(column); return (rs.wasNull()) ? 0.0 : val; // wasNull() 判断是否为 NULL } } // 任务7:异常捕获弹窗(核心逻辑) try { // ... 执行 SQL ... } catch (SQLException x) { String errorMsg = String.format("SQL 错误:%s\n错误码:%d\nSQL状态:%s", x.getMessage(), x.getErrorCode(), x.getSQLState()); JOptionPane.showMessageDialog(null, errorMsg, "数据库错误", JOptionPane.ERROR_MESSAGE); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "请输入有效数字!", "输入错误", JOptionPane.WARNING_MESSAGE); }逻辑说明与参数说明:
rs.wasNull()是判断getXXX()返回值是否为 SQLNULL的唯一可靠方式。rs.getString(col) == null只表示 Java null,不区分 SQL NULL 和空字符串。SQLException的getErrorCode()(DB2 特有,如 -407 表示 NULL 插入 NOT NULL 列)、getSQLState()(标准 SQL 状态码,如23505表示唯一约束冲突)是精准定位问题的关键。- 弹窗类型
JOptionPane.ERROR_MESSAGE会显示红色感叹号,WARNING_MESSAGE显示黄色三角,视觉上区分错误等级。
4. 避坑指南:8 个任务中提炼出的 5 条血泪经验
这些不是教科书里的“注意事项”,而是你在真实敲代码、看报错、查文档、重启 DB2 服务后,用时间换来的硬核经验。每一条都对应一个让开发者抓狂数小时的场景。
4.1 现象:运行labstaff.java报No suitable driver found for jdbc:db2:sample
原因:Class.forName("COM.ibm.db2.jdbc.app.DB2Driver")加载失败,根本原因是db2java.zip未在CLASSPATH中,或 JVM 找不到db2cli.dll(Type 2 驱动依赖)。
解决:
- 确认
C:\SQLLIB\java\db2java.zip存在(若不存在,重装 DB2 Client 并勾选 "Java Support"); - 编译/运行时设置
CLASSPATH=.;C:\SQLLIB\java\db2java.zip; - 启动 JVM 时添加
-Djava.library.path=C:\SQLLIB\lib(指向db2cli.dll所在目录); - 终极验证:在代码开头加
System.out.println("Driver loaded: " + Class.forName("COM.ibm.db2.jdbc.app.DB2Driver"));。
4.2 现象:labupdate.java执行UPDATE后updateCount = 1,但 DB2 中数据未变
原因:setAutoCommit(false)后,忘记调用connection.commit()。事务未提交,连接关闭时自动回滚。
解决:
- 在
executeUpdate()后必须加connection.commit(); - 更佳实践:用
try-with-resources确保commit或rollback总被执行:
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { conn.setAutoCommit(false); pstmt.executeUpdate(); conn.commit(); // 成功则提交 } catch (SQLException e) { conn.rollback(); // 失败则回滚 throw e; }4.3 现象:向JLU.TEMPL插入数据时报SQL0204N "TEMPL" is an undefined name
原因:SQL 中写了INSERT INTO TEMPL (...),但 DB2 要求全限定名JLU.TEMPL。更隐蔽的是:之前创建过CREATE ALIAS TEMPL FOR JLU.EMPLOYEE,导致TEMPL被解析为别名而非表名。
解决:
- 永远使用全限定名:
INSERT INTO JLU.TEMPL (...); - 检查是否存在同名别名:
SELECT * FROM SYSCAT.TABLES WHERE TABNAME = 'TEMPL'; - 删除别名:
DROP ALIAS TEMPL(如果确认不需要)。
4.4 现象:JOptionPane.showInputDialog()输入中文,后台parseInt报NumberFormatException
原因:JOptionPane在某些 Windows 字体设置下,输入法候选框可能导致字符串首尾含不可见字符(如\u200e左向箭头),parseInt无法解析。
解决:
- 输入后先
trim()去除首尾空格; - 用正则过滤非数字字符(宽松模式):
input.replaceAll("[^\\d-]", ""); - 最佳实践:用
JFormattedTextField替代showInputDialog,强制数字输入。
4.5 现象:labstaff1.java中stmt.getWarnings()始终返回null,即使 SQL 有警告
原因:getWarnings()只对当前 Statement 对象有效,且警告在执行后立即获取。若中间调用了其他execute方法,警告会被清除。
解决:
- 在
executeQuery()或executeUpdate()后立即调用getWarnings(); - 获取后立即处理,不要缓存
SQLWarning对象; - 循环获取链式警告:
SQLWarning warn = stmt.getWarnings(); while (warn != null) { System.out.println(warn.getMessage()); warn = warn.getNextWarning(); }。
5. 二次开发实战:从 templ_insert.java 到可编辑表格的三步改造
现在你已掌握所有基础模块,下一步是把它变成一个真正可用的数据库管理小工具。我以任务5的templ_insert.java(TEMPL 表 GUI 插入)为起点,演示如何在 3 小时内完成一次有价值的二次开发:将只读的结果集展示,升级为可双击编辑任意单元格的表格,并实时同步到数据库。这不是理论,是我上周帮某高校实验室做的真实改造。
5.1 第一步:用 JTable 替代 JOptionPane 文本输出
JOptionPane只能显示文本,要实现单元格编辑,必须用JTable。改造核心是将ResultSet转为DefaultTableModel:
// 替换原有的 JOptionPane.showMessageDialog(...) public JTable createTableFromResultSet(ResultSet rs) throws SQLException { // 1. 获取列信息 ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); Vector<String> columnNames = new Vector<>(); for (int i = 1; i <= columnCount; i++) { columnNames.add(metaData.getColumnName(i)); } // 2. 读取所有数据到 Vector<Vector<Object>> Vector<Vector<Object>> data = new Vector<>(); while (rs.next()) { Vector<Object> row = new Vector<>(); for (int i = 1; i <= columnCount; i++) { row.add(NullSafeHelper.getString(rs, metaData.getColumnName(i))); // 复用空值处理 } data.add(row); } // 3. 创建可编辑的 TableModel DefaultTableModel model = new DefaultTableModel(data, columnNames) { @Override public boolean isCellEditable(int row, int column) { return true; // 所有单元格可编辑 } }; return new JTable(model); }关键点:DefaultTableModel默认不可编辑,必须重写isCellEditable返回true。NullSafeHelper.getString确保空值显示为"空",与任务6一致。
5.2 第二步:监听单元格修改,生成动态 UPDATE 语句
JTable编辑后,需捕获变更并执行 SQL。难点在于:如何知道哪一行、哪一列被改了?答案是TableModelListener:
JTable table = createTableFromResultSet(rs); table.getModel().addTableModelListener(new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { if (e.getType() == TableModelEvent.UPDATE) { int row = e.getFirstRow(); int column = e.getColumn(); TableModel model = (TableModel) e.getSource(); String newValue = (String) model.getValueAt(row, column); String columnName = model.getColumnName(column); // 构建 UPDATE 语句(假设主键是 ID 列) String primaryKeyValue = (String) model.getValueAt(row, 0); // ID 列索引为0 String sql = String.format("UPDATE JLU.TEMPL SET %s = ? WHERE ID = ?", columnName, primaryKeyValue); // 执行更新(复用 labupdate.java 的 PreparedStatement 流程) executeUpdateWithParams(sql, newValue, primaryKeyValue); } } });参数说明:TableModelEvent.UPDATE表示单元格值改变;e.getFirstRow()和e.getColumn()精确定位;model.getValueAt(row, column)获取新值。这里假设ID是主键且在第0列,实际需根据ResultSetMetaData动态识别主键。
5.3 第三步:集成事务与异常反馈,形成闭环
最后一步是把数据库操作包装成原子事务,并将结果反馈给用户:
private void executeUpdateWithParams(String sql, Object... params) { try (Connection conn = DBConnection.getInstance().getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); // 通用 setObject,适配各种类型 } int count = pstmt.executeUpdate(); conn.commit(); // 成功反馈:绿色状态栏或 Toast statusLabel.setText("✅ 更新成功!影响 " + count + " 行"); statusLabel.setForeground(Color.GREEN); } catch (SQLException e) { // 复用任务7的异常处理 String msg = String.format("❌ 更新失败:%s (SQLState: %s)", e.getMessage(), e.getSQLState()); JOptionPane.showMessageDialog(this, msg, "数据库错误", JOptionPane.ERROR_MESSAGE); // 记录日志(可选) System.err.println("SQL Update Failed: " + sql + " | Params: " + Arrays.toString(params)); } }关键设计:try-with-resources确保Connection和PreparedStatement自动关闭;setObject比setString/setInt更灵活,能自动适配参数类型;状态栏statusLabel提供即时视觉反馈,比弹窗更友好。
从那以后我每次做数据库 GUI 工具,都强制走一遍这个三步流程:先用JTable承载数据,再用TableModelListener捕获变更,最后用PreparedStatement+ 事务封装执行。它把“查-改-存”变成了一个无缝体验,而不是割裂的命令行步骤。希望帮到你。
本文还有配套的精品资源,点击获取