1. Oracle 例外处理到底在解决什么问题
写 PL/SQL 的人迟早会碰到这样的场景:存储过程跑得好好的,某天上游传进来一个空值,或者SELECT INTO突然返回两行,程序直接抛出一串ORA-01422、ORA-06502之类的报错,日志里只有一行红字,根本不知道是哪条业务分支炸的。Oracle 例外(异常)处理就是专门用来兜住这些运行时错误的机制,它让你能把「出错」变成「可控的分支」,而不是让整个事务莫名其妙地回滚。
Oracle 的例外大致分三类:预定义例外(有名字的系统错误,比如NO_DATA_FOUND、TOO_MANY_ROWS、ZERO_DIVIDE)、非预定义例外(系统有错误码但没给你起名,需要自己PRAGMA EXCEPTION_INIT绑定)、自定义例外(跟 Oracle 错误无关,纯粹是业务逻辑上你认为「这算异常」,用RAISE手动抛出)。这三类的捕获写法不一样,排查思路也不一样,很多人卡就卡在「不知道当前这个报错属于哪一类」。
这篇面向的是正在写或维护 PL/SQL 的开发者,尤其是那种「报错能复现、但定位不到具体例外分支」的情况。我会用一个可复制的 TaoToken 统一 Key 配置骨架,把调试环境先搭顺,再演示一次完整的异常复现与验证动作,让你能照着把自己的例外分支一条条跑通。核心检索词就三个:Oracle、例外、异常处理。
2. 用 TaoToken 统一 Key 把调试环境先理顺
调试 PL/SQL 例外最烦的不是写代码,而是环境零散:本地一个连接串、测试库一个、有时候还要连同事的库看数据,Key 和地址到处复制,改一次错一次。我习惯用 TaoToken 做统一入口,把模型对话、编码辅助、API 调用收敛到一套 Key 上,这样排查例外时注意力能集中在 SQL 本身,而不是在配置里翻来翻去。
TaoToken 在这里的角色是「统一 Key 的接入层」:你申请一个 Key,配置好 base_url,就能在支持 OpenAI 兼容协议的工具里直接调用,包括做 PL/SQL 代码补全、报错解释、例外分支生成的场景。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM,直接填进配置里)。
需要先拿 Key 的话,去控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,然后在 API Keys 页面生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。拿到形如sk-xxxx的字符串后,别急着写进代码,先放进环境变量,后面所有配置都引用变量,避免 Key 泄漏到版本库里。
注意:Key 只用于你自己的调试环境,不要硬编码进存储过程或提交到 Git。PL/SQL 里如果需要调用外部 HTTP 服务,也要走服务端封装,不要把 Key 写进数据库对象。
如果你用的是 Claude Code 这类编码工具来辅助写 PL/SQL,可以走 Anthropic 兼容入口:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite ,它和统一 Key 是同一套账号体系,配置一次就能复用。
3. 可复制的配置骨架与 settings.json 片段
先给一份最小可用的配置骨架。核心就三样:base_url、api_key、model。base_url 固定填https://taotoken.net/api,api_key 从环境变量读,model 按你实际用的填。下面这份settings.json片段可以直接抄,注意把env里的变量名对上你系统里实际设置的。
{ "env": { "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" }, "models": { "default": "gpt-4o-mini", "coding": "claude-3-5-sonnet" }, "request": { "timeout": 60, "max_retries": 2 } }如果你不想把 Key 写进 json,用环境变量注入更稳妥。Linux/macOS 下:
export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Windows PowerShell:
$env:TAOTOKEN_API_KEY="sk-你的Key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"配置好之后,先用一个最简单的请求验证连通性,别等写了一大段 PL/SQL 才发现 Key 是错的。用 curl 测一下:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "用一句话解释 Oracle 的 NO_DATA_FOUND 例外"}] }'返回里能看到choices字段和一段解释,就说明 Key 和地址都通了。这一步过了,再进 PL/SQL 的例外调试。
4. 异常复现与验证:从 ORA 报错到自定义例外捕获
现在进入正题。我准备了一张简化的员工表emp1,字段有empno和sal,然后写一个存储过程,故意制造三种例外场景,逐个验证捕获分支。
先建表和数据:
CREATE TABLE emp1 ( empno NUMBER(4) PRIMARY KEY, sal NUMBER(7,2) ); INSERT INTO emp1 VALUES (7369, 800); INSERT INTO emp1 VALUES (7499, 1600); COMMIT;4.1 预定义例外:NO_DATA_FOUND 与 TOO_MANY_ROWS
第一个过程演示SELECT INTO查不到数据时触发NO_DATA_FOUND:
CREATE OR REPLACE PROCEDURE ex_no_data(p_empno NUMBER) IS v_sal emp1.sal%TYPE; BEGIN SELECT sal INTO v_sal FROM emp1 WHERE empno = p_empno; DBMS_OUTPUT.PUT_LINE('薪资: ' || v_sal); EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('没有找到员工: ' || p_empno); WHEN TOO_MANY_ROWS THEN DBMS_OUTPUT.PUT_LINE('返回了多行,请检查条件'); END; /调用一个不存在的工号:
SET SERVEROUTPUT ON; EXEC ex_no_data(9999);输出是没有找到员工: 9999,说明NO_DATA_FOUND分支被正确捕获。如果你把WHERE条件去掉,就会触发TOO_MANY_ROWS,输出第二行提示。这两个是最常见的预定义例外,写SELECT INTO时几乎必须带上。
4.2 非预定义例外:用 PRAGMA EXCEPTION_INIT 绑定错误码
有些 Oracle 错误没有预定义名字,比如ORA-02291(违反外键约束)。你得先声明一个例外名,再用PRAGMA EXCEPTION_INIT把错误码绑上去:
CREATE OR REPLACE PROCEDURE ex_fk(p_empno NUMBER) IS e_fk_violation EXCEPTION; PRAGMA EXCEPTION_INIT(e_fk_violation, -2291); BEGIN INSERT INTO emp1 VALUES (p_empno, 100); EXCEPTION WHEN e_fk_violation THEN DBMS_OUTPUT.PUT_LINE('外键约束冲突,工号: ' || p_empno); WHEN DUP_VAL_ON_INDEX THEN DBMS_OUTPUT.PUT_LINE('主键重复,工号: ' || p_empno); END; /调用EXEC ex_fk(7369);会命中DUP_VAL_ON_INDEX,因为 7369 已存在。这里DUP_VAL_ON_INDEX是预定义例外,而e_fk_violation是非预定义,两者可以写在同一个EXCEPTION块里,Oracle 会按顺序匹配。
4.3 自定义例外:业务逻辑上的「异常」
自定义例外跟 Oracle 错误无关,是你自己认为「这算异常」。比如更新员工薪资时,如果SQL%NOTFOUND为真,说明没更新到任何行,业务上要报错:
CREATE OR REPLACE PROCEDURE ex_test(spNo NUMBER) IS myex EXCEPTION; BEGIN UPDATE emp1 SET sal = sal + 100 WHERE empno = spNo; IF SQL%NOTFOUND THEN RAISE myex; END IF; DBMS_OUTPUT.PUT_LINE('更新成功,工号: ' || spNo); EXCEPTION WHEN myex THEN DBMS_OUTPUT.PUT_LINE('没有做任何更新,工号: ' || spNo); END; /调用EXEC ex_test(8888);输出没有做任何更新,工号: 8888,调用EXEC ex_test(7369);输出更新成功,工号: 7369。这就是自定义例外的完整闭环:声明、RAISE、捕获。
4.4 用 TaoToken 辅助定位报错
当你拿到一个陌生的ORA-xxxxx报错,不确定它属于哪一类例外时,可以直接把报错贴给模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,让它告诉你这个错误码对应的预定义例外名、是否需要PRAGMA EXCEPTION_INIT、以及推荐的捕获写法。实测下来,这一步能省掉大量翻文档的时间,尤其是那些不常见的错误码。
如果你在长期维护一套 PL/SQL 代码库,需要反复生成例外分支、补全EXCEPTION块,可以考虑 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,把编码辅助的调用额度固定下来,不用每次临时配。
5. 本篇常见错误排查
ORA-06550 / PLS-00201:标识符必须声明。多半是PRAGMA EXCEPTION_INIT写在了BEGIN之后,或者例外名拼写不一致。声明区必须在IS和BEGIN之间。
捕获不到自定义例外。检查RAISE的例外名和WHEN后面的名字是否完全一致,Oracle 对大小写不敏感但拼写必须一致。另外WHEN OTHERS如果写在前面,会吞掉后面的具体分支,顺序要从具体到宽泛。
DBMS_OUTPUT 没输出。忘了SET SERVEROUTPUT ON,或者客户端缓冲区太小。在 SQL*Plus 里先执行SET SERVEROUTPUT ON SIZE UNLIMITED。
Key 配置报 401。先确认Authorization头是Bearer sk-xxx格式,中间有空格;再确认 base_url 是https://taotoken.net/api而不是带/v1的完整路径(具体看工具要求,有的工具需要补/v1)。用第 3 节的 curl 命令先测通再进代码。
例外被 WHEN OTHERS 静默吞掉。这是最常见的坑。WHEN OTHERS里至少写一句DBMS_OUTPUT.PUT_LINE(SQLERRM)或写日志表,否则出错你什么都看不到。生产环境建议把SQLCODE和SQLERRM一起记录。
非预定义例外的错误码符号搞反。PRAGMA EXCEPTION_INIT里的错误码要带负号,比如-2291,写成正数会绑定失败。
6. 把例外分支一条条跑通
例外处理这件事,光看文档记不住,必须自己造数据、触发、看输出。我的建议是:每写一个存储过程,至少准备三个调用用例——正常路径、预定义例外路径、自定义例外路径,跑一遍确认每个WHEN分支都能命中。上面那三个过程ex_no_data、ex_fk、ex_test就是最小验证集,你可以直接拿去改。
调试环境用 TaoToken 统一 Key 收敛之后,换库、换工具都不用重新配 Key,注意力能真正放在 SQL 逻辑上。需要新建 Key 或管理额度时,控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到配置问题先翻文档再排查,比盲目试错快得多。