简介本资源是吉林大学《数据库应用程序开发》课程的完整实验报告面向计算机专业本科生及Java数据库开发初学者聚焦JDBC核心实践能力培养。报告覆盖8个递进式实验任务从JDBC连接配置、SELECT查询与ResultSet遍历到PreparedStatement参数化更新、事务控制setAutoCommit、GUI界面集成JOptionPane、多模式数据插入单行/多行/子查询、空值统一显示、结构化异常处理约束/类型/溢出直至结果集动态编辑完整呈现数据库应用开发全流程排错思路与实操细节。资源为1个1.76MB的Word文档.doc内容含详细调试过程、典型错误截图分析、SQL权限配置、DB2驱动加载、事务提交修复等关键知识点结构清晰、步骤翔实。目前已有900人学习下载是掌握JavaDB2数据库交互、夯实JDBC编程基础的优质教学实践材料。1. 这不是一份交差的实验报告而是一套可直接复用的 JDBC DB2 Swing 实战模板你有没有试过写完一段 JDBC 连接代码跑起来报DB2ADMIN.TEMPL is an undefined name查了半小时才发现是 schema 名没写全或者executeUpdate()返回 1但数据库里死活查不到新数据最后发现setAutoCommit(false)后忘了commit()又或者用户输个 20程序直接java.lang.ArithmeticException: Overflow崩掉连错误提示都卡在控制台里——而你的 GUI 界面还傻乎乎弹着“操作成功”。这不是玄学是每个刚接触企业级数据库 Java 开发的人必踩的坑。这份来自吉林大学《数据库应用程序开发》实验的完整源码包恰恰把这八类高频翻车场景全打穿了从最基础的DriverManager.getConnection配置、PreparedStatement参数绑定到事务开关时机、空值统一渲染、SQL 异常分级捕获约束冲突/类型不匹配/溢出/非法符号再到 Swing 表单与 ResultSet 的双向绑定改造。它不讲 JDBC 接口定义只给你能粘贴进自己项目、改两行就能跑通的.java文件它不画 UML 图但每个任务的代码结构都暗合 DAO 分层雏形它甚至把JOptionPane的调用封装成一行showErrorDialog(违反主键约束)——你拿到手就是一套带血泪经验的数据库交互脚手架。2. 环境准备与工程结构DB2 驱动、JDBC URL、Schema 命名三件套必须对齐2.1 DB2 数据库环境初始化SAMPLE 数据库与 JLU Schema 是前提实验所有任务均依赖本地已安装的 IBM DB2 Express-C 或类似版本实验文档未指定具体版本但根据COM.ibm.db2.jdbc.app.DB2Driver类名可判定为 DB2 8.x–10.x 时代经典驱动。关键前提是数据库实例中必须存在名为SAMPLE的示例数据库DB2 自带可通过db2sampl命令创建SAMPLE库内需存在JLU模式schema且该模式下有staff、employee、TEMPL等表注意TEMPL是实验自建表非 DB2 默认表用户db2admin必须被显式授予对JLU下所有表的SELECT,INSERT,UPDATE,DELETE权限。提示权限问题在任务 1 中首次暴露。正确授予权限的 SQL 语句为CONNECT TO SAMPLE USER db2admin USING db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE JLU.staff TO USER db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE JLU.employee TO USER db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE JLU.TEMPL TO USER db2admin;2.2 Java 工程依赖配置DB2 JDBC 驱动 jar 包路径必须硬编码进 classpath实验使用的是 DB2 旧版 JDBC 驱动db2java.zip其核心类为COM.ibm.db2.jdbc.app.DB2Driver。该驱动不兼容新版ibm-db2-jcc即com.ibm.db2.jcc.DB2Driver若强行替换会导致ClassNotFoundException。因此构建工程时必须将db2java.zip或解压后的db2java.jar放入项目lib/目录在编译和运行命令中显式添加-cp参数确保驱动在 classpath 顶端。例如编译并运行labstaff.java的完整命令链为# 编译假设当前目录含 labstaff.java 和 lib/db2java.zip javac -cp .:lib/db2java.zip labstaff.java # 运行注意JDBC URL 中的 JDBC:db2:sample 是 DB2 专用格式冒号不能少 java -cp .:lib/db2java.zip labstaff注意Linux/macOS 用:分隔 classpathWindows 用;。若漏掉db2java.zip会报java.lang.ClassNotFoundException: COM.ibm.db2.jdbc.app.DB2Driver——这是任务 3 中明确要求Class.forName(...)的根本原因驱动类必须由 JVM 显式加载注册。2.3 JDBC URL 与 Schema 命名规范JLU.TEMPL 不是笔误而是 DB2 的强制约定任务 1 中的错误DB2ADMIN.TEMPL is an undefined name表面看是权限问题实则是 DB2 对对象引用的严格解析规则所致。DB2 要求所有表、视图等数据库对象必须以 schema 名作为前缀除非当前连接用户默认 schema 就是该对象所属 schemaTEMPL是表名JLU是 schema 名二者组合JLU.TEMPL才构成 DB2 可识别的完全限定名Fully Qualified Name若代码中写TEMPLDB2 会默认查找DB2ADMIN.TEMPL因当前用户是db2admin而该用户下无此表故报错。因此所有 SQL 语句中的表名都必须补全 schema// ✅ 正确显式指定 JLU schema String sql SELECT * FROM JLU.staff WHERE ID ?; String insertSql INSERT INTO JLU.TEMPL (ID, NAME) VALUES (?, ?); // ❌ 错误DB2 无法解析裸表名 String wrongSql SELECT * FROM staff; // → 解析为 DB2ADMIN.staff2.4 工程目录结构8 个任务对应 8 个独立 .java 文件无 Maven/Gradle整个实验包是典型的教学型扁平结构无构建工具纯手工管理依赖。标准目录如下jlu-db2-lab/ ├── lib/ │ └── db2java.zip # DB2 旧版 JDBC 驱动核心依赖 ├── src/ # 所有 Java 源文件 │ ├── labstaff.java # 任务 2基础查询Statement ResultSet │ ├── labupdate.java # 任务 3更新操作PreparedStatement 事务 │ ├── labupdate_gui.java # 任务 4GUI 版更新JOptionPane 输入/输出 │ ├── labinsert.java # 任务 5插入操作单行/多行/子查询 │ ├── labinsert_null.java # 任务 6空值统一显示为空 │ ├── labinsert_error.java # 任务 7异常分级对话框提示 │ └── labstaff1.java # 任务 8带 SQLWarning 处理的参数化查询 └── README.md # 环境说明与运行指引提示所有.java文件均无 package 声明全部位于默认包default package。这意味着它们必须放在 classpath 根目录下编译运行不可嵌套子目录否则NoClassDefFoundError会立刻报错。3. 核心模块拆解从 Connection 到 PreparedStatement一条 SQL 的完整生命周期3.1 连接建立DriverManager.getConnection 的三个参数缺一不可DriverManager.getConnection()是 JDBC 的入口实验中所有任务均采用相同模式Connection sample DriverManager.getConnection( JDBC:db2:sample, // URLDB2 专用格式JDBC:db2: 前缀 数据库别名 db2admin, // 用户名必须与 DB2 实例中创建的用户一致 db2admin // 密码明文硬编码教学场景可接受生产环境严禁 );URL 解析JDBC:db2:sample中sample是数据库别名Database Alias非数据库名。该别名需在 DB2 Catalog 中注册db2 catalog db sample且指向实际数据库路径。若别名错误报SQLException: [IBM][CLI Driver] SQL1013N。用户名/密码db2admin是 DB2 安装时创建的默认管理员账户。若实验环境修改过密码此处必须同步更新否则报SQLException: [IBM][CLI Driver] SQL0801N认证失败。连接池缺失教学代码未引入 HikariCP 或 Druid每次操作新建连接。这在实验中可接受但真实项目必须改造为连接池管理否则高并发下会耗尽 DB2 连接数。3.2 查询执行Statement 与 PreparedStatement 的本质区别在哪任务 2 使用Statement任务 3/5/8 使用PreparedStatement。二者差异不仅是“预编译”这个教科书说法而是安全与性能的双重分水岭维度Statement任务 2PreparedStatement任务 3/5/8SQL 注入防护❌ 无防护SELECT * FROM staff WHERE ID userInput✅ 天然免疫pstmt.setInt(1, userInput)参数化绑定执行效率每次执行都需 DB2 解析、编译、生成执行计划首次编译后缓存执行计划后续仅绑定参数快 3–5 倍类型安全❌ 全靠字符串拼接WHERE SALARY salaryStr易错✅pstmt.setDouble(1, salary)强制类型转换错则早报以任务 3 的更新为例关键代码段如下// 1. 定义带 ? 占位符的 SQL注意? 不能用于表名/列名 String sqlstmt UPDATE JLU.staff SET SALARY SALARY * ? WHERE ID ?; // 2. 创建 PreparedStatement此时 SQL 已被 DB2 解析并预编译 PreparedStatement pstmt sample.prepareStatement(sqlstmt); // 3. 绑定参数索引从 1 开始类型严格匹配 pstmt.setDouble(1, 1.01); // 第一个 ? → double 类型乘数 pstmt.setInt(2, 15); // 第二个 ? → int 类型 ID // 4. 执行返回受影响行数int非 boolean int updateCount pstmt.executeUpdate(); System.out.println(更新了 updateCount 行);注意executeUpdate()返回int表示 DMLINSERT/UPDATE/DELETE影响的行数。若返回 0说明WHERE条件未匹配到任何记录如任务 3 中输入 16 时这并非错误而是业务正常态。新手常误以为 0失败导致加冗余日志。3.3 结果集处理ResultSet.next() 是游标移动不是数据读取ResultSet是 JDBC 中最易误解的对象。任务 2 和 8 中反复出现rs.next()但它的真实作用是移动内部游标初始位置在第一行之前rs.next()将游标移至下一行返回布尔值true表示成功移到有效行false表示已到末尾不读取数据数据读取需调用rs.getString(NAME)等 getter 方法。典型错误写法任务 2 调试过程暴露// ❌ 错误next() 被调用两次跳过第一行 boolean more rs.next(); // 移到第 1 行返回 true more rs.next(); // 移到第 2 行第 1 行数据丢失 String name rs.getString(NAME); // 读取的是第 2 行的 NAME // ✅ 正确一次 next() 一次读取循环处理 while (rs.next()) { // 移到下一行并检查是否有效 String name rs.getString(NAME); // 读取当前行数据 String job rs.getString(JOB); double sal rs.getDouble(SALARY); System.out.println(name , job , sal); }提示rs.getString(COL_NAME)中列名大小写敏感必须与数据库元数据中定义的完全一致JLU.staff表中列为大写NAME/JOB/SALARY。若写成rs.getString(name)返回null。3.4 事务控制setAutoCommit(false) 后commit() 是唯一生效出口任务 3 明确要求sample.setAutoCommit(false)这是理解数据库事务的关键切口。其逻辑链为关闭自动提交 → 所有 DML 操作进入“待定状态”不写入磁盘手动调用sample.commit()→ 将待定变更持久化其他连接可见手动调用sample.rollback()→ 废弃所有待定变更回滚到上一个 commit 点。实验中“修改成功但数据库无变化”的根本原因就是漏掉了commit()sample.setAutoCommit(false); // 关闭自动提交 pstmt.executeUpdate(); // 执行更新但仅在内存/事务日志中 // ❌ 此处缺少 sample.commit(); // → 程序退出时JVM 关闭连接DB2 自动 rollback隐式回滚 // ✅ 正确流程 try { pstmt.executeUpdate(); sample.commit(); // 显式提交数据真正落库 } catch (SQLException e) { sample.rollback(); // 出错则回滚 throw e; }注意DB2 的setAutoCommit(false)是连接级设置同一Connection对象的所有操作共享该状态。若一个线程中混用多个PreparedStatement必须确保它们共用同一个Connection并统一管理 commit/rollback。4. GUI 与异常处理从控制台到 JOptionPane错误信息必须对用户友好4.1 Swing GUI 改造JOptionPane 是教学场景的最优解任务 4 和 5 要求将控制台 I/O 替换为图形界面。实验选用javax.swing.JOptionPane因其零依赖、无需布局管理器、一行代码即可弹窗完美契合教学快速验证需求。核心方法映射关系如下控制台操作JOptionPane 替代方案示例代码System.out.println()JOptionPane.showMessageDialog()信息提示JOptionPane.showMessageDialog(null, 更新成功);Scanner.nextLine()JOptionPane.showInputDialog()输入对话框String idStr JOptionPane.showInputDialog(请输入员工ID);System.err.println()JOptionPane.showMessageDialog()错误提示JOptionPane.showMessageDialog(null, ID 格式错误, 输入错误, ERROR_MESSAGE);以任务 4 的labupdate_gui.java为例用户输入与结果反馈的完整闭环// 1. 获取用户输入字符串 String idStr JOptionPane.showInputDialog(请输入员工ID); if (idStr null) return; // 用户点取消直接退出 // 2. 类型转换关键捕获 NumberFormatException int id; try { id Integer.parseInt(idStr); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, ID 必须为整数, 输入错误, JOptionPane.ERROR_MESSAGE); return; } // 3. 执行数据库更新省略 JDBC 代码 int count executeUpdate(id, 1.01); // 假设此方法封装了 PreparedStatement // 4. 反馈结果 if (count 0) { JOptionPane.showMessageDialog(null, 成功更新 count 行, 操作成功, JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(null, 未找到 ID 为 id 的员工, 提示, JOptionPane.WARNING_MESSAGE); }提示JOptionPane的parentComponent参数传null表示无父窗口弹窗居中显示。若传入JFrame实例则弹窗相对父窗体居中且模态阻塞父窗体。4.2 空值统一渲染ResultSet.getObject() null 判断是唯一可靠方案任务 6 要求将结果集中所有NULL值显示为字符串空。常见误区是用rs.getString()—— 该方法对NULL列返回null但若列类型为INT或DATErs.getString()可能抛SQLException。正确做法是rs.getObject() 显式 null 判断// 遍历 ResultSet 所有列通用处理 for (int i 1; i rs.getMetaData().getColumnCount(); i) { Object value rs.getObject(i); // 安全获取任意类型值 String display (value null) ? 空 : value.toString(); System.out.print(display \t); } System.out.println();rs.getMetaData().getColumnCount()动态获取列数避免硬编码列数rs.getObject(i)返回Object可安全处理NULL及所有 JDBC 类型Integer,String,Date,BigDecimal等value.toString()对非 null 值调用null值已被提前拦截不会触发 NPE。4.3 异常分级捕获SQLState 是 DB2 错误分类的黄金标准任务 7 要求对“违反约束、类型不匹配、溢出、非法符号”四类错误分别提示。DB2 通过SQLState字符串5 位码精准标识错误类型比getMessage()文本解析更稳定。实验中关键SQLState映射如下错误类型SQLState 前两位DB2 典型错误码JOptionPane 提示文案主键/唯一约束冲突23SQL0803N违反主键约束该ID已存在外键约束失败23SQL0530N外键约束失败部门不存在类型不匹配42SQL0408N类型不匹配请检查输入格式如ID应为数字数值溢出22SQL0802N数值溢出计算结果超出字段范围SQL 语法错误42SQL0104NSQL 语法错误语句中包含非法符号异常处理核心代码} catch (SQLException e) { String sqlState e.getSQLState(); String message e.getMessage(); if (sqlState.startsWith(23)) { JOptionPane.showMessageDialog(null, 违反约束 message, 数据约束错误, JOptionPane.ERROR_MESSAGE); } else if (sqlState.startsWith(22)) { JOptionPane.showMessageDialog(null, 数值溢出 message, 计算错误, JOptionPane.ERROR_MESSAGE); } else if (sqlState.startsWith(42)) { JOptionPane.showMessageDialog(null, 语法或类型错误 message, 输入错误, JOptionPane.ERROR_MESSAGE); } else { JOptionPane.showMessageDialog(null, 数据库错误 message, 系统错误, JOptionPane.ERROR_MESSAGE); } }注意e.getSQLState()返回String必须用startsWith()判断前缀而非equals()。因为SQLState后三位可能随具体错误微调如23505和23503都属约束类。4.4 避坑 / 常见问题 / 排查8 个任务中踩出的 5 条血泪经验现象 1java.lang.ClassNotFoundException: COM.ibm.db2.jdbc.app.DB2Driver原因db2java.zip未加入 classpath或路径错误导致 JVM 找不到该 jar。解决确认lib/db2java.zip存在编译/运行命令中-cp参数包含该路径且COM.ibm.db2.jdbc.app.DB2Driver类名拼写完全正确注意大小写和包路径中的点号。现象 2SQLException: [IBM][CLI Driver] SQL0801N用户名/密码错误原因DriverManager.getConnection()第二、三参数用户名/密码与 DB2 实际用户不匹配。解决用 DB2 命令行db2 connect to SAMPLE user db2admin using db2admin测试连通性若失败重置db2admin密码或检查 DB2 实例是否启动。现象 3executeUpdate()返回 0但业务上应有数据原因WHERE条件未匹配到任何行如WHERE ID 16但staff表中无 ID16 的记录非程序错误。解决先用 DB2 命令db2 select * from JLU.staff where ID 16验证数据存在性若数据存在检查 SQL 中 schema 名JLU.是否遗漏。现象 4GUI 窗口弹出后程序无响应假死原因Swing 是单线程模型Event Dispatch Thread若 JDBC 操作如executeQuery()在 EDT 中执行会阻塞 UI 线程。解决将数据库操作移至后台线程SwingWorkerUI 更新放回 EDT。教学代码未处理故建议仅在数据量小时使用真实项目必须改造。现象 5ArithmeticException: Overflow发生在pstmt.setDouble(1, 2.0)之后原因SALARY字段在 DB2 中定义为SMALLINT范围 -32768~32767SALARY * 2后超出上限。解决用pstmt.setDouble(1, 1.01)降低倍数或修改表结构ALTER TABLE JLU.staff ALTER COLUMN SALARY SET DATA TYPE DECIMAL(10,2)扩大精度。5. 二次开发实战如何把实验代码改造成可维护的 DAO 工具类5.1 从散装代码到 DAO 分层抽取 ConnectionFactory 与 StaffDAO实验的 8 个.java文件全是面向过程的脚本式代码重复出现DriverManager.getConnection()、pstmt.setString()等样板。要投入真实项目第一步必须抽象为 DAOData Access Object模式。核心改造点步骤 1创建ConnectionFactory工具类统一管理连接// src/utils/ConnectionFactory.java package utils; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionFactory { private static final String URL JDBC:db2:sample; private static final String USER db2admin; private static final String PASSWORD db2admin; static { try { // 驱动加载只需一次在类加载时完成 Class.forName(COM.ibm.db2.jdbc.app.DB2Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(DB2 Driver not found!, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }优势驱动加载Class.forName()移至静态块避免每个 DAO 方法重复调用连接参数集中管理修改只需改一处。步骤 2创建StaffDAO封装 staff 表的 CRUD// src/dao/StaffDAO.java package dao; import utils.ConnectionFactory; import java.sql.*; public class StaffDAO { // 查询单个员工 public Staff findById(int id) throws SQLException { String sql SELECT ID, NAME, JOB, SALARY FROM JLU.staff WHERE ID ?; try (Connection conn ConnectionFactory.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, id); try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { return new Staff( rs.getInt(ID), rs.getString(NAME), rs.getString(JOB), rs.getDouble(SALARY) ); } return null; // 未找到 } } } // 更新员工薪资支持事务 public int updateSalary(int id, double multiplier) throws SQLException { String sql UPDATE JLU.staff SET SALARY SALARY * ? WHERE ID ?; try (Connection conn ConnectionFactory.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setDouble(1, multiplier); pstmt.setInt(2, id); conn.setAutoCommit(false); // 开启事务 int count pstmt.executeUpdate(); conn.commit(); // 显式提交 return count; } catch (SQLException e) { // 自动 rollbacktry-with-resources 关闭 conn 时触发 throw e; } } }关键改进使用try-with-resources自动关闭Connection/PreparedStatement/ResultSet杜绝资源泄漏updateSalary()内部管理事务调用方无需关心commit()/rollback()返回Staff实体对象需定义Staff.javaPOJO而非原始ResultSet实现数据与表现分离。5.2 GUI 层升级用 JTable 替代 JOptionPane实现真正的表格编辑任务 5 的JOptionPane.showInputDialog()仅支持单行输入无法满足“任意行、任意列修改”的任务 8 要求。真实场景应使用JTable步骤 1创建StaffTableModel继承AbstractTableModel// src/gui/StaffTableModel.java package gui; import dao.StaffDAO; import javax.swing.table.AbstractTableModel; import java.util.List; public class StaffTableModel extends AbstractTableModel { private final String[] columnNames {ID, 姓名, 职位, 薪资}; private ListStaff data List.of(); // 初始化为空 Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return columnNames.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Staff staff data.get(rowIndex); return switch (columnIndex) { case 0 - staff.getId(); case 1 - staff.getName(); case 2 - staff.getJob(); case 3 - staff.getSalary(); default - null; }; } Override public String getColumnName(int column) { return columnNames[column]; } // 刷新数据从数据库加载 public void refreshData() throws SQLException { StaffDAO dao new StaffDAO(); data dao.findAll(); // 需在 StaffDAO 中补充 findAll() 方法 fireTableDataChanged(); // 通知 JTable 更新视图 } }步骤 2在 JFrame 中嵌入 JTable并监听单元格编辑// src/gui/StaffEditorFrame.java JTable table new JTable(new StaffTableModel()); table.setDefaultEditor(Object.class, new DefaultCellEditor(new JTextField())); // 允许编辑 // 监听编辑结束事件 table.getModel().addTableModelListener(e - { if (e.getType() TableModelEvent.UPDATE) { int row e.getFirstRow(); int col e.getColumn(); StaffTableModel model (StaffTableModel) table.getModel(); Object newValue model.getValueAt(row, col); // TODO调用 StaffDAO.updateById() 更新数据库 System.out.println(更新第 row 行第 col 列 newValue); } });效果用户直接在表格中双击单元格修改回车确认TableModelListener捕获变更并触发数据库更新彻底告别JOptionPane的割裂感。5.3 异常处理增强Log4j 日志 自定义业务异常任务 7 的JOptionPane提示仅面向终端用户但开发调试需要详细日志。引入 Log4j 2轻量级步骤 1添加 log4j-api 和 log4j-core 依赖Mavendependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version2.20.0/version /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.20.0/version /dependency步骤 2在 DAO 中添加结构化日志// StaffDAO.java 中 updateSalary 方法内 Logger logger LogManager.getLogger(StaffDAO.class); try { // ... 执行更新 logger.info(Staff salary updated: id{}, multiplier{}, rows{}, id, multiplier, count); return count; } catch (SQLException e) { logger.error(Failed to update staff salary, e); // 输出完整堆栈 throw new BusinessException(薪资更新失败请联系管理员, e); }价值错误发生时日志中同时包含业务上下文id15和底层异常SQL0802N排查效率提升 50% 以上。从那以后我每次接手一个老项目第一件事就是把所有System.out.println()和裸catch (Exception e) { e.printStackTrace(); }替换成LoggerBusinessException第二件事是把所有DriverManager.getConnection()抽成ConnectionFactory。这两步做完项目就从“能跑”变成了“敢改”。希望帮到你。本文还有配套的精品资源点击获取