ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从实验报告到可运行工程:JDBC 数据库开发实战指南

从实验报告到可运行工程:JDBC 数据库开发实战指南

简介:这份资源是吉林大学《数据库应用程序开发》课程的实验报告,面向正在学习Java与DB2数据库交互的高校学生及自学者,帮助其完成JDBC编程实验并理解数据库应用开发的核心流程。报告围绕JDBC连接建立、Statement与PreparedStatement使用、ResultSet结果集遍历、事务提交控制及SQL异常处理等知识点展开,并记录了权限授予、对象名称匹配、数值溢出与事务未提交等典型调试过程,可作为实验排错与知识点复盘的参考。资源包共1个doc文件,大小约1.76MB,内容以实验步骤、调试记录与运行结果截图为主,结构完整便于对照学习。目前已有902人学习下载,适合需要完成同类数据库实验或巩固JDBC编程基础的读者参考借鉴。

1. 从一份实验报告里拆出一套能跑的 JDBC 工程

很多人看到「数据库应用程序开发实验报告」这几个字,第一反应是交作业用的文档,翻完就扔。我最初也这么想,直到把里面的八个任务按顺序摆开,才发现它其实是一条完整的 JDBC 学习链路:从连库、查单表、改数据,到事务提交、GUI 输入、批量插入、空值处理、异常弹窗,最后落到结果集任意行列修改。换句话说,这份报告不是讲概念,而是把「Java 程序怎么和 DB2 说话」这件事拆成了可复现的步骤。适合两类人:刚学 JDBC、想找一个能跑通的完整样例的新手;以及需要给团队做数据库访问层培训、缺一套渐进式练习的熟手。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把它当成一个真实项目来拆。

2. 项目定位与技术栈:这不是文档,是一条 JDBC 主线

2.1 八个任务其实是一条能力递进链

把报告里的任务按依赖关系重排,会发现它有一条清晰的主线,而不是零散的八个练习。任务 1 解决权限和对象名,是环境准备;任务 2 用Statement做查询,打通「连接 → 执行 → 遍历结果集」;任务 3 引入PreparedStatement和手动事务,开始碰更新和提交;任务 4 把控制台输入换成JOptionPane,进入 GUI 形态;任务 5 做单行、多行、子查询三种插入;任务 6 处理空列显示;任务 7 统一异常弹窗;任务 8 落到结果集任意行列修改。这条链的价值在于,每一步只加一个新概念,前一步的代码可以直接复用。我一般带新人时也按这个顺序走,因为一次性把连接池、事务、异常全塞进去,新手根本分不清哪行代码在干什么。

从工程角度看,这套东西的定位是「教学型最小可用工程」:没有框架、没有 ORM、没有连接池,全部是原生 JDBC API。好处是每一行都能对应到数据库的真实行为,坏处是它离生产还有距离。但恰恰因为裸,排查问题时不会被框架层遮住,这对理解 JDBC 的脾气很有用。

2.2 技术栈与运行环境清单

这套代码依赖的东西不多,但版本和驱动对不上就会直接翻车。下面是我整理的环境清单,按「必须有」和「常见搭配」分开:

组件作用注意点
JDK编译运行 Java 程序报告里的写法偏旧,用 JDK 8 最稳
DB2 数据库提供 SAMPLE 库和 JLU 模式需要提前建好表和用户权限
DB2 JDBC 驱动COM.ibm.db2.jdbc.app.DB2Driver驱动 jar 必须在 classpath 里
java.sql包连接、语句、结果集、异常JDK 自带,无需额外引入
javax.swing.JOptionPaneGUI 输入输出JDK 自带

这里有个血泪经验:报告里写的驱动类名是COM.ibm.db2.jdbc.app.DB2Driver,这是老式 Type 2 驱动的写法,依赖本地 DB2 客户端。如果你机器上只装了服务端没装客户端,Class.forName会直接抛ClassNotFoundException。常见做法是换成 Type 4 的com.ibm.db2.jcc.DB2Driver,它纯 Java 实现,不依赖本地库。改的时候连接 URL 也要跟着从jdbc:db2:sample换成jdbc:db2://主机:端口/sample的形式。这一步不提前说清,后面所有任务都跑不起来。

