简介这份文档是吉林大学《数据库应用程序开发》课程的实验报告面向正在学习 Java 与 DB2 数据库交互的高校学生及自学者可用于对照完成 JDBC 编程实验、复盘调试过程或整理课程笔记。报告以八个任务为主线从补充教材程序片段、为 db2admin 授予 EMPLOYEE 表访问权限、修正对象名称到编写 Java 程序完成数据库连接、创建 Statement 对象、执行 SELECT 查询并遍历结果集再到借助 PreparedStatement 执行更新、关闭自动提交以手动控制事务、捕获错误码与状态信息处理异常并延伸至图形界面下的单行、多行与子查询插入以及结果集空值显示等练习。其中对数值放大后类型转换溢出的排查、自动提交关闭导致修改未生效的分析均给出可复用的排错思路。资源包仅含一个 doc 文件约 1.76MB篇幅完整、截图与代码片段齐备已有 903 人学习下载。1. 权限没给和名字写错是两条完全不同的报错数据库应用程序开发这门课的 JDBC 实验几乎所有人都卡在同一处程序能编译、能运行控制台直接打印出SQL0204N DB2ADMIN.TEMPL 是一个未定义的名称或者更早就卡在SQL0551N DB2ADMIN 没有对表 LAB.EMPLOYEE 执行操作 SELECT 的特权。这两条看着都像环境坏了实际一条是权限没授一条是 schema 限定名没写全跟数据库装得好不好没半点关系。最折腾人的地方在于实验报告里往往只写一句“授予权限”“改成 LAB.TEMPL”照抄还是报错因为中间那步“为什么名字会被补上 DB2ADMIN 前缀”没人讲。下面按我实际踩过的顺序把每条报错对应到具体命令、根因和验证方法。2. 报错「未定义的名称」schema 限定名和别名冲突是两码事这两类报错在控制台里长得挺像其实处理顺序完全相反。SQL0204N 的含义是“我按你给的名字去找了这个路径下没有这个对象”SQL0551N 的含义是“对象我找到了但你这个用户对这个动作没特权”。方向判错就会在授权和改代码之间来回打转半天过去还在原地。我的习惯是先把对象到底在哪确认清楚再决定改代码还是补授权因为权限报错本身已经把真实的 schema 名带出来了。2.1 先分清 SQL0204N 和 SQL0551N一个是找不到一个是没权限SQL0204N后面跟着的完整对象名是要重点看的。DB2 对不带 schema 前缀的对象名有个默认解析规则用当前连接的授权 ID 当作 schema。你用db2admin连上去代码里写SELECT * FROM TEMPLDB2 会自动解析成DB2ADMIN.TEMPL而表实际建在LAB这个 schema 下所以当然找不到。报错里那串DB2ADMIN.TEMPL不是数据库瞎编的是它把你省略的部分补齐后告诉你“我找的是这个”。SQL0551N反过来它已经把真实位置写明白了没有对表 LAB.EMPLOYEE 执行操作 SELECT 的特权。这句话里 schema 是LAB对象是EMPLOYEE动作是SELECT三个要素齐全。所以看到 42501 这类权限错误第一反应应该是去授权而不是去改代码里的表名。确认当前会话落在哪个 schema一条命令就够-- 看当前连接的 schema决定了不带前缀的名字会被补成什么 VALUES CURRENT SCHEMA; -- 看某个 schema 下到底有哪些对象TYPE 字段 T 是表、V 是视图、A 是别名 SELECT TABNAME, TYPE, OWNER FROM SYSCAT.TABLES WHERE TABSCHEMA LAB;第一句返回的如果是DB2ADMIN那就解释了为什么TEMPL会被解析成DB2ADMIN.TEMPL。第二句能一次性告诉你LAB下有没有EMPLOYEE、有没有TEMPL以及它们到底是表还是别名。这两条查完SQL0204N 基本就破案了剩下的只是决定是改代码写全限定名还是SET CURRENT SCHEMA LAB把默认路径挪过去。我的排查顺序是这样的先跑上面两条查对象确认对象在哪个 schema、是什么类型再拿报错里的对象名和查出来的结果对一遍对不上就是 SQL0204N对得上但动作被拒就是 SQL0551N。顺序反过来很容易在权限上折腾半天最后发现压根是名字错了。2.2 GRANT 之后还要改代码授权不改变名称解析规则授权语句本身不复杂坑在于很多人授完权就直接跑程序然后发现报错从 42501 变成了 42704以为授权没生效其实是名称解析那一步没过。DB2 里给另一个用户授表权限的写法-- 用有权限的账号连上目标库先把对象权限授出去 CONNECT TO sample USER labadmin USING labadmin; -- 查询、增删改一起授避免后面做插入和更新时再报一次 42501 GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE LAB.STAFF TO USER db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE LAB.EMPLOYEE TO USER db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE LAB.TEMPL TO USER db2admin;ON TABLE后面必须写全限定名只写STAFF会按当前连接的 schema 解析容易授错对象。TO USER后面是接收权限的授权 ID大小写按数据库里的实际值来DB2 默认会把未加引号的标识符转成大写。GRANT 属于 DDL执行完自动提交不需要额外COMMIT。注意一点授权解决的是“能不能操作”不解决“能不能找到”。授完权代码里那句SELECT ... FROM EMPLOYEE依然会被解析成DB2ADMIN.EMPLOYEE该报 42704 还是报。所以授权之后必须回到 Java 代码里把 schema 补全两件事要一起做才算完。import java.sql.*; public class LabStaff { public static void main(String[] args) { Connection conn null; PreparedStatement pstmt null; ResultSet rs null; try { // 驱动类名随 DB2 版本不同会变找不到类时先去 jar 里确认类名是否存在 Class.forName(COM.ibm.db2.jdbc.app.DB2Driver); // URL 里的 sample 是数据库名不是 schema 名两者别混 conn DriverManager.getConnection( jdbc:db2:sample, db2admin, db2admin); // schema 必须写全否则会被补成当前用户的 schema pstmt conn.prepareStatement( SELECT ID, NAME, JOB, SALARY FROM LAB.STAFF WHERE ID ?); pstmt.setInt(1, 10); rs pstmt.executeQuery(); if (rs.next()) { System.out.println(rs.getString(NAME) / rs.getBigDecimal(SALARY)); } } catch (ClassNotFoundException e) { System.out.println(驱动类没找到检查 classpath e.getMessage()); } catch (SQLException e) { // 三个信息一起打缺一个都可能定位不到 System.out.println(SQLState e.getSQLState() ErrorCode e.getErrorCode() Msg e.getMessage()); } finally { // 关闭顺序和打开顺序相反 if (rs ! null) try { rs.close(); } catch (SQLException ignore) {} if (pstmt ! null) try { pstmt.close(); } catch (SQLException ignore) {} if (conn ! null) try { conn.close(); } catch (SQLException ignore) {} } } }这段代码里几个参数值得单独说。jdbc:db2:sample中的sample是数据库名连接串里出现数据库名不代表 schema 也是它schema 要靠 SQL 里的前缀指定。Class.forName那一行的类名在不同版本的 DB2 驱动里不通用老驱动是COM.ibm.db2.jdbc.app.DB2Driver新驱动是com.ibm.db2.jcc.DB2Driver报ClassNotFoundException时先确认 jar 里有没有这个类再改代码别硬猜。另外不要用Statement拼字符串传参数实验里图省事写WHERE ID id很常见一旦 ID 来源是输入框就等于给自己留了个注入的口子后面改成 GUI 时还得再返工一遍。2.3 建表失败「名字已被使用」别名把表名抢了还有一种更隐蔽的情况权限对了、schema 也写全了结果执行CREATE TABLE LAB.TEMPL (...)报SQL0601N 名称 LAB.TEMPL 已被使用但你去SYSCAT.TABLES里查又找不到这张表。这不是数据库发疯是别名占了名字。DB2 里别名和表共用同一套命名空间。之前的任务里为了省事执行过CREATE ALIAS TEMPL FOR EMPLOYEE这个别名注册在LAB下名字就叫TEMPL。之后再想建一张叫TEMPL的表命名空间冲突了。而且DROP TABLE LAB.TEMPL删不掉它会报对象类型不符因为它是别名不是表。-- TYPE A 是别名T 是真表V 是视图 SELECT TABNAME, TYPE, OWNER, CREATE_TIME FROM SYSCAT.TABLES WHERE TABSCHEMA LAB AND TABNAME TEMPL; -- 确认是别名之后再删删表用 DROP TABLE 是删不掉的 DROP ALIAS LAB.TEMPL;第一条查出来TYPE是A答案就明确了。DROP ALIAS之后重建表SQL0601N立刻消失。提示建对象之前先跑一次SELECT ... FROM SYSCAT.TABLES WHERE TABSCHEMA?比等到撞名再回头查省事得多。别名、视图、表、序列都在这个视图里有记录类型字段一看就清楚。这个坑之所以折腾人是因为报错信息说的是“名称已被使用”人的第一反应是“那我删了重建”结果DROP TABLE又报类型错两个报错互相矛盾很容易开始怀疑是不是数据库没提交。其实只要查一下对象类型两分钟就能解决。这也是我后来养成的一个习惯任何“对象已存在 / 对象不存在”的报错先查SYSCAT.TABLES确认对象类型和所属 schema再动手。3. 更新成功但数据没变autocommit、0 行匹配和数值溢出三连比报错更难查的是“没报错”。实验里改labupdate程序打印更新成功跑完去数据库一查SALARY一点没动。这类问题最消耗耐心因为控制台干净得像什么都没发生没有堆栈、没有红字只有一句自己写的“更新完成”。实际上它背后至少有三个独立原因长得像同一件事处理方式完全不同。3.1 setAutoCommit(false) 是开关不是保险为了演示事务代码里写sample.setAutoCommit(false)。这一行的语义是“从现在起每条 SQL 执行完都不自动提交等你发话”。发话的方式是commit()或rollback()。如果代码从头到尾没写commit()程序结束时连接被关闭DB2 按事务规则回滚未提交的修改数据自然一点没变。这不是玄学是 JDBC 的默认契约被主动关掉了。默认autoCommit是true每条语句各自成为一个事务执行完就落库一旦关掉就是一个长事务必须显式收尾。麻烦在于关闭自动提交后哪怕程序走完整个main也没报错肉眼完全看不出来。// 关掉自动提交开启手动事务 conn.setAutoCommit(false); try { PreparedStatement pstmt conn.prepareStatement( UPDATE LAB.STAFF SET SALARY SALARY * ? WHERE DEPT ?); pstmt.setBigDecimal(1, new BigDecimal(1.01)); pstmt.setInt(2, deptNo); int updateCount pstmt.executeUpdate(); System.out.println(影响行数 updateCount); conn.commit(); // 缺这一行前面的修改等于没做 } catch (SQLException e) { conn.rollback(); // 出错必须回滚否则连接复用时脏数据会跟着走 throw e; } finally { conn.setAutoCommit(true); // 恢复默认避免影响连接池里的下一位使用者 }commit()的位置要在所有更新逻辑执行完之后、异常处理之外rollback()放在 catch 里保证出错时事务干净收场finally里恢复自动提交是为了连接被复用时不把长事务状态带出去。这三个位置任何一个放错都会制造出“有的记录改了、有的没改”的半吊子现场。怎么验证开一个 DB2 命令行窗口另开一个会话连上同一个库在 Java 程序执行executeUpdate但还没commit的时候去查同一批数据会发现要么查不到、要么被锁住等待。这就证明修改确实在事务里挂着只是没落地。这招我一般在怀疑事务问题时先用一遍比反复读代码快。3.2 提示「修改了 0 行」where 条件根本没命中另一类“没生效”是压根没更新到数据。executeUpdate()的返回值是受影响的行数返回 0 说明语句执行成功、但 where 条件一行也没匹配上。实验里输入16时返回 0当时第一反应是程序有 bug去数据库一查才发现STAFF表里根本没有DEPT 16的记录。这个返回值得当回事不能只打印“更新成功”int updateCount pstmt.executeUpdate(); if (updateCount 0) { // 不是异常是 where 条件没命中先查条件本身对不对 System.out.println(警告没有匹配到 DEPT deptNo 的记录); }排查的时候直接在 DB2 命令行跑同一条 where 条件看看到底有多少行-- 先确认条件命中几行再谈程序对不对 SELECT COUNT(*) FROM LAB.STAFF WHERE DEPT 16; -- 顺便看看这张表里部门号到底有哪些输入值是不是拍脑袋来的 SELECT DISTINCT DEPT FROM LAB.STAFF ORDER BY DEPT;如果COUNT(*)是 0程序没有任何问题是你的输入值不在数据里。这种情况在学生实验里特别常见因为测试数据是别人建的STAFF表只有十几行部门号不连续。我的做法是把DISTINCT DEPT的结果打印在程序启动时输入前先看一眼有哪些值可选比盲试快得多。3.3 SQLState 22003salary*2 溢出但不是所有人都溢出溢出这条报错信息看着很吓人实际非常好定位。实验里输入 15 和 20 时SALARY SALARY * 2报类型转换溢出换成SALARY * 1.01之后两个都不溢出了。这个对比实验本身就是一次标准的二分排查变量只有一个结果变了就能锁定原因。根因是十进制字段的精度固定。DECIMAL(p, s)的整数位最多p - s位乘法会让结果的精度扩张一旦超过目标列能表示的上限DB2 直接抛SQL0804NJDBC 侧拿到的SQLState是22003。这不是 Java 的问题把同样的 SQL 拿到命令行跑一样报错。先查清楚字段的实际精度别照着感觉猜-- 查字段类型和精度SCALE 是小数位数LENGTH 是总长度 SELECT COLNAME, TYPENAME, LENGTH, SCALE FROM SYSCAT.COLUMNS WHERE TABSCHEMA LAB AND TABNAME STAFF AND COLNAME SALARY; -- 用同一批数据把乘法结果算出来看谁先超过上限 SELECT ID, SALARY, SALARY * 2 AS DOUBLED FROM LAB.STAFF WHERE DEPT IN (15, 20);第一句告诉你这个字段能吃多大的数第二句告诉你哪些行的乘积会越界。两句一对比为什么 15 溢出、为什么 1.01 不溢出答案就摆在结果集里了。Java 侧再补一刀金额一律用BigDecimal别用double。double的二进制表示存不下 0.01 这类十进制小数累加几次就会出现1234.5600000000001这种值写回DECIMAL字段时要么被静默截断要么在严格模式下直接报错。// 用字符串构造避免 double 字面量带进来的精度误差 BigDecimal factor new BigDecimal(1.01); BigDecimal salary rs.getBigDecimal(SALARY); BigDecimal result salary.multiply(factor).setScale(2, RoundingMode.HALF_UP);setScale(2, RoundingMode.HALF_UP)是在向数据库的字段精度对齐四舍五入到两位小数。如果不显式指定舍入模式setScale在需要截断时会直接抛ArithmeticException这个异常跟 SQL 一点关系没有但报错位置在写库前一行很容易被误当成数据库错误。顺带说一个实验里容易忽略的点溢出报错要不要捕获、捕获之后提示什么。很多人的做法是catch (SQLException e) { e.printStackTrace(); }然后界面上什么都不显示用户以为按钮没反应。这一类必须转成可读提示下一章展开。4. 异常处理把 SQLException 拆成四类弹窗才不会只会说「操作失败」GUI 改造之后最尴尬的场面是点了按钮弹窗永远只有一句“操作失败”。这句话对排查没有任何价值比不弹还糟因为用户以为程序坏了其实只是输入了一个超出字段范围的数字。要做的是把SQLException里的信息拆开按错误类别给不同的提示文案。这一步做完实验里要求的“违反约束、类型不匹配、溢出、非法符号”四种场景就能各自给出对得上的提示。4.1 getErrorCode、getSQLState、getMessage 分别拿什么这三个方法名字相近作用完全不同混用会导致判断条件永远不成立。getSQLState()返回五位标准状态码前两位是类别跨数据库通用23开头是完整性约束22开头是数据异常42开头是语法或访问规则问题。做分类判断优先用它因为换一个数据库厂商这套编码基本不变。getErrorCode()返回厂商私有的错误码。DB2 里通常对应 SQLCODE比如唯一索引冲突是-803非空约束是-407。它比 SQLState 更细但不可移植只在需要精确定位某个具体 DB2 行为时用。getMessage()是一段给人看的文本里面通常带对象名、字段名、具体值最适合原样记日志或者拼进提示的后半段。catch (SQLException e) { // 三个一起打排查时不用回头改代码 System.out.println(SQLState e.getSQLState()); System.out.println(ErrorCode e.getErrorCode()); System.out.println(Message e.getMessage()); }还有一个容易漏的SQLWarning。它不抛异常挂在Statement或Connection上得主动去取。某些截断、舍入行为只会产生警告而不报错数据看着写进去了实际已经被改过。实验文档里提到用stmt.getWarnings()拿它取值时机要在语句执行之后、下一条语句执行之前否则会被覆盖。// 语句执行完立刻取警告晚了就没了 SQLWarning warn stmt.getWarnings(); while (warn ! null) { System.out.println(WARN warn.getSQLState() warn.getMessage()); warn warn.getNextWarning(); }4.2 四类操纵类错误的判定条件和提示文案分类判断用SQLState打头判断最省事。下面这张对照表是我自己常用的映射覆盖实验要求的四类场景场景SQLStateDB2 典型 SQLCODE提示方向非空约束违反23502-407哪个字段必填没填唯一约束违反23505-803主键或唯一索引重复外键约束违反23503-530引用的父记录不存在检查约束违反23513-545值不在允许范围内数值溢出22003-802数值超出字段精度类型转换失败22018-420字符转数值或日期失败SQL 语法错误42601-104非法符号、逗号括号不配对对象不存在42704-204schema 或对象名写错权限不足42501-551该用户没有对应动作的特权这张表的价值在于看到SQLState前两位就能决定提示文案不用去背每条报错的全文。23开头的都归到“数据不合规”22开头的归到“数值或格式有问题”42开头的归到“SQL 或对象有问题”。private String toReadableMessage(SQLException e) { String state e.getSQLState(); if (state null) { return 未知错误 e.getMessage(); } if (state.startsWith(23)) { // 约束类把原始信息带上里面通常有约束名和字段名 return 数据不符合表约束请检查必填项或重复值。\n e.getMessage(); } if (22003.equals(state)) { return 数值超出字段范围请调小后再试。\n e.getMessage(); } if (22018.equals(state) || 22007.equals(state)) { return 输入格式不对请检查数字或日期写法。\n e.getMessage(); } if (42601.equals(state)) { return SQL 语句有语法错误注意引号、逗号和括号是否配对。; } return SQLState state 错误码 e.getErrorCode() \n e.getMessage(); }判断顺序要注意startsWith(23)必须放在最前面因为它是一个范围后面几个用equals精确匹配。如果先写equals(23505)再写startsWith(23)逻辑上没错但会漏掉 23502 这类没单独列出的情况。用范围兜底、用精确匹配细化这个顺序不要反。4.3 改造 labupdate 的 GUI输入、输出、异常三处落地GUI 化不是把System.out换成弹窗就完事三处地方要分别处理输入值解析、结果集输出、异常提示。输入这块JOptionPane.showInputDialog返回的是字符串用户点取消返回null输入非数字会抛NumberFormatException。这两个分支必须单独处理否则一个取消操作就能让程序抛异常退出。String input JOptionPane.showInputDialog(null, 请输入部门号); if (input null) { return; // 用户点了取消直接退出不要当成 0 处理 } int deptNo; try { deptNo Integer.parseInt(input.trim()); } catch (NumberFormatException e) { // 解析失败要在这里拦住别让它走到 SQL 层变成 22018 JOptionPane.showMessageDialog(null, 部门号必须是整数, 输入有误, JOptionPane.WARNING_MESSAGE); return; }输出这块把结果集拼成文本再一次性弹出来比每行弹一次体验好得多。拼接时用StringBuilder列值用getObject取再判空空值统一显示成“空”这一条在后面的任务里还要单独改。StringBuilder sb new StringBuilder(); while (rs.next()) { Object salary rs.getObject(SALARY); sb.append(rs.getInt(ID)).append( ) .append(rs.getString(NAME)).append( ) .append(salary null ? 空 : salary.toString()) // 空值统一显示 .append(\n); } JOptionPane.showMessageDialog(null, sb.length() 0 ? 无记录 : sb.toString(), 查询结果, JOptionPane.INFORMATION_MESSAGE);异常这块把上一节的toReadableMessage接进来弹窗用ERROR_MESSAGE类型标题写清楚是哪个操作失败。到这里四种场景就都能给出对得上的提示了违反约束会说“数据不符合表约束”类型不匹配会说“输入格式不对”溢出会说“数值超出范围”语法错误会说“注意引号逗号括号”。同一个catch块提示质量完全不一样。5. 避坑记录五个反复出现的现场下面这五条不是从文档里抄的是改代码过程中反复撞上的现场。每条的形态都不一样共同点是报错信息都不指向真正的根因得顺着往下追一步。5.1 改了代码没重新编译报错还是老的现象明明把表名从TEMPL改成了LAB.TEMPL重新运行还是报SQL0204N DB2ADMIN.TEMPL 是一个未定义的名称一度怀疑是 DB2 缓存了执行计划。原因javac编译输出了.class到某个目录运行时用的却是另一个目录下旧的.class。命令行里java LabStaff找的是当前目录或CLASSPATH里的类跟源码在不在同一个文件夹没关系。改了源码没重新编译或者编译了但运行的是旧路径跑的就是老逻辑。解决编译和运行都用显式路径把输出目录固定下来。# 编译到当前目录-d 指定输出位置避免和旧 class 混在一起 javac -d ./out LabStaff.java # 运行时把 classpath 指到同一个目录别依赖默认值 java -cp ./out LabStaff改完先ls ./out看一眼.class的时间戳比反复读代码快。5.2 用 db2admin 建的表换用户查不到现象用db2admin建的TEMPL代码里换成另一个账号连接报对象不存在但表和别名都确认过是存在的。原因CREATE TABLE TEMPL (...)没写 schema 时表会建在当前连接的授权 ID 同名 schema 下。db2admin建的表落在DB2ADMINschema另一个用户连接后默认解析到自己的 schema自然找不到。解决建表时一律写全限定名跨用户访问时 SQL 里也写全。-- 建表就钉死 schema别依赖当前连接的默认值 CREATE TABLE LAB.TEMPL ( ID INTEGER NOT NULL, NAME VARCHAR(20) ); -- 确认表到底落在哪个 schema SELECT TABNAME, OWNER FROM SYSCAT.TABLES WHERE TABNAME TEMPL;OWNER字段和TABSCHEMA对不上就是这个问题。5.3 结果集关了还去读抛结果集已关闭现象查询本身没问题一到取值就抛ResultSet closed或者操作不允许堆栈指向rs.getString。原因把rs.close()写在了while (rs.next())循环里面第一次迭代结束就把结果集关了或者把ResultSet返回给了方法外部方法内的finally已经把它关掉。解决关闭动作只放在finally循环体里只读不关不要把ResultSet跨方法返回要返回就把数据转成集合或字符串带走。try { rs pstmt.executeQuery(); while (rs.next()) { // 循环里只读 list.add(rs.getString(NAME)); } } finally { if (rs ! null) try { rs.close(); } catch (SQLException ignore) {} // 只在这里关 }5.4 空值显示成空白而不是「空」现象界面上某些格子是空的看不出是数据为NULL还是空字符串实验要求“所有为空的列值均以空的形式显示”结果一个都没显示出来。原因rs.getString对NULL返回null直接append到StringBuilder里会拼成字符串null而如果中间做过String.valueOf或者三元判断写反了就会成为空串。两种表现都跟预期不符。解决统一走一个转换方法所有列都过一遍别在每列上手写判断。// 所有列的显示值都从这里出保证空值表现一致 private String display(Object value) { return value null ? 空 : value.toString(); } // 用法用 getObject 取避免 getString 把 NULL 和空串混为一谈 String name display(rs.getObject(NAME));5.5 循环里复用 PreparedStatement参数没重设现象多行插入时第一行数据正确后面的行要么重复第一行的值要么某个字段是上一轮的残留。原因PreparedStatement预编译之后参数是“粘”在上面的同一轮循环里没重新setXxx的占位符会保留上一次的值。少设一个参数写进去的就是上一行的数据而且不报任何错。解决循环体内把每个占位符都显式赋值一遍哪怕值没变或者干脆每轮重新创建语句。String sql INSERT INTO LAB.TEMPL (ID, NAME) VALUES (?, ?); PreparedStatement ps conn.prepareStatement(sql); for (Row row : rows) { ps.setInt(1, row.id); // 每轮都设别省 ps.setString(2, row.name); // 哪怕 name 为 null 也要显式 setNull ps.addBatch(); } ps.executeBatch();setString(2, null)在某些驱动下会变成空串需要精确写入NULL时用ps.setNull(2, Types.VARCHAR)。6. 快速定位一条 JDBC 报错到手先看哪三样拿到一条 JDBC 报错我现在的顺序固定为三步基本十分钟内能定性。第一步看SQLState前两位。23是约束、22是数据异常、42是语法或对象、08是连接问题。这一步决定后面往哪个方向查不用一上来就读代码。第二步把同一条 SQL 拿到 DB2 命令行里原样跑一遍。命令行报错问题在 SQL 或数据命令行正常问题在 Java 代码或事务。这一步能砍掉一半的排查面。跑之前先VALUES CURRENT SCHEMA确认 schema很多时候差异就出在这里。第三步查对象和字段的真实定义。表在不在、是表还是别名、字段精度多少全在系统视图里比猜快得多。现象优先查一条命令对象不存在schema 与对象类型SELECT TABNAME, TYPE FROM SYSCAT.TABLES WHERE TABSCHEMALAB权限不足授权记录SELECT * FROM SYSCAT.TABAUTH WHERE TABSCHEMALAB数值溢出字段精度SELECT COLNAME, TYPENAME, LENGTH, SCALE FROM SYSCAT.COLUMNS WHERE TABNAMESTAFF更新不生效事务提交VALUES CURRENT ISOLATION加上程序里的commit()影响 0 行where 命中数SELECT COUNT(*) FROM LAB.STAFF WHERE DEPT?还有一个小技巧把连接的关键状态在程序启动时打一遍出问题时不用回头加日志。// 启动时打一次出问题时有据可查 DatabaseMetaData meta conn.getMetaData(); System.out.println(DB meta.getDatabaseProductName() meta.getDatabaseProductVersion()); System.out.println(Driver meta.getDriverName() meta.getDriverVersion()); System.out.println(AutoCommit conn.getAutoCommit());AutoCommitfalse出现在不该出现的场景里就是事务问题的信号驱动版本对不上就是类名和协议不匹配的信号。从那次实验之后我每次碰 JDBC都强制走一遍“先命令行复现、再查系统视图、最后看事务状态”这三步不管报错多简单。这个习惯救过我好几次——有一次线上报更新丢了看着像数据库故障最后查出来是连接池把autoCommit关了而业务代码没提交跟数据库一点关系没有。希望这些经验能帮到你少在同一个地方卡第二次。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
测试工程师如何从脏乱差代码中挖掘职业价值 1. 脏乱差模块的危机与机遇 在软件测试行业摸爬滚打十几年,我发现一个有趣的现象:那些被开发团队避之不及的"屎山"代码,反而成了测试工程师最好的职业护城河。这些代码腐化、业务耦合度高的遗留模块,就像一座座未经开发… · 2026/11/4 10:13:23
ARTEMIS:移动端AI原生交互协议栈解析 1. ARTEMIS不是另一个“手机遥控器”,而是把AI变成真人的操作系统层你有没有试过让AI帮你点开微信、翻到某个聊天窗口、截图发给同事?不是靠写死的坐标点击,也不是靠录屏回放,而是它自己“看懂”屏幕、理解任务、像人一样一步步操… · 2026/11/4 10:13:23
JDBC与DB2实验避坑:更新不生效、权限及连接异常排查 简介:这份资源是吉林大学《数据库应用程序开发》课程的实验报告文档,面向正在学习 Java 与 DB2 数据库交互的高校学生及需要巩固 JDBC 编程的开发者。报告围绕三个核心实验任务展开:JDBC 基础编程与表权限授予、labstaff.java 中通过 DriverM… · 2026/11/4 10:13:23
直流无刷电机AX BY CZ怎么划分?120°电角度与90°坐标系详解 一个叫"ax"的热搜词,把我拽回了直流无刷电机的老本行。搜这个词的人,大概率是在某张接线图或绕线图上被AX、BY、CZ三个字母组合卡住了,随后抛出这个经典问题:直流无刷电机ax by cz怎么划分,是按照垂直轴线划… · 2026/11/4 10:57:33
ZPL标签打印预览实战:Labelary渲染PNG与参数调优指南 标签打印这个行当里,ZPL 算是个绕不开的老朋友。只要用过斑马系打印机,或者对接过仓储、物流、零售后台的打印模块,大概率都跟这串以^XA开头、^XZ结尾的指令打过交道。它本质上是打印机自己能读的“母语”,描述的是标签上每一行文… · 2026/11/4 10:57:33
GOCI2波段参数与月球定标链路:辐亮度校验到ROLO模型 简介:《GOCI2 说明手册 波段信息说明》面向海洋遥感、卫星载荷标定与定量反演方向的研究人员、工程师及研究生,聚焦第二代地球同步轨道海洋水色成像仪 GOCI-II 的波段设置与辐射性能指标,帮助读者厘清短波至近红外各波段在藻华识别、悬浮颗粒… · 2026/11/4 10:57:33
从零构建AI工程能力:后端老兵的踩坑与重构实录 1. 从零搭建AI工程能力:一个后端老兵的踩坑与重构实录 "ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。不是因为它有多花哨,恰恰相反,它朴素得有点不像这个时代的项目名。但正是这种朴素ÿ… · 2026/11/4 10:57:33
直流无刷电机三相绕组ax by cz划分与120度电角度详解 前阵子修一台直流无刷电机,端子排上贴着 ax、by、cz 三个标签,驱动板却只标了 U、V、W。我拿着表笔对着标签发愣:这 ax、by、cz 到底按什么划分出来的?有人挂在嘴边说,三相绕组就是按垂直轴线分的,一组一组… · 2026/11/4 10:57:33
无人机水质监测系统设计与实战经验分享 1. 无人机水质监测的技术革新 最近在环境监测领域出现了一个有趣的现象:越来越多的科研团队开始采用无人机进行水质采样和监测。这种创新方法不仅解决了传统人工采样效率低、危险性高的问题,还能获取更全面的水域数据。作为一名长期从事环境监测的技术人… · 2026/11/4 10:56:32
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38