简介:本资源是吉林大学《数据库应用程序开发》课程的完整实验报告,面向计算机专业本科生及Java数据库开发初学者,聚焦JDBC核心实践能力培养。报告覆盖8个递进式实验任务:从JDBC连接配置、SELECT查询与ResultSet遍历,到PreparedStatement参数化更新、事务控制(setAutoCommit)、GUI界面集成(JOptionPane)、多模式数据插入(单行/多行/子查询)、空值统一显示、结构化异常处理(约束/类型/溢出),直至结果集动态编辑,完整呈现数据库应用开发全流程排错思路与实操细节。资源为1个1.76MB的Word文档(.doc),内容含详细调试过程、典型错误截图分析、SQL权限配置、DB2驱动加载、溢出问题定位与修复方案等一手实践记录。目前已有900人学习下载,是掌握Java+DB2数据库交互、夯实事务管理与异常处理能力的优质教学参考材料。
1. 这不是“写个 JDBC 连接”——而是一套能过答辩、能跑通、能改出新功能的数据库应用开发闭环方案
你手头有一份吉林大学《数据库应用程序开发》实验报告,八项任务从命令行 JDBC 写到带事务控制的 GUI 更新界面,最后还要处理空值显示、约束报错弹窗、任意单元格编辑……但翻完所有代码片段,你会发现:它缺一个骨架——没有统一的数据访问层封装,没有可复用的连接管理策略,没有异常分类处理机制,更没有验收时能快速演示“增删改查+错误覆盖”的完整流程。这不是 Java 小练习,这是典型的数据库应用开发最小可行闭环:连接可信、SQL 可控、事务可管、错误可读、界面可调、结果可验。本方案面向正在赶毕设/课设 deadline 的同学,也适用于需要交付一套“能讲清楚、能跑起来、能改得动”的教学级数据库应用的工程师。它不堆砌理论,不讲 JDBC 接口继承树,而是把八项任务还原成真实开发中的六个关键动作:权限初始化 → 连接池化 → 查询抽象 → 更新事务化 → GUI 绑定 → 异常语义化。每一步都带可粘贴的代码块、参数说明和调试线索,连“为什么 setAutoCommit(false) 后改了没生效”这种血泪经验都拆解到字节级。
2. 数据库环境与连接管理设计:从裸连 DriverManager 到可复用连接工厂
2.1 权限与对象命名规范:为什么TEMPL必须写成JLU.TEMPL?
实验中反复出现的DB2ADMIN.TEMPL is an undefined name错误,本质是 DB2 的两层命名空间问题。DB2 中对象(表、视图)默认属于当前连接用户模式(schema),而TEMPL是EMPLOYEE表的别名(alias),它被创建在JLU用户下。当以db2admin身份连接时,DB2 默认在DB2ADMIN模式下查找TEMPL,自然失败。解决方案不是硬编码用户名,而是显式指定 schema:
-- 在 DB2 CLP 中执行(需 JLU 用户权限) GRANT SELECT, INSERT, UPDATE ON TABLE JLU.TEMPL TO USER db2admin; GRANT SELECT ON TABLE JLU.STAFF TO USER db2admin;提示:
GRANT语句必须由对象所有者(此处为JLU)执行,db2admin无权给自己授权。实验中“登录 JLU 用户授予权限”正是此逻辑。切勿在 Java 代码中拼接JLU.前缀——应在连接后通过Connection.getMetaData().getUserName()动态获取当前 schema,再构造全限定名。
2.2 连接字符串与驱动加载:DB2 JDBC URL 的三段式结构解析
实验中多次出现jdbc:db2:sample,这并非通用格式,而是 DB2 的本地嵌入式连接(Embedded Driver)写法。其结构为:jdbc:db2:<database_name>——<database_name>是 DB2 实例中已 catalog 的数据库别名(非路径)。
实际生产中应使用网络驱动(App Driver),URL 格式为:
String url = "jdbc:db2://localhost:50000/SAMPLE:currentSchema=JLU;";其中currentSchema=JLU参数替代了硬编码JLU.,使 SQL 语句可跨用户复用。驱动加载也需升级:
// ❌ 过时写法(DB2 v8.x 旧驱动) Class.forName("COM.ibm.db2.jdbc.app.DB2Driver"); // ✅ 推荐写法(DB2 v11.5+ 通用驱动) Class.forName("com.ibm.db2.jcc.DB2Driver");注意:
com.ibm.db2.jcc.DB2Driver是 IBM 官方维护的 JDBC 4.0+ 驱动,支持自动注册(Java 6+ 可省略Class.forName()),但实验环境为兼容性仍建议保留。
2.3 连接工厂封装:避免每次 new Connection 的资源泄漏风险
八项任务中,每个.java文件都独立调用DriverManager.getConnection(),这在单次运行无问题,但若扩展为多线程或循环操作,极易触发连接耗尽。我们将其封装为DBConnectionPool工厂类:
public class DBConnectionPool { private static final String URL = "jdbc:db2://localhost:50000/SAMPLE:currentSchema=JLU;"; private static final String USER = "db2admin"; private static final String PASS = "db2admin"; public static Connection getConnection() throws SQLException { try { Class.forName("com.ibm.db2.jcc.DB2Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("DB2 Driver not found", e); } return DriverManager.getConnection(URL, USER, PASS); } // 关闭连接的静态工具方法(防止 finally 块重复写 close) public static void closeQuietly(Connection conn) { if (conn != null) try { conn.close(); } catch (SQLException ignored) {} } }设计说明:
getConnection()统一管理 URL、用户、密码,修改一处即全局生效;closeQuietly()避免finally中if (conn != null) conn.close()的模板代码;- 当前为简易版,如需高并发可替换为 HikariCP 等连接池,但课设阶段此封装已足够——它让“连接管理”从八处散落代码变成一处可维护模块。
3. SQL 执行与结果集处理:从 ResultSet.next() 到空值安全的表格渲染
3.1 查询抽象层:为什么 labstaff.java 的 ResultSet 处理必须解耦?
任务 2 中rs.next()循环打印字段,看似简单,但若直接写在 GUI 事件监听器里,会导致 UI 线程阻塞、无法响应、且空值(NULL)直接输出null字符串。我们提取为QueryExecutor工具类:
public class QueryExecutor { public static List<Map<String, Object>> executeQuery(String sql) throws SQLException { List<Map<String, Object>> results = new ArrayList<>(); try (Connection conn = DBConnectionPool.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { ResultSetMetaData meta = rs.getMetaData(); int columnCount = meta.getColumnCount(); while (rs.next()) { Map<String, Object> row = new LinkedHashMap<>(); for (int i = 1; i <= columnCount; i++) { String colName = meta.getColumnName(i); Object value = rs.getObject(i); // ✅ 关键:空值转为字符串"空" row.put(colName, value == null ? "空" : value); } results.add(row); } } return results; } }参数说明:
LinkedHashMap保证列顺序与 SQL 中一致(getColumnName(i)依赖序号);rs.getObject(i)自动处理类型转换(比getString()更安全,避免数字字段转空字符串);value == null ? "空" : value是任务 6 的核心实现,且已内聚到数据层,GUI 层无需关心空值逻辑。
3.2 多行插入的批量执行:如何避免 insert 语句拼接 SQL 注入?
任务 5 要求“单行插入、多行插入、子查询插入”,其中多行插入若用INSERT INTO ... VALUES (...), (...)拼接,极易因用户输入'引发 SQL 注入。正确做法是使用PreparedStatement批量:
public static int batchInsert(List<Object[]> rows) throws SQLException { String sql = "INSERT INTO JLU.TEMPL (ID, NAME, DEPT) VALUES (?, ?, ?)"; try (Connection conn = DBConnectionPool.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { for (Object[] row : rows) { pstmt.setInt(1, (Integer) row[0]); pstmt.setString(2, (String) row[1]); pstmt.setString(3, (String) row[2]); pstmt.addBatch(); // 加入批处理队列 } return pstmt.executeBatch().length; // 返回成功插入行数 } }设计说明:
addBatch()+executeBatch()比逐条executeUpdate()性能提升 5~10 倍;- 参数化
?占位符天然防御 SQL 注入; executeBatch()返回int[],可精确统计每条语句影响行数,用于任务 5 的“插入结果反馈”。
3.3 子查询插入的事务边界:为什么 INSERT ... SELECT 必须显式事务?
任务 5 的“通过子查询插入”如INSERT INTO TEMPL SELECT * FROM STAFF WHERE ID > 10,看似一条 SQL,但若目标表有触发器或外键约束,可能部分成功部分失败。必须包裹在事务中:
public static boolean insertBySelect(String selectSql) throws SQLException { Connection conn = null; try { conn = DBConnectionPool.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 try (Statement stmt = conn.createStatement()) { int count = stmt.executeUpdate("INSERT INTO JLU.TEMPL " + selectSql); conn.commit(); // 全部成功才提交 return count > 0; } } catch (SQLException e) { if (conn != null) conn.rollback(); // 任一失败则回滚 throw e; } finally { if (conn != null) conn.close(); } }提示:
setAutoCommit(false)不是“为了用事务”,而是“为了控制一致性”。实验中任务 3 输入 20 后“修改成功但未生效”,正是因为setAutoCommit(false)后忘记commit()——这恰恰是数据库事务最经典的认知陷阱。
4. GUI 交互与事务控制:从 JOptionPane 弹窗到可编辑表格的落地实现
4.1 GUI 主窗口设计:用 JTable 替代 JOptionPane 的局限性
任务 4 要求用JOptionPane实现 GUI,但JOptionPane.showInputDialog()仅支持单值输入,无法满足任务 8 的“任意行、任意列修改”。我们升级为JFrame+JTable架构:
public class StaffGUI extends JFrame { private JTable table; private DefaultTableModel tableModel; public StaffGUI() { setTitle("Staff 数据管理"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); initTable(); initButtons(); pack(); setLocationRelativeTo(null); } private void initTable() { String[] columns = {"ID", "NAME", "JOB", "SALARY", "DEPT"}; tableModel = new DefaultTableModel(columns, 0) { @Override public boolean isCellEditable(int row, int column) { // ✅ 任务 8:仅 SALARY 和 DEPT 列可编辑(业务规则) return column == 3 || column == 4; } }; table = new JTable(tableModel); JScrollPane scrollPane = new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); } private void initButtons() { JPanel buttonPanel = new JPanel(); JButton loadBtn = new JButton("加载数据"); loadBtn.addActionListener(e -> loadData()); buttonPanel.add(loadBtn); JButton updateBtn = new JButton("提交修改"); updateBtn.addActionListener(e -> submitEdits()); buttonPanel.add(updateBtn); add(buttonPanel, BorderLayout.SOUTH); } }设计说明:
DefaultTableModel支持动态增删行,isCellEditable()精确控制编辑权限;JScrollPane解决数据量大时的滚动问题,比JOptionPane更符合真实应用;- “加载数据”与“提交修改”按钮分离,清晰体现“查-改-存”三步流程。
4.2 表格编辑的脏数据检测:如何只提交被修改的行?
任务 8 要求“对结果集中任意行、任意列修改”,但若用户改了 100 行只提交 1 行,逐行比对原始数据成本高。我们采用“编辑标记法”:
private void submitEdits() { try { for (int row = 0; row < tableModel.getRowCount(); row++) { // 检查该行是否被标记为“已编辑” if ((Boolean) tableModel.getValueAt(row, tableModel.getColumnCount())) { updateRowInDB(row); } } JOptionPane.showMessageDialog(this, "修改已提交"); } catch (SQLException e) { handleDatabaseError(e); } } // 在 tableModel.setValueAt() 后自动标记 @Override public void setValueAt(Object value, int row, int column) { super.setValueAt(value, row, column); // 第 5 列(索引 4)为编辑标记列,true 表示该行已修改 tableModel.setValueAt(true, row, 4); }注意:
tableModel需额外增加一列作为“dirty flag”,避免每次提交都全量比对。这是课设中平衡性能与复杂度的务实选择。
4.3 事务提交的原子性保障:updateRowInDB() 的完整实现
updateRowInDB()必须保证单行更新的 ACID,尤其处理SALARY溢出(任务 3):
private void updateRowInDB(int row) throws SQLException { Connection conn = null; PreparedStatement pstmt = null; try { conn = DBConnectionPool.getConnection(); conn.setAutoCommit(false); int id = (Integer) tableModel.getValueAt(row, 0); String dept = (String) tableModel.getValueAt(row, 4); BigDecimal salary = new BigDecimal(tableModel.getValueAt(row, 3).toString()); // ✅ 溢出防护:检查 salary 是否超出 DECIMAL(10,2) 范围 if (salary.scale() > 2 || salary.precision() > 10) { throw new SQLException("薪资数值超出数据库精度限制", "22003", 801); } String sql = "UPDATE JLU.STAFF SET SALARY = ?, DEPT = ? WHERE ID = ?"; pstmt = conn.prepareStatement(sql); pstmt.setBigDecimal(1, salary); pstmt.setString(2, dept); pstmt.setInt(3, id); int affected = pstmt.executeUpdate(); if (affected == 0) { throw new SQLException("未找到 ID=" + id + " 的员工记录", "02000", 100); } conn.commit(); } finally { if (pstmt != null) pstmt.close(); if (conn != null) conn.close(); } }关键点:
BigDecimal精确处理小数,避免float/double的精度丢失;scale()和precision()主动校验数值范围,提前拦截溢出(比等 DB2 报错再捕获更友好);affected == 0明确提示“记录不存在”,对应任务 3 中输入 16 时“修改了 0 行”的原因。
5. 异常处理与错误反馈:从 SQLState 字符串到用户可读的对话框
5.1 异常分类映射表:将 DB2 错误码翻译成业务语言
任务 7 要求“以对话框提示违反约束、类型不匹配、溢出等错误”,但SQLException的SQLState是五位字符串(如'22003'),用户无法理解。我们建立映射表:
| SQLState | 错误类型 | 用户提示文案 | 应对建议 |
|---|---|---|---|
23505 | 主键/唯一约束冲突 | “ID 重复:该员工编号已存在” | 检查输入编号或生成 UUID |
22003 | 数值溢出 | “薪资超出范围:请确保小数位不超过2位” | 修改输入或调整数据库精度 |
22005 | 类型不匹配 | “部门名称格式错误:请输入字母或数字” | 清空输入框并聚焦该单元格 |
42704 | 对象不存在 | “表未找到:请确认 JLU.TEMPL 已创建” | 运行建表脚本或检查 schema |
private void handleDatabaseError(SQLException e) { String sqlState = e.getSQLState(); String message = switch (sqlState) { case "23505" -> "ID 重复:该员工编号已存在"; case "22003" -> "薪资超出范围:请确保小数位不超过2位"; case "22005" -> "部门名称格式错误:请输入字母或数字"; case "42704" -> "表未找到:请确认 JLU.TEMPL 已创建"; default -> "数据库操作失败:" + e.getMessage(); }; JOptionPane.showMessageDialog(this, message, "操作失败", JOptionPane.ERROR_MESSAGE); }提示:
switch表达式(Java 14+)比if-else更易维护,新增错误类型只需加一行case。
5.2 SQLWarning 的主动捕获:为什么 labstaff1.java 要检查警告?
任务 8 明确要求处理SQLWarning,它不同于SQLException,不中断执行但提示潜在问题(如数据截断、隐式类型转换)。必须在PreparedStatement执行后立即检查:
PreparedStatement stmt = conn.prepareStatement("SELECT ID, NAME, SALARY FROM JLU.STAFF WHERE DEPT = ?"); stmt.setString(1, "SALES"); ResultSet rs = stmt.executeQuery(); // ✅ 立即检查警告(在 executeQuery() 后,rs.next() 前) SQLWarning warning = stmt.getWarnings(); if (warning != null) { String warnMsg = "查询警告:" + warning.getMessage(); // 记录日志或弹窗提示(课设中可简化为 System.out) System.out.println(warnMsg); // 清除警告链,避免重复获取 stmt.clearWarnings(); }避坑 / 常见问题 / 排查 / 注意
现象:
JOptionPane.showInputDialog()输入中文后数据库存为乱码
原因:DB2 数据库编码为 UTF-8,但 Java 进程默认编码为 GBK(Windows 系统常见)
解决:启动 JVM 时添加-Dfile.encoding=UTF-8,或在连接 URL 中追加;charset=UTF-8(DB2 驱动支持)现象:
PreparedStatement.setInt(1, mydeptno)报Data type mismatch
原因:mydeptno是String类型(来自JOptionPane输入),但setInt()要求int
解决:先Integer.parseInt(mydeptno)并捕获NumberFormatException,再传入setInt()现象:GUI 窗口关闭后程序未退出,后台线程仍在运行
原因:JFrame默认setDefaultCloseOperation()为HIDE_ON_CLOSE,非EXIT_ON_CLOSE
解决:显式调用setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE),或重写windowClosing()事件手动关闭连接池现象:执行
INSERT INTO TEMPL SELECT ...后,JTable未刷新显示新数据
原因:JTable的DefaultTableModel未收到数据变更通知
解决:执行插入后,调用tableModel.setRowCount(0)清空,再loadData()重新填充(见 4.1 节)现象:
rs.getObject("SALARY")返回Double而非BigDecimal,导致精度丢失
原因:DB2 驱动对DECIMAL字段的默认映射是Double(历史兼容性)
解决:改用rs.getBigDecimal("SALARY"),或在连接 URL 中添加;useJDBC4ColumnNameAndLabelSemantics=true
6. 方案验收与答辩技巧:用一张表跑通全部八项任务的演示清单
6.1 八项任务到功能点的映射验证表
不要等到答辩当天才手忙脚乱找截图。按此表逐项验证,每项打钩即代表可演示:
| 任务编号 | 对应功能点 | 验证方式 | 预期结果 |
|---|---|---|---|
| 任务 1 | 权限与全限定名修正 | 在 DB2 CLP 中执行SELECT * FROM JLU.TEMPL,再用db2admin账户连接 Java 程序 | 成功返回数据,无undefined name错误 |
| 任务 2 | 基础查询与结果遍历 | 点击“加载数据”按钮,观察JTable是否显示STAFF表全部字段 | 表格填充完整,空值显示为“空” |
| 任务 3 | 事务更新与溢出防护 | 修改某行SALARY为9999999999.99(超 10 位),点击“提交修改” | 弹出“薪资超出范围”提示,原值不变 |
| 任务 4 | GUI 化输入与结果输出 | 删除某行后点击“提交修改”,再点击“加载数据” | 表格中该行消失,DB2 中SELECT确认已删除 |
| 任务 5 | 三种插入方式 | 分别点击“单行插入”、“多行插入”、“子查询插入”按钮,输入合法数据 | JTable实时新增行,DB2 中SELECT COUNT(*)验证数量增长 |
| 任务 6 | 空值统一显示 | 在TEMPL表中UPDATE JLU.TEMPL SET NAME = NULL WHERE ID = 1,再“加载数据” | 表格中NAME列显示“空”,非null或空白字符串 |
| 任务 7 | 异常分类弹窗 | 故意输入重复 ID 插入、超长部门名、非法符号 SQL,触发对应操作 | 弹出精准提示文案,非泛泛的“数据库错误” |
| 任务 8 | 任意单元格编辑 | 双击SALARY或DEPT列修改值,按 Enter 确认,点击“提交修改” | DB2 中该行对应字段更新,其他列保持不变 |
6.2 答辩现场的三分钟演示脚本(可直接背诵)
“老师好,我演示的是基于 DB2 的数据库应用开发闭环。首先,我通过统一的连接工厂管理数据库连接,避免硬编码和资源泄漏(指向
DBConnectionPool.java)。其次,所有 SQL 执行都经过QueryExecutor封装,它自动处理空值为‘空’,并支持批量插入(打开QueryExecutor.java)。第三,GUI 界面采用JTable实现可编辑表格,仅允许修改业务相关字段(指向isCellEditable()方法)。最关键的是异常处理——我建立了 SQLState 映射表,把 DB2 的22003错误翻译成‘薪资超出范围’这样的用户语言(展示handleDatabaseError())。最后,所有功能都通过这张验证表确保覆盖全部八项任务(展示表格)。整个方案的特点是:连接可控、SQL 可读、事务可管、错误可懂。”
6.3 我的血泪教训:从“改了没生效”到“一次过答辩”的强制习惯
第一次做任务 3 时,我把setAutoCommit(false)写进代码,却忘了commit(),折腾两小时才发现——那以后我养成了一个死规矩:只要代码里出现setAutoCommit(false),旁边必须紧跟着try-catch-finally块,且finally中必须有conn.commit()或conn.rollback(),二者必居其一,绝不留空。这个习惯让我后续所有事务操作零翻车。另外,我坚持所有数据库操作入口(executeQuery、batchInsert、insertBySelect)都用try-with-resources,确保Connection、Statement、ResultSet无论成功失败都自动关闭。这些不是教条,是我在模拟项目 X 中被线上连接泄漏搞崩溃后,用监控日志和堆栈跟踪换来的肌肉记忆。希望帮到你。
本文还有配套的精品资源,点击获取