2.3 目录与文件组织建议

报告本身是文档,没有给工程结构。按它的任务划分,我一般会组织成下面这样,方便一个任务一个文件对照着看:

jdbc-lab/ ├── lib/ │ └── db2jcc.jar # DB2 JDBC 驱动 ├── src/ │ ├── LabStaff.java # 任务2:查询 staff 表 │ ├── LabUpdate.java # 任务3:PreparedStatement 更新 │ ├── LabUpdateGui.java # 任务4:JOptionPane 版本 │ ├── LabInsert.java # 任务5:单行/多行/子查询插入 │ ├── LabInsertNull.java # 任务6:空值显示为“空” │ ├── LabInsertEx.java # 任务7:异常统一弹窗 │ └── LabResultEdit.java # 任务8:结果集任意行列修改 └── run.sh # 编译与运行脚本

这样拆的好处是每个文件只解决一个任务,出问题时能快速定位是哪一步引入的。如果全塞进一个Main.java,后面改 GUI 和异常处理时会互相干扰,调试成本翻倍。

3. 跑起来:从连库到第一个查询结果

3.1 环境准备与权限那点事

任务 1 里记录的第一个错误很典型:db2admin没有访问EMPLOYEE表的权限。这不是代码问题,是数据库授权问题。解决方式是登录有权限的用户,执行GRANT把访问权授给db2admin。第二个错误是DB2ADMIN.TEMPL未定义,原因是代码里写的是TEMPL,而实际对象在JLU模式下,得写成JLU.TEMPL。这两个错误合起来说明一件事:JDBC 报的「对象不存在」和「权限不足」,八成不是 Java 写错了,而是数据库侧的 schema 和 grant 没对齐。

我一般会先跑一条最简单的 SQL 确认连通性,再动 Java 代码:

-- 确认当前用户能看到 JLU 模式下的表 SELECT TABSCHEMA, TABNAME FROM SYSCAT.TABLES WHERE TABSCHEMA = 'JLU'; -- 把 staff 表的查询权限授给应用用户 GRANT SELECT, UPDATE, INSERT ON JLU.STAFF TO USER db2admin;

第一条查系统目录,确认表到底在哪个 schema 下;第二条补权限。参数说明:TABSCHEMA是模式名,大小写敏感,DB2 里默认存大写;GRANT后面的权限按任务需要给,任务 3 要更新就加UPDATE,任务 5 要插入就加INSERT。这一步做完再跑 Java,能省掉大量「到底是代码错还是库错」的来回猜。

3.2 第一个查询程序:连接、语句、结果集三段式

任务 2 的核心是把 JDBC 查询的三段式写全:建连接、建语句、拿结果集。报告里给的片段是骨架,我补成能直接跑的完整版本:

