JDBC数据库应用开发实验:六段链路与高频易错点复盘
简介吉林大学《数据库应用程序开发》实验报告面向高校数据库课程学习者与Java JDBC初学者围绕DB2环境下的数据库编程实验整理成完整文档可作为课程实验参考与排错手册。全文按任务推进涵盖JDBC连接建立、Statement与PreparedStatement使用、ResultSet结果集遍历、事务提交控制、JOptionPane图形界面交互及异常处理等内容并记录了权限授予、对象名不匹配、数值溢出、修改未生效等典型问题的定位过程与解决思路。压缩包共1个doc文件约1.76MB为可编辑的Word实验报告含各任务调试过程、原因分析与运行结果截图便于对照复现实验步骤。目前已有903人学习下载适合需要完成课程实验、理解Java与数据库交互细节或补充JDBC排错经验的学生按任务查漏补缺。1. 先把地图摊开数据库应用程序开发实验的六段链路“数据库应用程序开发”这门课的实验看上去是七八个互不相干的编程任务摊开看其实是一条固定链路加载驱动 → 建立连接 → 造语句对象 → 拿结果集 → 控制事务 → 接住异常。每个任务只是在这条链上换一个环节做文章。反直觉的地方在于真正让人卡住的往往不是 SQL 写不出来而是 SQL 之外的东西连接用户对应的默认模式、被别名占掉的表名、自动提交开关、异常里的状态码。这些点在日常写代码时没人提醒一进实验环境就全冒出来。下面按这条链路逐段拆适合正在做 JDBC 实验、或者准备回头复习数据库编程的人当一张可以逐条对照的地图用。2. 连接层驱动加载、URL 拼装与模式限定连接层是整个实验里最容易被当成“抄一遍模板就行”的部分但它恰恰是错误最密集的地方。实验任务 1 到任务 3 报出来的两类错——没有权限、表名未定义——都不在 Java 代码的逻辑里而在“我用哪个身份连的、这个身份默认看到哪个模式、SQL 里的裸表名被解析成了谁”这三个问题上。把这三件事搞清楚后面所有任务里的 SQL 都能少写一半的调试时间。2.1 五步连接模板每一步在干什么教材里给的模板代码基本固定值得逐行拆开看因为每一行都对应一个可能失败的环节。下面这段是实验里反复出现的骨架只把模式名换成了通用的教学模式 LABimport java.sql.*; public class LabConnect { public static void main(String[] args) throws Exception { // 1. 加载驱动触发驱动类静态块向 DriverManager 注册自己 Class.forName(COM.ibm.db2.jdbc.app.DB2Driver); // 2. 建立连接URL 三段式 jdbc:子协议:数据库名 Connection sample DriverManager.getConnection( jdbc:db2:sample, db2admin, db2admin); // 3. 创建语句对象用来承载 SQL Statement stmt sample.createStatement(); // 4. 执行查询返回结果集表名写全限定名避免默认模式干扰 ResultSet rs stmt.executeQuery( select NAME, JOB, SALARY from LAB.staff where ID 10); // 5. 游标下移一行再取值next() 返回 false 表示没有下一行 while (rs.next()) { System.out.println(rs.getString(NAME) / rs.getDouble(SALARY)); } rs.close(); stmt.close(); sample.close(); } }第 1 行的Class.forName作用是让驱动类的静态初始化块执行静态块里会调用DriverManager.registerDriver。Java 6 之后有了 SPI 自动发现机制理论上可以省略这一行但实验环境里的 DB2 驱动版本往往偏老显式写出来最保险。驱动类名本身也有讲究COM.ibm.db2.jdbc.app.DB2Driver是本地客户端那一类com.ibm.db2.jdbc.net.DB2Driver走网络层两者 URL 前缀也不同写错了会直接抛ClassNotFoundException或者连接被拒。第 2 行的 URL 是三段结构jdbc是协议名db2是子协议由驱动识别sample是数据库名。用户名和口令分开传参不要拼进 URL 里拼进去一旦密码带特殊字符就会解析出错。第 4 行的查询里表名写成LAB.staff而不是裸的staff原因在下一节展开这是实验里出现频率最高的一类报错源头。第 5 行的next()有个容易记反的细节结果集刚拿到时游标停在第一行之前必须先调一次next()才能读第一行。所以“读一行”和“遍历”用的是同一个方法循环写法天然正确但手写单行读取时常常忘记先调一次结果读到的是空值或者直接抛异常。2.2 全限定名与默认模式TEMPL 为什么显示“未定义”实验里出现过一条典型报错命令行执行时提示DB2ADMIN.TEMPL 是一个未定义的名称。这条报错的关键不是 TEMPL 不存在而是前面的DB2ADMIN这个模式前缀。数据库里每个对象都属于某个模式schemaSQL 里写裸表名时数据库会拿“当前连接的默认模式”去补全。默认模式通常等于连接用户名的 uppercase 形式所以用db2admin登录裸写TEMPL就被解析成DB2ADMIN.TEMPL。但教学环境里表往往建在另一个模式比如给每个学生分配的模式LAB下实际对象是LAB.TEMPL两边对不上于是报未定义。解决办法有两种选一种并坚持用到底做法写法适用场景SQL 里写全限定名select * from LAB.TEMPL临时脚本、单人实验改一处生效连接后设置当前模式set schema LAB程序里 SQL 很多统一改一次第二种做法在 JDBC 里对应执行一条Statement.execute(set schema LAB)之后同一连接上的裸表名都会解析到 LAB 下。这一步放在连接建立之后、业务 SQL 之前位置不能乱。反过来如果程序里混用了两种风格一半写全限定名一半写裸名排查时会非常痛苦因为报错信息只告诉你“未定义”不会告诉你它拿哪个模式去补的。2.3 别名、视图、表共用一个命名空间实验任务 5 里还有一条更隐蔽的报错建LAB.TEMPL表失败。原因不是权限而是这个模式里已经存在一个叫 TEMPL 的别名alias指向之前建的 EMPLOYEE 表。在 DB2 里表、视图、别名共享同一个模式内的对象命名空间名字不能重复不管类型是否相同。确认某个名字被什么对象占了可以查系统目录-- 查某个模式下所有对象的名字和类型 SELECT TABNAME, TYPE FROM SYSCAT.TABLES WHERE TABSCHEMA LAB; -- TYPE 取值T 表、V 视图、A 别名命令行里等价的操作是LIST TABLES FOR SCHEMA LAB。查到是别名就先DROP ALIAS LAB.TEMPL再建表查到是残留的旧表就先清数据或改名。这个坑的教训是报“已存在”时不要急着换名字先弄清楚占用名字的是什么类型的对象否则换个名字之后还会在别的地方再撞一次。2.4 权限GRANT 到底授的是什么任务 1 的第一条错误是db2admin没有访问 EMPLOYEE 表的权限。这类错误的判断依据很直接SQL 语句本身没问题换一个身份执行就成功那就说明问题出在授权上。授权动作必须由对象的所有者或者有管理权限的账号来做语法是标准的-- 把查询权限授给指定用户 GRANT SELECT ON TABLE LAB.EMPLOYEE TO USER db2admin; -- 增删改查一起授 GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE LAB.EMPLOYEE TO USER db2admin;粒度上要注意三点。第一权限是按对象和操作分开的只授了 SELECT程序里一旦执行 UPDATE 还是会报错实验里跨任务复用时尤其容易漏。第二GRANT ... TO USER后面跟的是数据库用户不是模式名两者别混。第三授权之后已有的连接不会自动刷新权限需要重新建立连接才生效这一点在调试时特别容易被误判成“授权没成功”。3. 语句层Statement、PreparedStatement 与结果集游标连接建立之后所有交互都通过语句对象完成。实验里出现了两种语句对象和三种结果集用法它们的差别不在于“哪个更高级”而在于“这条 SQL 要执行几次、有没有外部输入、结果要不要回头改”。把这三个问题想清楚选型就不会错。3.1 两种语句对象的分工对比下面这张表是实验里两种对象的核心差异复习时按行过一遍即可维度StatementPreparedStatementSQL 形态每次执行传入完整字符串创建时传入带?的模板编译一次参数设置无靠字符串拼接setInt/setString按序号赋值重复执行效率每次重新解析、编译复用执行计划批量场景优势明显外部输入安全拼接容易把数据当语句参数只当值不参与语法解析典型用途固定条件的一次性查询带用户输入、循环执行、批量插入实验任务 2 用Statement查固定 ID 的记录任务 3 用PreparedStatement做带参数的更新这个分工是对的。判断标准可以简化成一句话SQL 里只要有外部传进来的值就用PreparedStatement没有外部值、只执行一次Statement写起来更短。需要注意PreparedStatement的预编译优势是相对于同一个连接而言的。连接一关缓存的执行计划就没了所以“预编译”不等于“跨程序永久缓存”不要把它当成性能优化的万能选项。3.2 ResultSet 的游标模型与取值方式结果集本质上是一个带游标的二维表。默认类型是只进只读游标只能往下走next()返回布尔值表示有没有下一行。取值有两个重载按列序号getString(1)和按列名getString(NAME)。列名写法可读性好但列名拼错时运行期才报错序号写法快但 SELECT 列表一改顺序就全错。实验代码里两种都出现过建议在同一个项目里统一成列名。空值处理是任务 6 的核心。数据库里的 NULL 取出来getString返回 Java 的nullgetInt返回 0getDouble返回 0.0。后两种是“用 0 冒充空”光看返回值分不清到底是空还是真的是 0。正确做法是取完值再调一次wasNull()// 取字符串null 直接判断即可 String name rs.getString(NAME); if (name null) { name 空; // 任务 6 要求的显示替换 } // 取数值必须配合 wasNull 才能区分 0 和 NULL double salary rs.getDouble(SALARY); if (rs.wasNull()) { System.out.print(空); } else { System.out.print(salary); }wasNull()判断的是“最近一次 getXxx 调用取到的值是不是 NULL”所以它必须紧跟在取值之后中间插一句别的取值就会把状态覆盖掉。这个顺序约束是硬性的写错不会报错只会得到错误的判断结果属于调试时最难发现的那一类。3.3 参数占位符的编号与类型匹配PreparedStatement里的?从 1 开始编号按在 SQL 中出现的先后顺序排跟列名、表名没有任何关系。看下面这条 SQL// SQL 中有两个占位符第 1 个是乘数第 2 个是部门号 String sql update LAB.staff set salary salary * ? where dept ?; PreparedStatement pstmt sample.prepareStatement(sql); pstmt.setDouble(1, 1.01); // 乘数 pstmt.setInt(2, mydeptno); // 部门号 int updateCount pstmt.executeUpdate(); // 返回受影响行数实验记录里写的是pstmt.setInt(1, mydeptno)这在只有一个占位符的版本里是对的一旦 SQL 改成两个问号部门号就必须挪到序号 2。序号写错有两种后果类型不匹配的当场抛异常类型碰巧兼容的静默执行把部门号当成乘数乘到工资上数据错了还不报错。后者比前者危险得多所以每次改 SQL 之后都要回头核对一遍占位符数量和序号。类型匹配同样严格。setInt对应整数列setDouble/setBigDecimal对应小数和金额列。往 DECIMAL 列里塞 double 会有精度问题金额类字段用BigDecimal更稳。executeUpdate()的返回值是受影响行数这个数字在后面的排查里非常有用——返回 0 说明 SQL 语法没问题只是没有行满足 WHERE 条件。3.4 可滚动可更新结果集任意行任意列怎么改任务 8 要求对结果集中任意行、任意列做修改靠默认的只进只读结果集做不到需要在创建语句对象时声明能力// 第二个参数控制游标移动方式第三个参数控制能否反写数据库 Statement stmt sample.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, // 可任意定位不感知他人并发修改 ResultSet.CONCUR_UPDATABLE); // 结果集可更新改动可回写 ResultSet rs stmt.executeQuery(select ID, NAME, SALARY from LAB.STAFF); rs.absolute(3); // 直接跳到第 3 行 rs.updateString(NAME, 新名字); // 先改结果集里的值不落库 rs.updateRow(); // 再回写这一行这一步才真正发 UPDATE三个参数各有含义。TYPE_SCROLL_INSENSITIVE允许absolute()、previous()、first()这类跳转但不保证看到别人已提交的修改换成TYPE_SCROLL_SENSITIVE能感知并发变化代价是需要驱动额外支持。CONCUR_UPDATABLE是能调用updateXxx和updateRow的前提缺了它这两步会抛异常。驱动能力是这套写法最大的变量。老版本驱动或者某些只读连接可能不支持可更新结果集创建时直接抛SQLFeatureNotSupportedException或者创建成功但updateRow时报错。稳妥做法是在实验环境里先跑一遍最小验证确认驱动支持再往业务代码里套。另外可更新结果集只适合单表、带主键的简单查询一旦 SELECT 里有 JOIN 或聚合回写就无从谈起这种场景还是老老实实写 UPDATE 语句。4. 事务与异常提交开关、溢出边界和错误码到这一层代码已经能跑通查询和更新了剩下的问题都出在“改完之后有没有真正落库”和“出错之后能不能说清楚错在哪”。实验里最耗时的几个问题恰好都集中在这里改了不生效、某些输入溢出、异常信息看不懂。这三件事看着互不相关其实共享同一套排查思路——先确认状态再看数据最后看错误码。4.1 setAutoCommit(false) 之后到底发生了什么JDBC 连接默认是自动提交模式每条 SQL 执行完立刻生效没有“撤销”的余地。要控制事务得先关掉它Connection sample DriverManager.getConnection( jdbc:db2:sample, db2admin, db2admin); sample.setAutoCommit(false); // 关闭自动提交开启事务边界 try { PreparedStatement pstmt sample.prepareStatement( update LAB.staff set salary salary * ? where dept ?); pstmt.setDouble(1, 1.01); pstmt.setInt(2, mydeptno); int n pstmt.executeUpdate(); sample.commit(); // 显式提交这一步之前数据对外不可见 System.out.println(影响行数 n); } catch (SQLException e) { sample.rollback(); // 出错必须回滚否则连接被挂起 throw e; } finally { sample.setAutoCommit(true); // 恢复默认方便连接复用 }关掉自动提交之后executeUpdate只是把修改写进当前事务既不落盘也不对别的连接可见。必须显式commit()才算数。异常路径里的rollback()不能省一是避免脏数据留在事务里二是避免连接被事务状态挂住导致后续操作异常。finally里恢复自动提交是给连接池场景留的习惯实验里连接用完就关这一步属于可选项但养成习惯不亏。4.2 “执行成功但库里没变”的四步排查实验里出现过一段很典型的描述输入 20 或 30程序没报错回去查数据库发现 salary 一点没变同样在数据库客户端里手动执行 UPDATE 却成功。这基本可以断定数据库本身没问题问题在 Java 程序这一侧。按下面顺序排查基本四步能定位看提交状态。是否setAutoCommit(false)之后漏了commit()这是最常见的一种。程序退出时未提交的事务会被回滚表现就是“跑了但没变”。看影响行数。executeUpdate返回 0 说明 WHERE 条件没匹配到任何行。实验里输入 16 反馈“修改了 0 行”查库确认该部门确实没有记录这就解释清楚了不是程序错。看连接目标。程序连的数据库、模式是否和查询时用的是同一个。两个环境各有一份同名表时改的和看的不是一张表。看隔离级别。事务已提交但另一个连接看不到通常是对方处在可重复读的快照里重新连接或结束事务后再查即可。这四步里前两步能覆盖绝大多数情况。养成“每次更新后打印影响行数”的习惯等于给事务加了一个廉价的观测点。4.3 溢出不是玄学先算边界再做乘法实验里那段关于溢出的验证过程写得很扎实值得完整复盘一遍。现象是把工资乘以 2 时输入 15 和 20 都报类型转换溢出改成乘以 1.01 之后两个输入都不溢出。这个对照实验直接证明了溢出来自“放大后的数值超出列的表示范围”而不是输入值本身有问题。DECIMAL(p,s) 类型的列能表示的最大值是 p 位有效数字、其中 s 位小数整数部分只剩 p−s 位。乘以 2 相当于整数位进一位原本贴着上限的记录就会越界。数据库抛出的通常是 SQLSTATE 22003数值超出范围部分版本伴随 SQLCODE -802 或 -304。排查方法很朴素先用 SQL 把边界量出来再算乘数上限。-- 查当前列的实际取值范围判断还能放大多少 SELECT MIN(SALARY), MAX(SALARY), COUNT(*) FROM LAB.STAFF; -- 查列定义确认精度和小数位 SELECT COLNAME, TYPENAME, LENGTH, SCALE FROM SYSCAT.COLUMNS WHERE TABSCHEMA LAB AND TABNAME STAFF AND COLNAME SALARY;拿到上限之后用上限 × 乘数 ≤ 列最大值反推乘数能取多大。实验最后选 1.01本质是让放大后的值仍落在范围内。要注意的是这个结论只对当前数据集成立换一批数据、换一个乘数都要重新算不能把 1.01 当成通用安全值。4.4 SQLException 三件套与 SQLWarning 链异常处理最容易写成e.printStackTrace()了事信息量大打折扣。SQLException自带三个取值方法各管一段信息方法含义排查时的用途getErrorCode()数据库厂商自己的错误码定位具体是哪个数据库的哪条规则被违反getSQLState()五位标准状态码跨数据库通用适合做错误分类getMessage()文本描述给人看作为前两者的补充标准状态码里最常遇到的几个22003数值超出范围、23505唯一约束冲突、23502非空约束、22018字符值转换错误、42601语句语法错误。任务 7 要求把违反约束、类型不匹配、溢出、非法符号四类错误都用对话框提示按这几个状态码分流刚好能一一对上。告警和异常是两条线。SQLWarning不会中断执行挂在语句对象或连接对象上需要主动去取而且是一个链表SQLWarning warn stmt.getWarnings(); while (warn ! null) { System.out.println(告警 warn.getMessage() 状态码 warn.getSQLState()); warn warn.getNextWarning(); // 沿着链往下走直到 null }很多驱动会在执行成功后挂一条“数据被截断”之类的告警程序不取就完全看不到。实验里if ((SQLWarn stmt.getWarnings()) ! null)这种写法只取了第一条链上还有几条就漏了严格说不完整。5. 易漏点清单五个最容易在实验里翻车的地方下面这五条都是从实际报错里反推出来的共同特征是“代码看起来没问题、报错信息又不说人话”。每条按“漏在哪、为什么会漏、怎么补”写复习时对照自己的代码过一遍比重新读一遍教材快。5.1 命名空间与连接管理类易漏点一SQL 里写裸表名。漏的原因是本地测试时恰好连的是对象所属的模式一切正常换台机器、换个账号就连环报“未定义”。补法是定一条硬规矩程序里的表名一律带模式前缀或者连接建立后立刻执行set schema二选一不许混用。易漏点二报“已存在”就换名字。漏的原因是默认占用名字的一定是表实际上视图、别名都算。补法是先查SYSCAT.TABLES的TYPE列确认占用者的类型再决定是删还是改名换名字的代价是引用它的代码、脚本全要同步改远比删掉一个别名麻烦。易漏点三连接和结果集不关。漏的原因是实验里跑几次就退出泄漏看不出来一旦改成循环执行或者接进界面程序很快就卡死。补法是按“后开先关”的顺序释放ResultSet→Statement→Connection或者直接用 try-with-resources 让编译器托管。5.2 事务与异常处理类易漏点四setAutoCommit(false)之后忘记收尾。漏的原因是程序不报错只是数据没变很容易被误判成 SQL 写错。补法是把“提交或回滚”和“关闭连接”绑在同一个 finally 块里成对出现写完就检查这两句在不在。易漏点五只捕获异常不取状态码。漏的原因是printStackTrace也能看到一行红字感觉够用了但真正做错误分类、给用户弹不同对话框时没有getSQLState()就无从分支。补法是在 catch 块里固定打印状态码和厂商码写成一个小的公共方法所有 catch 都调它。上面五条里前三条属于“环境认知”后两条属于“代码习惯”。环境认知类的错误换个数据库、换个版本还会再犯代码习惯类的错误改一次就能长期受益投入产出比更高。6. 复习清单把八个任务压缩成一张自测表复习这类实验最有效的方式不是重读代码而是把每个任务还原成“它想考哪个能力点”然后自问能不能不看代码说清楚。下面这张表是我自己整理时用的左列是能力点中列对应实验任务右列是一句话判据——能脱口而出判据这个点就算过了。能力点对应任务一句话判据驱动加载与连接建立任务 1、2能说出 URL 三段各是什么驱动类名写错会抛什么异常模式与全限定名任务 1、5能解释“未定义名称”报错里前缀是怎么来的对象命名空间冲突任务 5能说出表、视图、别名为什么不能重名授权语句任务 1能默写 GRANT 的完整语法并说明粒度语句对象选型任务 2、3能给出“有外部输入就用 PreparedStatement”的理由占位符编号任务 3能对着一条双参数 SQL 正确写出两个 set 语句结果集游标任务 2、8能说清 next() 之前游标在哪、absolute() 需要什么前提空值判定任务 6能说出 wasNull 必须紧跟取值调用的原因事务提交任务 3能列出“改了不生效”的四个排查方向数值边界任务 3能根据列精度反推乘数上限异常分类任务 7能说出 SQLState 和 ErrorCode 各管什么告警链任务 8能写出遍历 SQLWarning 的循环骨架自测时有个技巧把右列盖住只看左列口头讲一遍。讲不下去的地方就是薄弱点回去翻对应那一节比从头顺一遍效率高得多。第二遍再过的时候重点看上次卡住的三四个点其他的快速扫过。还有一件事值得单独做一遍把整个实验的错误记录按“报错原文 → 定位手段 → 根因 → 改法”四栏整理成一张表。这张表的价值在于报错原文是你下次遇到同样问题时唯一能搜到的关键词而根因和改法是你自己总结出来的、别人搜不到的东西。整理时不要只写“改好了”一定写清楚当时是怎么一步步排除的否则过两周自己都看不懂。我自己的教训是第一次做这套实验时遇到“执行成功但没生效”第一反应是去改 SQL来回折腾了快一个小时最后才发现是自动提交关掉之后没提交。从那以后我每次写带事务的代码都会在setAutoCommit(false)那一行后面紧跟一句注释写明“必须 commit”并且在测试时先打印影响行数再查库确认。这个习惯后来帮我省下的时间远超当时多写那两行注释的成本。希望这份梳理能帮到你也祝你的实验一次跑通。本文还有配套的精品资源点击获取