简介这份资源是吉林大学《数据库应用程序开发》课程的实验报告文档面向学习数据库编程、需要完成JDBC实验或准备课程提交的高校学生。内容围绕Java与DB2的交互展开记录Tasks 1至8的完整调试过程涵盖权限授予与GRANT语句、表别名导致的未定义名称、DriverManager建立连接、Statement与ResultSet遍历、PreparedStatement参数设置、setAutoCommit事务提交开关、SQL异常错误码与状态捕获以及salary放大倍数引发的类型转换溢出分析和GUI版JOptionPane改造、单行多行与子查询插入等实践环节。资源包共1个doc文件大小约1.76MB按任务分节列出典型错误、解决办法与运行结果截图可直接作为实验报告的写作模板与排错对照手册。已有903人学习适合希望少走弯路、快速理清JDBC操作要点与事务细节的读者参考。1. 一张地图看懂 JDBC 实验七个任务其实是一条链路这套数据库应用程序开发实验报告表面上是七八个互不相干的任务拼在一起梳理下来会发现它其实是一条完整链路先解决“连不上”再解决“取不到”然后解决“改不动”最后解决“出错说不清”。链条上的每一环恰好对应 JDBC 编程里最容易被跳过的那部分知识驱动加载与连接串、Statement 与 PreparedStatement 的分工、结果集游标与空值判断、事务提交时机、异常分类与 GUI 封装。它适合已经写过两行 Java 但一碰数据库就卡住的人也适合考前想把“连接—执行—结果—事务—异常”这条线一次性打通的人。把这条链路看成地图比逐个任务背代码有效得多——因为考试和实际开发里坑永远出在环节之间的衔接处而不是单个 API 的拼写上。2. 连接与权限从 DriverManager 到 schema 归属2.1 一次连接要过的四道关驱动、URL、账号、schemaJDBC 连接从来不是一行getConnection就完事的事它要同时过四道关驱动类能不能加载、URL 指不指向正确的库、账号密码能不能通过认证、SQL 里的对象名能不能在当前 schema 下解析出来。这四道关任何一道没过报出来的错看起来都像“程序写错了”实际上八成跟代码无关。实验里最典型的一次排查就是程序里查TEMPL一直报“未定义的名称”改了半天 Java 代码毫无进展最后发现是对象根本不在当前用户的默认 schema 下把表名写成带前缀的全限定名就通了。这个教训的价值在于报错信息里的“名称未定义”先怀疑解析上下文再怀疑拼写。// 固定骨架加载驱动 → 建立连接 → 创建语句对象 Class.forName(DRIVER_CLASS); // 驱动类全名缺失会抛 ClassNotFoundException Connection conn DriverManager.getConnection(url, user, password); // 库名/账号/密码三项都要对 conn.setAutoCommit(false); // 提前关自动提交给后续批量更新留出回滚余地上面三段代码里DRIVER_CLASS要填数据库厂商提供的驱动类全名写成常量便于换库时只改一处url的形态通常是jdbc:子协议://主机:端口/库名子协议写错和库名写错报的是完全不同的异常别混为一谈setAutoCommit(false)放的位置很关键它必须在任何 DML 之前执行放到executeUpdate后面就等于没设。连接对象用完必须关闭顺序是 ResultSet → Statement → Connection倒着关否则连接池很快会被耗光本地跑十几次就卡住很多人第一反应是“数据库挂了”其实是自己漏了 close。连接阶段的失败用一张对照表定位最快报错现象大概率原因该做的验证ClassNotFoundException驱动包没进 classpath检查构建路径或运行时的-cp参数认证失败 / 拒绝连接账号密码错、库名错、实例没起用同一组凭据在命令行工具里连一次表或视图不存在对象不在默认 schema 下查系统表确认对象的真实归属第一次查询耗时异常长驱动初始化 首次语句编译预热一次再计时别当性能问题处理提示连接串里的库名和 SQL 里写的 schema 是两个概念前者决定“连到哪个库”后者决定“在库里找谁”。这两件事混淆是初学者最常见的定位失误。2.2 权限与对象命名为什么会出现“未定义的名称”权限问题在实验里出现得非常早用管理员账号登录后查某张业务表直接报没有访问权限。解决方式不是改代码而是回到数据库侧用对象的拥有者身份把权限授出去。这一步考察的其实是“数据库权限模型”这一块知识——谁能授权、授给谁、授哪些操作。GRANT语句的四个要素必须写全操作类型、对象全名、被授权者、以及可选的WITH GRANT OPTION。少写对象前缀是最常见的失误因为GRANT SELECT ON EMPLOYEE在某些环境下会按当前用户的默认 schema 解析授出去的可能是另一张表甚至直接报对象不存在。-- 以对象拥有者身份执行把四类 DML 权限授给应用账号 GRANT SELECT, INSERT, UPDATE, DELETE ON APP.EMPLOYEE TO APP_USER; -- 确认对象究竟挂在哪个 schema 下避免“名称未定义”瞎猜 SELECT TABSCHEMA, TABNAME FROM SYSCAT.TABLES WHERE TABNAME TEMPL;第一句的关键在于对象名要带 schema 前缀把它写成APP.EMPLOYEE而不是裸的EMPLOYEE授权才是确定的第二句查系统目录表把同名对象的所有归属列出来一眼就能看出代码里该写哪个前缀。很多“未定义名称”的报错用第二句一查就真相大白——对象存在只是不在你以为的地方。比权限更隐蔽的是别名冲突。实验里出现过“无法创建表”的情况原因是之前为了图省事给另一张表建过同名的别名alias别名和表共享同一个命名空间先占位的别名会让后建的实名表直接失败。处理办法是把旧别名删掉再建表-- 先清掉占位的别名再创建实名表 DROP ALIAS APP.TEMPL; CREATE TABLE APP.TEMPL ( ID INTEGER NOT NULL PRIMARY KEY, NAME VARCHAR(30), SALARY DECIMAL(9,2) );这里的DECIMAL(9,2)值得单独记一笔它表示总共 9 位有效数字、其中 2 位是小数整数部分只剩 7 位最大约 9999999.99。这个精度定义在后面“更新时溢出”的实验里是决定性因素——溢出不是 Java 的int装不下而是数据库列的精度装不下。凡是涉及金额的列写建表语句时就要把精度算清楚事后改列类型要迁移数据成本高得多。3. 执行链路Statement、PreparedStatement 与结果集遍历3.1 三种执行入口的分工JDBC 里执行 SQL 的入口就那么几个但选哪个直接决定了代码的安全性和性能。裸Statement适合执行完全固定、不带任何外部输入的语句比如建表、建索引PreparedStatement适合带参数、需要重复执行的语句它预编译一次、绑定多次既避开字符串拼接带来的注入风险也省掉重复解析的开销CallableStatement面向存储过程实验里没涉及但概念上要知道它是对前两者的扩展。入口典型用法返回值主要风险Statement.executeQuery固定查询ResultSet拼接参数易出注入与引号错误Statement.executeUpdate固定 DML/DDL受影响行数同上且无法复用执行计划PreparedStatement带参数、循环执行ResultSet或行数参数下标从 1 开始易错位CallableStatement调用存储过程混合出参注册顺序易漏参数下标从 1 开始这一条几乎每个人都栽过一次。下面这段是更新任务的骨架注意setInt(1, ...)里的 1 指的是第一个问号不是第 0 个String sql UPDATE APP.STAFF SET SALARY SALARY * ? WHERE DEPT ?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setBigDecimal(1, new BigDecimal(1.01)); // 第 1 个问号放大系数 pstmt.setInt(2, deptNo); // 第 2 个问号部门号 int updateCount pstmt.executeUpdate(); // 返回真正被改动的行数executeUpdate的返回值是“实际受影响的行数”不是“SQL 有没有执行成功”。这个区别在做实验时会制造巨大的困惑输入一个数据库里根本不存在的部门号程序不报错返回值是 0很多人以为更新成功了回头查表发现毫无变化。返回 0 就代表没有一行匹配它是结果信息不是异常。想确认某次更新到底动了哪些行要么先用同样的 WHERE 条件查一遍要么把返回值打到界面上盯着看。3.2 结果集遍历与游标位置ResultSet是 JDBC 里最容易“看着会、写起来错”的部分核心只有一个概念游标。游标初始位置在第一行之前每调用一次next()才向前挪一行同时返回“挪过去之后还有没有数据”。所以第一次next()返回true说明结果集至少有一行此时可以直接getXxx取值返回false说明结果集是空的这时候再去取值就会报“游标未定位”之类的错误。ResultSet rs pstmt.executeQuery(); boolean more rs.next(); // 首次调用游标移到第一行并告知是否有数据 while (more) { int id rs.getInt(ID); // 按列名取值比按下标更抗表结构变动 String name rs.getString(NAME); if (rs.wasNull()) { // 必须紧跟在上一次 getXxx 之后调用 name 空; } System.out.println(id \t name); more rs.next(); // 推进到下一行循环条件由它驱动 }这段代码里有两处值得反复看。第一处是getInt与wasNull的配对数据库里的 NULL 映射到 Java 的int会变成 0你无法区分“这一列真的是 0”和“这一列是 NULL”所以必须用wasNull()补一刀而它判断的是最近一次 getXxx 读到的值中间夹了别的取值调用就失效了。第二处是循环写法把next()的返回值存进变量再判断比while (rs.next())多一行但能让“首次定位”和“后续推进”这两件事在代码里显式分开读起来不容易误解游标状态。按列名取值和按下标取值各有取舍。列名可读性好、加列不影响但大小写敏感度取决于驱动实现写NAME和name在部分驱动上行为不同下标从 1 开始、性能略好但一旦 SELECT 列表顺序调整整段取值代码全错位。实验阶段建议统一按列名取遇到大小写问题时把 SELECT 里的别名显式写出来比如SELECT NAME AS NAME用别名把大小写固定住。3.3 事务开关 setAutoCommit 的隐性影响实验里最折磨人的一个现象是程序提示更新成功返回值大于 0去数据库里一查数据纹丝不动。这不是玄学是事务没提交。默认情况下连接是自动提交的每条 DML 执行完立刻落库一旦代码里调用了setAutoCommit(false)之后所有改动都挂在一个未提交的事务里不commit()就永远是“看起来改了”。conn.setAutoCommit(false); // 关闭自动提交进入手动事务模式 try { int n pstmt.executeUpdate(); if (n 0) { conn.rollback(); // 没有匹配行主动回滚避免悬挂事务 return; } conn.commit(); // 确认改动落库 } catch (SQLException e) { conn.rollback(); // 任何异常都回到事务起点 throw e; }这段骨架的重点不在语法而在“每条路径都有归宿”正常提交、零行回滚、异常回滚三条路都写死了才不会留下一个既不提交也不回滚的连接。实验里出现的“改了不生效”排查顺序应该是这样的——先看setAutoCommit是不是false再看有没有对应的commit()然后确认连的是不是同一个库同一个 schema最后才怀疑 SQL 本身。把顺序倒过来查会在错误的方向上耗掉大量时间。顺带说清楚另一个高频误解更新不生效和更新溢出是两回事。溢出的表现是抛异常根本走不到提交那一步不生效的表现是不抛异常、返回值正常、数据没动。两种现象长得完全不像但如果只看“界面显示成功没成功”很容易把前者当成后者或者把后者当成前者这就是为什么异常处理和事务控制必须放在一起设计。4. GUI 化与插入三态单行、多行、子查询4.1 JOptionPane 的输入输出拆解把命令行程序改成图形界面实验要求的做法是用JOptionPane的静态方法接管输入输出showInputDialog负责要参数showMessageDialog负责报结果。这两个 API 用起来只有几行但有两个必须处理的边界——用户点“取消”时返回null用户输入非数字时parseInt抛NumberFormatException。这两类错误属于“输入校验”和数据库异常完全是两个层次混在一个 catch 里处理用户会看到一堆看不懂的技术信息。String input JOptionPane.showInputDialog(null, 请输入部门号); if (input null) { // 用户点了取消 return; } int deptNo; try { deptNo Integer.parseInt(input.trim()); // 去掉首尾空格避免 15这种输入 } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, 部门号必须是整数); return; }这段代码体现的分层思路值得记住界面层只做“取值 格式校验”校验通过再把干净的参数交给业务层业务层碰到的才是真正的 SQLException。如果界面上直接 catch SQLException 并弹原始错误文本用户看到的是一长串驱动内部信息既不友好也不利于排查。4.2 三种插入写法的差别插入任务的三种形态——单行插入、多行插入、通过子查询插入——表面上是三个功能本质是三种数据来源来自一条手工输入、来自一批手工输入、来自库里另一张表。搞清来源写法自然就定了。// 形态一单行插入一条语句一次绑定 String sql1 INSERT INTO APP.TEMPL (ID, NAME, SALARY) VALUES (?, ?, ?); // 形态二多行插入同一模板反复绑定攒批提交 for (Record r : records) { pstmt.setInt(1, r.id); pstmt.setString(2, r.name); pstmt.setBigDecimal(3, r.salary); pstmt.addBatch(); // 只入批不执行 } int[] counts pstmt.executeBatch(); // 一次性提交整批返回每条的受影响行数 // 形态三子查询插入数据来源是库内另一张表一次搞定 String sql3 INSERT INTO APP.TEMPL (ID, NAME) SELECT ID, NAME FROM APP.STAFF WHERE DEPT ?;形态二的addBatch/executeBatch组合在数据量大时优势明显网络往返从 N 次降到 1 次。但要注意executeBatch返回的是数组长度和批大小对应某一条失败时整批的行为取决于驱动有的驱动抛异常就中断有的会继续执行后面的。实验阶段一行一行插更稳妥量大了再上批处理别一上来就批处理然后被难懂的报错劝退。形态三的难点不在语法在于列要一一对应INSERT后面的列清单和SELECT后面的列清单必须数量一致、类型兼容顺序对不上不一定报错但插进去的数据会错位这种错误最难查因为程序一路绿灯。写完这句一定要顺手SELECT一遍新表确认。4.3 空值显示与异常分类提示结果集里的 NULL 在界面上直接拼字符串会显示成null或者空白实验要求统一显示为“空”。这件事的正确做法不是把null替换成字符串了事而是在取值时用wasNull()判断然后赋一个展示用的占位文本。写入方向用setNull(index, java.sql.Types.VARCHAR)读出来再按同一套规则展示读写两头的口径才一致。异常处理这块实验要求把“违反约束、类型不匹配、溢出、非法符号”四类错误都用对话框提示出来。这四类错误在SQLException体系里有明确的对应关系按getSQLState()的前两位分支是最稳的做法错误类别SQLState 前缀常见场景提示语建议违反完整性约束23主键重复、非空列给 NULL、外键悬空数据不符合约束条件类型不匹配22往数值列塞字符串、日期格式错字段类型与输入不符数值溢出22003放大后的值超出列精度数值超出字段允许范围语法或非法符号42601SQL 拼错、符号放错位置语句有语法错误权限或连接问题08 / 42501没授权、连接断了无权限或连接已断开注意不同驱动抛出的具体异常子类不完全一样但 SQLState 前两位是相对稳定的。分支判断用state.startsWith(23)这种写法比instanceof更扛驱动差异也比直接解析错误文本可靠得多。除了SQLException实验里还要求处理SQLWarning。它是“非致命提醒”不会中断执行但能告诉你某次操作有隐含问题比如数据被截断。标准写法是在语句执行后主动取一次SQLWarning warn pstmt.getWarnings(); while (warn ! null) { // 警告可能是一条链要循环取完 System.out.println(warn.getSQLState() warn.getMessage()); warn warn.getNextWarning(); }getWarnings取完一次就清空所以要在执行完语句后立刻取中间插了别的操作就取不到了。这个 API 在实际项目里用得不多但在教学实验里它是“完整的错误处理”这一环知道它存在、知道它是一次性消费的就够用了。5. 易错点这五处翻车率最高5.1 易漏点把“未定义名称”当成拼写错误为什么漏报错文本里带着对象名第一反应总是去核对代码里那几个字母有没有敲错而实际上代码里写的名字往往一字不差错的是“解析上下文”——对象挂在 A schema连接用的是 B 用户的默认 schema名字再对也找不到。这类错误在换环境后集中爆发本地跑得好好的换台机器就报错因为两台机器的默认 schema 不一样。怎么补遇到“表或视图不存在 / 名称未定义”先不碰代码直接连上去查系统目录表把同名对象的所有TABSCHEMA列出来确认归属后把所有 SQL 里的裸表名统一改成SCHEMA.TABLE的全限定写法。养成“只要跨 schema就写全名”的习惯这一类错误基本可以清零。5.2 易错点把溢出当成程序 bug把零行当成执行失败为什么漏这两个错误的表象都很“像程序出问题”。溢出的时候界面弹出一段英文直觉是代码写错了零行更新的时候返回值是 0直觉是“没成功”于是反复改 SQL 语法。实验里有一个很漂亮的验证过程把放大系数从 2 改成 1.01同一个部门号就不溢出了说明根因是数值大小而不是代码逻辑——这种“改一个变量看现象是否变化”的对照法比读十遍文档有用。怎么补遇到溢出先算列的精度上限。DECIMAL(9,2)的整数部分只有 7 位原值乘 2 之后很容易越界乘 1.01 就安全得多。遇到零行先查 WHERE 条件在库里到底能查出几行查询结果为空就说明条件本身不匹配和 UPDATE 语句无关。把“数据问题”和“代码问题”分开验能省掉大量无效调试。5.3 易漏点事务没提交成功提示是假的为什么漏executeUpdate返回了正数程序没抛异常界面显示“更新成功”三条证据都指向“已经改好了”唯一没验证的是“数据库里到底变了没”。手动事务模式下改动只在当前连接的事务里可见commit()之前别的会话查不到连接一断就全丢。怎么补排查顺序固定成三步——看setAutoCommit是不是false、看有没有配对的commit()、看 catch 分支里是不是悄悄rollback()了。更进一步的做法是把“执行—提交—回滚”写成一个不可分割的小代码块让提交和回滚在视觉上永远挨着不给自己留忘记的机会。5.4 易错点想改结果集里的任意行却用了只读结果集为什么漏ResultSet默认是只读、只能向前滚动的这是很多人没注意到的默认值。代码里写了rs.updateString(...)然后rs.updateRow()运行时报“结果集不可更新”看着像方法名写错了其实是创建语句对象时没声明并发模式。怎么补要支持“任意行任意列修改”创建语句时必须显式指定两个参数——可滚动、可更新Statement stmt conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, // 游标可前后移动 ResultSet.CONCUR_UPDATABLE // 结果集可回写数据库 ); ResultSet rs stmt.executeQuery(SELECT ID, NAME, SALARY FROM APP.STAFF); rs.absolute(3); // 直接跳到第 3 行 rs.updateBigDecimal(SALARY, new BigDecimal(8888.00)); rs.updateRow(); // 没有这一句前面的 update 全是空转可滚动结果集给了absolute、first、last、previous这些定位方法改完必须调updateRow()才真正写回只调updateXxx相当于在内存里改了值但没提交。另外这类结果集对 SQL 有要求查询里带聚合、DISTINCT、多表连接时往往退化回只读写之前先用最简单的单表查询验证一遍。5.5 易漏点用 0 判断空值用列下标写死取值为什么漏getInt返回intNULL 到了 Java 侧就变成 0这个转换是隐式的、不报错的所以没人会主动去查“这一列原本是不是空”。列下标写死的问题同理写的时候表结构是那样后来 SELECT 加了一列代码不报错但取出来的全错位了。怎么补所有可能为空的列取值后立刻跟一句wasNull()判断中间不插入任何其他取值调用能按列名取就别按下标实在要按下标把下标抽成常量并在 SELECT 语句旁边写注释标好对应关系。这两条写进自己的检查清单比事后调试便宜太多。6. 复习清单从能跑通到能讲清楚复习这套内容最容易掉进的陷阱是“代码能跑就算会了”。但实验报告里真正值钱的部分恰恰是每个任务下面那段“调试过程”——错误现象、怀疑方向、验证动作、最终根因。把这四步复盘一遍比再抄一遍代码有用得多。下面这份清单是按“能不能讲清楚”来设计的逐条过一遍讲不清楚的那条就是漏洞。模块自检问题讲不清就回去看连接驱动类、URL、账号、schema 四者各管什么报错怎么区分2.1 节的四道关与失败对照表权限GRANT的四个要素是什么为什么对象名要带前缀2.2 节的授权语句与别名冲突执行Statement和PreparedStatement什么时候各用哪个参数下标从几开始3.1 节的入口对照表结果集游标初始在哪next()返回值含义wasNull必须紧跟谁3.2 节的遍历骨架事务更新成功但没生效按什么顺序排查3.3 节的三步排查法插入单行、批量、子查询三种写法各自的数据来源是什么4.2 节的三段代码异常约束、类型、溢出、语法四类错误分别对应哪个 SQLState 前缀4.3 节的映射表可更新结果集要改任意行创建语句时必须指定哪两个参数5.4 节的两参数写法过完表格再做一件更有用的事把整条链路用一段最小可运行代码串起来不看笔记手写一遍。写的时候刻意把“容易漏的那几句”放在显眼位置——Class.forName、setAutoCommit(false)、next()、wasNull()、commit()、updateRow()。这六句是全篇出现频率最高的失误点能默写出来说明链路真的进脑子了。Class.forName(DRIVER_CLASS); // 驱动 try (Connection conn DriverManager.getConnection(url, user, pwd)) { // 连接自动关闭 conn.setAutoCommit(false); // 事务开关DML 之前 try (PreparedStatement ps conn.prepareStatement(SQL)) { // 预编译 ps.setInt(1, deptNo); // 参数从 1 开始 if (ps.executeUpdate() 0) { // 零行不是异常 conn.rollback(); return; } conn.commit(); // 提交别忘了 } catch (SQLException e) { conn.rollback(); // 异常回滚 throw e; } }最后一条经验来自我自己踩过的坑早些年做这类实验我总是先写代码再补文档结果每次报错都只能靠回忆猜当时改了什么来回折腾好几轮。后来改成先把“预期现象”写在一行注释里再动手写代码跑出来对不上就立刻能定位是环境问题还是逻辑问题。从那以后我每次碰数据库连接相关的代码都强制走一遍“连接—事务—执行—结果—异常”这五步自检哪怕只写十行也照走。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?