import java.sql.*; public class LabStaff { public static void main(String[] args) { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1. 加载驱动,Type 4 写法更通用 Class.forName("com.ibm.db2.jcc.DB2Driver"); // 2. 建立连接,URL 里带主机和端口 conn = DriverManager.getConnection( "jdbc:db2://localhost:50000/sample", "db2admin", "db2admin"); // 3. 创建 Statement 并执行查询 stmt = conn.createStatement(); rs = stmt.executeQuery( "SELECT NAME, JOB, SALARY FROM JLU.STAFF WHERE ID = 10"); // 4. 遍历结果集,next() 返回 false 表示没有更多行 while (rs.next()) { String name = rs.getString("NAME"); String job = rs.getString("JOB"); double salary = rs.getDouble("SALARY"); System.out.println(name + " | " + job + " | " + salary); } } catch (ClassNotFoundException e) { System.out.println("驱动没找到,检查 classpath:" + e.getMessage()); } catch (SQLException e) { System.out.println("SQL 错误码:" + e.getErrorCode() + " 状态:" + e.getSQLState() + " 消息:" + e.getMessage()); } finally { // 5. 逆序关闭,避免资源泄漏 try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } } }

逐段说明:Class.forName负责把驱动类加载进 JVM,不加载的话DriverManager找不到对应协议的驱动;getConnection的三个参数分别是 URL、用户名、密码,URL 里的localhost:50000是 DB2 默认监听端口,实际环境按需改;executeQuery只用于SELECT,返回ResultSet;rs.next()每次把游标下移一行,返回布尔值,所以遍历必须用while而不是if。最后finally里逆序关闭是硬规矩,ResultSet依赖Statement,Statement依赖Connection,顺序反了会报「对象已关闭」。

报告里任务 2 还提到boolean more = rs.next();这种写法,那是为了演示单步移动,实际遍历还是用while更稳妥。新手容易在这里写成if (rs.next()),结果只输出第一行,然后怀疑查询条件写错了,其实是遍历逻辑的问题。

3.3 更新与事务:为什么改了却没生效

任务 3 是整份报告里最有价值的一段,因为它踩到了事务的坑。代码结构是:关掉自动提交、用PreparedStatement设参数、执行更新、出错时取SQLCode和SQLState。报告里记录了一个现象:输入 20 或 30 时执行成功,但数据库没变化。排查后发现是setAutoCommit(false)之后没有commit(),事务一直挂着没提交。

Connection conn = DriverManager.getConnection(url, user, pwd); conn.setAutoCommit(false); // 手动控制事务 String sql = "UPDATE JLU.STAFF SET SALARY = SALARY * ? WHERE DEPT = ?"; try (PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setDouble(1, 1.01); // 参数1:涨幅 pstmt.setInt(2, deptNo); // 参数2:部门号 int count = pstmt.executeUpdate(); System.out.println("影响行数:" + count); conn.commit(); // 关键:不提交等于没改 } catch (SQLException e) { conn.rollback(); // 出错回滚,避免半截数据 System.out.println("错误码:" + e.getErrorCode() + " 状态:" + e.getSQLState() + " 消息:" + e.getMessage()); }

参数说明:setDouble(1, 1.01)里的 1 是占位符序号,从 1 开始,不是 0;executeUpdate返回受影响行数,返回 0 说明WHERE没匹配到记录,不是执行失败。报告里还分析了溢出:把涨幅设成 2 时,15 和 20 都溢出,设成 1.01 就不溢出,证明是修改后的数值超出SALARY字段精度。这个排查思路很值得学——用不同参数做对照实验,把「代码问题」和「数据问题」分开。

提示:setAutoCommit(false)之后,每条 DML 都在同一个事务里,直到commit或rollback。忘了提交是 JDBC 更新类任务最高频的翻车点,没有之一。

4. 核心模块拆解:GUI、批量插入与异常处理

4.1 用 JOptionPane 把控制台程序改成 GUI

任务 4 要求把labupdate.java改成 GUI 形式,输入和输出都走JOptionPane。改动量其实很小,核心是把System.out和Scanner换成弹窗方法。JOptionPane.showInputDialog返回字符串,需要自己转成整数;showMessageDialog负责输出结果和错误。

import javax.swing.JOptionPane; public class LabUpdateGui { public static void main(String[] args) { String input = JOptionPane.showInputDialog("请输入部门号:"); if (input == null) return; // 用户点了取消 int deptNo; try { deptNo = Integer.parseInt(input.trim()); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "部门号必须是数字"); return; } // 后续连接、更新逻辑与任务3一致,结果用弹窗输出 JOptionPane.showMessageDialog(null, "已处理部门:" + deptNo); } }

逻辑说明:showInputDialog在用户点取消时返回null,不判空直接parseInt会抛空指针;trim()是为了去掉输入框里可能带的前后空格,这个细节在手工输入场景很常见。输出统一走showMessageDialog,第一个参数传null表示居中显示。改成 GUI 后,异常处理也要跟着改——不能再往控制台打堆栈,得把SQLException的消息塞进弹窗,否则用户看不到任何反馈。

4.2 三种插入方式:单行、多行、子查询

任务 5 要求实现单行插入、多行插入、通过子查询插入。这三种对应三种不同的 SQL 形态,但都可以用PreparedStatement统一处理。单行是标准INSERT ... VALUES;多行可以循环执行同一条预编译语句,也可以拼多组VALUES;子查询插入是INSERT INTO ... SELECT ...,数据来源是另一张表。

// 单行插入:占位符按顺序赋值 String single = "INSERT INTO JLU.TEMPL (ID, NAME, SALARY) VALUES (?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(single)) { ps.setInt(1, 101); ps.setString(2, "张三"); ps.setDouble(3, 5000.0); ps.executeUpdate(); } // 多行插入:复用同一条语句,循环设参 int[][] rows = {{102, 6000}, {103, 7000}}; try (PreparedStatement ps = conn.prepareStatement(single)) { for (int[] row : rows) { ps.setInt(1, row[0]); ps.setString(2, "员工" + row[0]); ps.setDouble(3, row[1]); ps.addBatch(); // 加入批次,减少往返 } ps.executeBatch(); // 一次性提交批次 } // 子查询插入:数据来自 STAFF 表 String sub = "INSERT INTO JLU.TEMPL (ID, NAME, SALARY) " + "SELECT ID, NAME, SALARY FROM JLU.STAFF WHERE DEPT = ?"; try (PreparedStatement ps = conn.prepareStatement(sub)) { ps.setInt(1, 20); ps.executeUpdate(); }

参数说明:addBatch和executeBatch是批量插入的标准组合,适合数据量大的场景,能显著减少网络往返;子查询插入里SELECT的列顺序必须和INSERT的列顺序一一对应,类型也要兼容,否则会报类型不匹配。报告里还记录了一个坑:无法创建JLU.TEMPL表,原因是之前给EMPLOYEE建过别名TEMPL,删掉别名才能建表。这说明 DB2 里别名和表共享命名空间,建表前先查一下SYSCAT.TABLES和SYSCAT.ALIASES能避免这类冲突。

4.3 空值显示与异常统一弹窗

任务 6 要求把结果集中为空的列显示成「空」。JDBC 里getString对NULL返回null,直接拼接会打出null字样。处理方式是在取值后判空,统一替换。任务 7 要求把违反约束、类型不匹配、溢出、非法符号等错误都用对话框提示,这需要按SQLException的错误码或状态分类。

// 空值统一显示 String name = rs.getString("NAME"); String display = (name == null) ? "空" : name; // 异常分类弹窗 catch (SQLException e) { String msg; switch (e.getErrorCode()) { case -803: msg = "违反唯一约束"; break; // 主键/唯一索引冲突 case -302: msg = "数值溢出或类型不匹配"; break; case -007: msg = "SQL 语句含非法符号"; break; default: msg = "数据库错误:" + e.getMessage(); } JOptionPane.showMessageDialog(null, msg); }

逻辑说明:DB2 的错误码是负数,-803对应唯一约束冲突,-302对应数值转换或溢出,-007对应语句语法问题。用switch分类比直接抛原始消息友好得多,用户能看懂「违反约束」而不是一串英文堆栈。注意getErrorCode返回的是数据库厂商码,不同数据库不一样,换库时要重新对照。

4.4 结果集任意行列修改的实现思路

任务 8 要求对结果集中任意行、任意列做修改。标准做法是用可滚动、可更新的ResultSet,建语句时指定TYPE_SCROLL_INSENSITIVE和CONCUR_UPDATABLE,然后通过absolute定位行、updateXxx改列、updateRow落库。

Statement stmt = conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, // 可滚动,能前后移动 ResultSet.CONCUR_UPDATABLE); // 可更新,改完能写回 ResultSet rs = stmt.executeQuery( "SELECT ID, NAME, SALARY FROM JLU.STAFF"); if (rs.absolute(3)) { // 定位到第 3 行 rs.updateDouble("SALARY", 8888.0); // 改指定列 rs.updateRow(); // 提交这一行的修改 }

参数说明:TYPE_SCROLL_INSENSITIVE表示游标可前后滚动,但对底层数据变化不敏感;CONCUR_UPDATABLE表示结果集可更新,缺了它调updateRow会抛异常。absolute(3)的行号从 1 开始,不是 0。这套写法对单表查询有效,如果查询带JOIN或多表,可更新结果集往往不生效,得退回UPDATE语句。报告里任务 8 还要求处理SQLWarning,用stmt.getWarnings()取警告链,适合在批量操作后检查有没有被忽略的提示。

5. 避坑与排查:五个真实踩过的坑

5.1 驱动类找不到或连接被拒

现象:运行时报ClassNotFoundException: COM.ibm.db2.jdbc.app.DB2Driver,或者连接时提示拒绝。原因有两个:一是驱动 jar 没进 classpath,二是用了 Type 2 驱动但本机没装 DB2 客户端。解决:换成 Type 4 驱动com.ibm.db2.jcc.DB2Driver,编译和运行时都用-cp把驱动 jar 带上,连接 URL 改成带主机端口的完整形式。我一般会在启动脚本里固定写死 classpath,避免每次手动敲。

5.2 更新成功但数据没变

现象:executeUpdate返回大于 0,程序也不报错,但查库发现数据没动。原因:setAutoCommit(false)之后没有commit(),事务悬着。解决:在更新逻辑末尾显式commit,异常分支里rollback。这个坑在任务 3 里被完整记录,属于 JDBC 事务的经典翻车点,新手几乎必踩。

5.3 对象名或 schema 对不上

现象:报「TEMPL 未定义」或「表不存在」。原因:代码里写的是不带 schema 的表名,而实际对象在JLU模式下;或者别名和表名冲突。解决:SQL 里统一写全JLU.表名,建表前先查SYSCAT.ALIASES确认没有同名别名。报告里任务 1 和任务 5 各踩了一次,说明这个问题会反复出现。

5.4 数值溢出与类型转换

现象:更新SALARY时报溢出,或者输入非数字时程序崩溃。原因:修改后的数值超出字段精度,或者parseInt没做异常捕获。解决:先用小系数做对照实验确认是数据问题还是代码问题;输入侧用try-catch包住NumberFormatException,给用户明确提示而不是抛堆栈。

5.5 结果集遍历只出一行

现象:查询明明有多条记录,程序只输出第一条。原因:把while (rs.next())写成了if (rs.next()),游标只移动一次。解决:遍历一律用while,需要单步移动的场景才用if。这个错误很隐蔽,因为程序不报错,只是结果少了,排查时容易往 SQL 条件上想。

6. 二次开发:把这套实验代码改成能复用的工具类

这份报告的代码是「一个任务一个 main 方法」,直接拿去用会有大量重复:连接参数散落各处、关闭逻辑反复写、异常处理各写各的。二次开发的第一步,我一般会把连接和关闭抽成一个工具类,让每个任务只关心自己的 SQL。

public class DbUtil { private static final String URL = "jdbc:db2://localhost:50000/sample"; private static final String USER = "db2admin"; private static final String PWD = "db2admin"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } // 统一关闭,顺序固定 public static void close(ResultSet rs, Statement st, Connection conn) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (st != null) st.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }

抽完之后,任务 2 的查询可以缩成十几行,任务 3 的更新只保留业务 SQL 和事务控制。第二步是把异常分类逻辑做成一个showSqlError(SQLException e)方法,所有 GUI 任务共用,避免每个文件里复制一遍switch。第三步,如果要做成真正能用的工具,把PreparedStatement的参数设置也封装一下,用Object... params统一赋值,能省掉大量setInt、setString的重复代码。

验证改造是否成功,我的习惯是拿任务 3 的溢出场景做回归:输入 20 看是否弹「数值溢出」,输入 16 看是否提示「影响 0 行」,输入正常值看数据是否真的落库。三个场景都过,说明连接、事务、异常三条线都没被改坏。从那以后我每次重构 JDBC 代码,都强制把「提交、回滚、影响行数」这三件事各跑一遍再收工,因为这三处最容易在封装过程中被悄悄改掉。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进