JDBC实战避坑指南:从连接查询到可更新结果集

发布时间:2026/10/27 11:45:56
JDBC实战避坑指南:从连接查询到可更新结果集 简介这份吉林大学《数据库应用程序开发》实验报告面向高校数据库课程学习者与Java JDBC初学者围绕DB2环境下Java程序与数据库的交互展开帮助读者理解从连接建立到事务控制的完整开发流程。压缩包内共1个doc文档约1.76MB以实验报告正文形式呈现包含各任务的调试过程、错误分析与运行结果截图说明。内容覆盖JDBC连接、Statement与PreparedStatement使用、ResultSet遍历、事务提交控制及SQL异常处理等核心知识点并记录了权限授予、对象名称不匹配、数值溢出与事务未提交等典型问题的排查思路。目前已有901人学习适合需要对照实验步骤复盘JDBC编程细节、查漏补缺的读者参考。1. 一份把 JDBC 从“能跑”逼到“敢改”的实验报告数据库应用程序开发这门课很多人学完 JDBC 只会背DriverManager.getConnection和rs.next()一到真实环境就翻车权限没授、表名带不带 schema、事务忘了提交、数值溢出报得莫名其妙。这份实验报告的价值不在于“八个任务全做完”而在于它把 JDBC 从连接、查询、更新一路推到 GUI 交互、子查询插入、空值显示、异常对话框和结果集任意行列修改每个任务都留了调试记录。适合正在做 JDBC 课程设计、需要一套可复现的 DB2 Java 操作模板的从业者和学生也适合想补齐事务与异常处理短板的开发者照着走一遍。2. JDBC 连接与查询从权限报错到 ResultSet 遍历2.1 先解决“连得上”之外的两个前置条件任务 1 和任务 2 表面是补代码实际卡人的是两个非代码问题。第一个是权限用db2admin连上库后查EMPLOYEE报没有访问权限原因不是 JDBC 写错而是对象属主是另一个用户。常见做法是切到对象属主用户执行授权而不是给db2admin加 DBA 权限了事-- 以对象属主身份登录后执行把查询权限授给应用连接用户 GRANT SELECT ON JLU.EMPLOYEE TO USER db2admin; -- 如果后续任务还要更新补上 UPDATE GRANT UPDATE ON JLU.STAFF TO USER db2admin;参数说明JLU是 schema 名EMPLOYEE/STAFF是表名USER db2admin指被授权账户。授权粒度按任务需要给别一上来就GRANT ALL后面排查权限问题时你会分不清是代码还是授权在捣乱。第二个是 schema 限定。命令行跑 Java 报DB2ADMIN.TEMPL未定义因为代码里写的是不带 schema 的TEMPL而当前连接用户的默认 schema 下没有这张表。解决办法是把 SQL 里的表名改成JLU.TEMPL。血泪经验DB2 里同名对象可能存在于多个 schemaJDBC URL 里不指定currentSchema时解析顺序依赖用户默认 schema跨用户环境必翻车。稳妥写法是在连接串里显式带上// 连接串追加 currentSchema避免依赖登录用户的默认 schema String url jdbc:db2://localhost:50000/sample;currentSchemaJLU;; Connection sample DriverManager.getConnection(url, db2admin, db2admin);2.2 查询骨架Statement 与 ResultSet 的正确打开方式任务 2 要求补全labstaff.java核心是五步建连接、建 Statement、执行查询、取结果集、逐行遍历。下面这段是可直接抄的骨架注释标了每步的作用import java.sql.*; public class LabStaff { public static void main(String[] args) { Connection sample null; Statement stmt null; ResultSet rs null; try { // 1. 加载 DB2 驱动老版本驱动类名固定 Class.forName(COM.ibm.db2.jdbc.app.DB2Driver); // 2. 建立连接用户名密码按实际环境替换 sample DriverManager.getConnection( jdbc:db2:sample;currentSchemaJLU, db2admin, db2admin); // 3. 创建 Statement 对象 stmt sample.createStatement(); // 4. 执行查询返回 ResultSet rs stmt.executeQuery( select NAME, JOB, SALARY from JLU.STAFF where ID 10); // 5. next() 首次调用把游标移到第一行返回 false 表示无数据 boolean more rs.next(); while (more) { System.out.println(rs.getString(NAME) | rs.getString(JOB) | rs.getDouble(SALARY)); more rs.next(); } } catch (ClassNotFoundException e) { System.out.println(驱动未找到 e.getMessage()); } catch (SQLException e) { System.out.println(SQLState e.getSQLState() Code e.getErrorCode() Msg e.getMessage()); } finally { // 关闭顺序与打开顺序相反先关 ResultSet 再关 Statement 再关连接 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (sample ! null) sample.close(); } catch (SQLException ignored) {} } } }逻辑说明rs.next()是游标前移第一次调用定位到第一行所以循环前先调一次再进while这是 JDBC 里最容易写错的地方——很多人直接while(rs.next())也能跑但任务里要求显式演示“先 next 再循环”的语义两种写法结果一致理解游标位置才是关键。参数上getString/getDouble按列类型取列名大小写不敏感但建议与 SQL 一致。异常分支里把SQLState和ErrorCode都打出来后面排查约束冲突和溢出全靠这两个值。3. 更新与事务PreparedStatement、溢出定位和提交开关3.1 手动事务下 executeUpdate 的完整链路任务 3 的labupdate.java是整份报告里信息密度最高的一段。它要求关自动提交、用 PreparedStatement 设参、执行更新、捕获 SQLException 取错误码。先看可复现的代码import java.sql.*; public class LabUpdate { public static void main(String[] args) { Connection sample null; PreparedStatement pstmt null; try { Class.forName(COM.ibm.db2.jdbc.app.DB2Driver); sample DriverManager.getConnection( jdbc:db2:sample;currentSchemaJLU, db2admin, db2admin); // 关闭自动提交后续必须显式 commit 才落库 sample.setAutoCommit(false); String sqlstmt update JLU.STAFF set SALARY SALARY * ? where DEPT ?; pstmt sample.prepareStatement(sqlstmt); // 参数 1 是放大系数参数 2 是部门号 pstmt.setDouble(1, 1.01); pstmt.setInt(2, 20); int updateCount pstmt.executeUpdate(); System.out.println(影响行数 updateCount); // 手动事务必须提交否则连接关闭时回滚 sample.commit(); } catch (SQLException e) { System.out.println(SQLState e.getSQLState() Code e.getErrorCode() Msg e.getMessage()); try { if (sample ! null) sample.rollback(); } catch (SQLException ignored) {} } finally { try { if (pstmt ! null) pstmt.close(); } catch (SQLException ignored) {} try { if (sample ! null) sample.close(); } catch (SQLException ignored) {} } } }参数说明setDouble(1, 1.01)对应第一个?setInt(2, 20)对应第二个?占位符编号从 1 开始顺序不能错。executeUpdate返回受影响行数返回 0 不代表出错可能只是没有匹配记录——报告里输入 16 反馈修改 0 行就是库里确实没有该部门记录这一点很多人会误判成程序 bug。3.2 溢出与“改了没生效”的两类根因报告里那段排查过程值得单独拎出来。输入 20 报类型转换溢出怀疑是 salary 放大后超范围把系数从 2 改成 1.01 后 15 和 20 都不溢出反证了是数值过大而非代码逻辑错。这类问题的定位方法是先用 DB2 命令行直接跑同样的 update确认数据库层能不能改能改就说明问题在 Java 侧。常见做法是查目标列的定义精度-- 查看 SALARY 列的类型和精度判断放大后是否越界 SELECT COLNAME, TYPENAME, LENGTH, SCALE FROM SYSCAT.COLUMNS WHERE TABSCHEMA JLU AND TABNAME STAFF AND COLNAME SALARY;如果TYPENAME是DECIMAL且精度有限放大系数一大就会溢出解决要么降系数要么改列精度要么在 Java 侧先做范围校验再更新。另一类更隐蔽输入 20 或 30 执行成功但数据库没变化。原因在setAutoCommit(false)之后没有commit()连接关闭时事务回滚。报告里把 false 改 true 能生效本质就是恢复了自动提交。我的建议是手动事务下把commit()放在 try 块末尾、rollback()放在 catch 里形成固定套路别依赖连接关闭时的隐式行为。4. GUI 化与插入操作JOptionPane、子查询插入和空值显示4.1 用 JOptionPane 把输入输出搬出控制台任务 4 要求把labupdate.java改成 GUI输入和结果都用JOptionPane。这不是为了好看而是练“输入解析 异常兜底”的组合。核心写法import javax.swing.JOptionPane; // 读取部门号取消或非数字都要有兜底 String input JOptionPane.showInputDialog(null, 请输入部门号); if (input null) return; // 用户点了取消 int deptno; try { deptno Integer.parseInt(input.trim()); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, 部门号必须是整数, 输入错误, JOptionPane.ERROR_MESSAGE); return; } // 执行更新后把影响行数回显 JOptionPane.showMessageDialog(null, 影响行数 updateCount);参数说明showInputDialog返回null表示取消必须判空否则parseInt(null)直接 NPEtrim()防用户多敲空格showMessageDialog的第四个参数是消息类型错误用ERROR_MESSAGE普通提示用INFORMATION_MESSAGE。GUI 化之后最容易漏的是把SQLException也弹窗别只printStackTrace用户看不到控制台。4.2 单行、多行、子查询三种插入的落地差异任务 5 要求向TEMPL插入行支持单行、多行、子查询插入。这里有个前置坑报告里无法创建JLU.TEMPL原因是之前给EMPLOYEE建过别名TEMPL删掉别名才能建表。建表前先确认对象名没被占用-- 查别名和表是否重名 SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA JLU AND TABNAME TEMPL; -- 若存在别名先删除 DROP ALIAS JLU.TEMPL;三种插入的 SQL 形态不同单行用INSERT INTO ... VALUES (...)多行用批量addBatch/executeBatch子查询插入用INSERT INTO ... SELECT ... FROM ...。批量插入的写法// 批量插入减少往返次数 String sql insert into JLU.TEMPL (ID, NAME, SALARY) values (?, ?, ?); PreparedStatement ps sample.prepareStatement(sql); for (Object[] row : rows) { ps.setInt(1, (Integer) row[0]); ps.setString(2, (String) row[1]); ps.setDouble(3, (Double) row[2]); ps.addBatch(); } int[] counts ps.executeBatch();参数说明addBatch只入队不执行executeBatch返回每条的受影响行数数组某条失败时数组里对应位置是Statement.EXECUTE_FAILED。子查询插入则把VALUES换成SELECT注意列数和类型对齐否则报类型不匹配。4.3 空值显示与异常对话框的收口任务 6 要求结果集里为空的列统一显示“空”。rs.getString对 NULL 返回null直接拼字符串会打出null正确做法是取完判空String name rs.getString(NAME); String display (name null) ? 空 : name;任务 7 要求把违反约束、类型不匹配、溢出、非法符号都弹对话框。统一在catch (SQLException e)里按SQLState分类提示比每个任务各写一套更稳。常见做法是维护一个SQLState到中文提示的映射未知状态回退到e.getMessage()至少让用户看到原始信息。5. 避坑与排查五个真实翻车点5.1 权限报错却去改代码现象查询报没有访问权限。原因对象属主与连接用户不一致授权没做。解决以属主身份GRANT SELECT/UPDATE别急着改 SQL。5.2 表名不带 schema 报未定义现象DB2ADMIN.TEMPL未定义。原因默认 schema 下无此表。解决SQL 里写全JLU.TEMPL或在连接串加currentSchemaJLU。5.3 更新成功但数据没变现象executeUpdate返回正数库里没变化。原因setAutoCommit(false)后未commit。解决try 末尾显式commit()catch 里rollback()。5.4 溢出报错误判为代码 bug现象输入较大值报类型转换溢出。原因放大后超出列精度。解决查SYSCAT.COLUMNS确认精度降系数或改列定义别盲目改 Java 类型。5.5 建表失败因别名占用现象创建TEMPL报已存在。原因之前建过同名别名。解决DROP ALIAS后再建表建表前先查SYSCAT.TABLES。6. 结果集任意行列修改可更新 ResultSet 的边界与验证任务 8 要求对结果集任意行、任意列做修改这是整份报告里最接近真实业务的一步。常规Statement返回的ResultSet默认只读要改必须用可滚动可更新的结果集。写法上分两种一是createStatement时指定类型和并发模式二是用PreparedStatement配合TYPE_SCROLL_INSENSITIVE和CONCUR_UPDATABLE。后者更常用// 可滚动、可更新的结果集注意查询必须带主键列 PreparedStatement stmt sample.prepareStatement( select ID, NAME, SALARY from JLU.STAFF where DEPT ?, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); stmt.setInt(1, mydeptno); ResultSet rs stmt.executeQuery(); // 定位到任意行后改任意列 if (rs.absolute(3)) { rs.updateDouble(SALARY, 8888.0); rs.updateRow(); // 不调 updateRow 不会落库 }参数说明TYPE_SCROLL_INSENSITIVE允许absolute/relative跳行CONCUR_UPDATABLE才允许updateXxx加updateRow。边界在于查询里必须包含能唯一定位行的列通常是主键否则驱动可能拒绝更新updateRow之后如果连接是手动事务仍要commit。验证方法是改完立刻用另一条查询回读别只看updateRow没抛异常就以为成功。另外报告里还要求处理SQLWarningstmt.getWarnings()返回非空时把警告链逐条打出来很多约束类提示只出现在 warning 里而不抛异常。从那以后我每次写可更新结果集都强制先确认查询带主键、再确认事务提交路径两步缺一个就会出现“改了没生效”的黑匣子。希望帮到你。本文还有配套的精品资源点击获取