PL/SQL异常处理补充:本文在基础异常处理语法基础上,重点补充了5个实用知识点:
- 常见预定义异常(如NO_DATA_FOUND)及其应用场景;
- 两种自定义异常实现方式(异常别名与RAISE_APPLICATION_ERROR);
- 异常处理最佳实践,包括日志表设计、带步骤标记的模板和异常处理策略决策树;
- 高级技巧如异常传播、批量操作异常隔离和11g+的堆栈追踪;
- 推荐的异常处理规范模板。
特别强调事务性操作必须ROLLBACK,并通过日志表记录异常细节,为开发人员提供了从基础到进阶的完整异常处理方案。
PL/SQL:异常处理补充
一、已有知识梳理(保持结构清晰)
1. 核心语法
sql
EXCEPTION
WHEN 异常1 THEN 处理动作1;
WHEN 异常2 THEN 处理动作2;
WHEN OTHERS THEN 处理动作; -- 捕获所有未处理的异常
2. 关键内置函数
| 函数 | 说明 |
|---|---|
SQLERRM |
返回完整的异常错误文本 |
SQLCODE |
返回错误代码(数字) |
二、重要补充内容
补充1:常见预定义异常(不用声明直接用)
| 异常名 | 触发场景 | SQLCODE |
|---|---|---|
NO_DATA_FOUND |
SELECT INTO 未查到数据 | +100 |
TOO_MANY_ROWS |
SELECT INTO 返回多行 | -1422 |
DUP_VAL_ON_INDEX |
违反唯一约束/主键 | -1 |
INVALID_CURSOR |
无效游标操作 | -1001 |
ZERO_DIVIDE |
除数为0 | -1476 |
INVALID_NUMBER |
字符串转数字失败 | -1722 |
LOGIN_DENIED |
登录失败 | -1017 |
PROGRAM_ERROR |
PL/SQL内部错误 | -6501 |
示例:按具体异常分类处理
sql
CREATE OR REPLACE PROCEDURE P_EXAMPLE(P_EMPNO NUMBER) IS
V_ENAME VARCHAR2(100);
BEGIN
SELECT ENAME INTO V_ENAME FROM EMP WHERE EMPNO = P_EMPNO;
DBMS_OUTPUT.PUT_LINE(V_ENAME);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('错误:员工编号' || P_EMPNO || '不存在');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('错误:数据重复,查出多行');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('未知错误:' || SQLERRM);
RAISE; -- 重新抛出异常
END;
补充2:自定义异常(业务逻辑异常)
方式一:预定义异常别名
sql
DECLARE
E_SAL_TOO_HIGH EXCEPTION;
PRAGMA EXCEPTION_INIT(E_SAL_TOO_HIGH, -20001); -- 绑定自定义错误号
BEGIN
IF V_SAL > 50000 THEN
RAISE E_SAL_TOO_HIGH;
END IF;
EXCEPTION
WHEN E_SAL_TOO_HIGH THEN
DBMS_OUTPUT.PUT_LINE('薪资超过限额');
END;
方式二:使用RAISE_APPLICATION_ERROR(最常用)
sql
CREATE OR REPLACE PROCEDURE P_UPDATE_SAL(P_EMPNO NUMBER, P_NEW_SAL NUMBER) IS
BEGIN
IF P_NEW_SAL < 0 THEN
RAISE_APPLICATION_ERROR(-20001, '薪资不能为负数');
END IF;
IF P_NEW_SAL > 50000 THEN
RAISE_APPLICATION_ERROR(-20002, '薪资不能超过50000');
END IF;
UPDATE EMP SET SAL = P_NEW_SAL WHERE EMPNO = P_EMPNO;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
-- 写入日志表
INSERT INTO ERROR_LOG(ERR_DATE, ERR_CODE, ERR_MSG, PROC_NAME)
VALUES(SYSDATE, SQLCODE, SQLERRM, 'P_UPDATE_SAL');
COMMIT;
RAISE; -- 继续向上抛出
END;
注意 :RAISE_APPLICATION_ERROR 的错误号范围:
-20000到-20999
补充3:异常处理的最佳实践(工作必备)
① 日志表示例
sql
CREATE TABLE ERROR_LOG (
LOG_ID NUMBER PRIMARY KEY,
PROC_NAME VARCHAR2(100),
ERR_CODE NUMBER,
ERR_MSG VARCHAR2(4000),
ERR_DATE DATE,
USER_NAME VARCHAR2(30)
);
CREATE SEQUENCE SEQ_ERROR_LOG;
② 带日志的存储过程模板
sql
CREATE OR REPLACE PROCEDURE P_BUSINESS(P_ID NUMBER) IS
V_STEP VARCHAR2(100);
BEGIN
V_STEP := '查询用户信息';
-- 业务逻辑1
V_STEP := '更新订单状态';
-- 业务逻辑2
V_STEP := '发送通知';
-- 业务逻辑3
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
INSERT INTO ERROR_LOG(LOG_ID, PROC_NAME, ERR_CODE, ERR_MSG, ERR_DATE, USER_NAME)
VALUES(SEQ_ERROR_LOG.NEXTVAL, 'P_BUSINESS', SQLCODE, SQLERRM, SYSDATE, USER);
COMMIT; -- 日志独立提交
-- 根据业务决定是否继续抛出
RAISE_APPLICATION_ERROR(-20099, '程序执行失败,失败步骤:' || V_STEP || ',原因:' || SQLERRM);
END;
③ 异常处理策略决策树
text
发生异常
│
├── 可预期且可恢复(如NO_DATA_FOUND)
│ └── 捕获后给默认值或跳过,继续执行
│
├── 可预期但不可恢复(如参数校验失败)
│ └── RAISE_APPLICATION_ERROR 返回友好错误信息
│
├── 不可预期异常
│ └── OTHERS捕获 → 写日志 → 回滚 → RAISE抛出
│
└── 事务性操作(多个DML)
└── 异常必须ROLLBACK,保证数据一致性
补充4:高级异常处理技巧
① 异常传播机制
sql
BEGIN
子程序调用 -- 子程序内异常若未捕获,会传播到外层
EXCEPTION
WHEN OTHERS THEN
-- 外层可以再处理
END;
② 批量操作中的异常处理(常用)
sql
DECLARE
TYPE T_EMP_ID IS TABLE OF EMP.EMPNO%TYPE;
V_IDS T_EMP_ID := T_EMP_ID(7369, 7499, 9999, 7521);
BEGIN
FOR I IN 1..V_IDS.COUNT LOOP
BEGIN -- 内层块隔离异常
UPDATE EMP SET SAL = SAL * 1.1 WHERE EMPNO = V_IDS(I);
IF SQL%NOTFOUND THEN
RAISE_APPLICATION_ERROR(-20001, '员工不存在:' || V_IDS(I));
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('处理员工' || V_IDS(I) || '失败:' || SQLERRM);
-- 继续处理下一个,不影响事务一致性
END;
END LOOP;
COMMIT;
END;
③ 获取异常完整堆栈(11g+)
sql
DECLARE
V_STACK CLOB;
BEGIN
-- 业务代码
RAISE ZERO_DIVIDE;
EXCEPTION
WHEN OTHERS THEN
DBMS_UTILITY.FORMAT_ERROR_STACK(); -- 错误信息
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE(); -- 调用堆栈(行号)
DBMS_UTILITY.FORMAT_CALL_STACK(); -- 完整调用栈
END;
补充5:RAISE 的使用对比
| 写法 | 含义 |
|---|---|
RAISE; |
重新抛出当前异常,保留原始错误信息 |
RAISE NO_DATA_FOUND; |
抛出指定的预定义异常 |
RAISE_APPLICATION_ERROR(code, msg); |
抛出自定义错误(不进入OTHERS?------会进入) |
三、工作中的异常处理规范(推荐)
sql
CREATE OR REPLACE PROCEDURE 存储过程名(参数) IS
-- 声明变量
V_ERR_MSG VARCHAR2(4000);
BEGIN
-- 1. 参数校验(主动抛异常)
IF 参数不合法 THEN
RAISE_APPLICATION_ERROR(-20001, '参数错误:...');
END IF;
-- 2. 业务处理(每一段关键操作后COMMIT或统一提交)
-- 3. 正常提交
COMMIT;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
ROLLBACK;
RAISE_APPLICATION_ERROR(-20002, '数据重复');
WHEN OTHERS THEN
ROLLBACK;
V_ERR_MSG := SQLERRM;
-- 写日志
INSERT INTO ERROR_LOG VALUES(...);
COMMIT;
-- 重新抛出给调用方
RAISE;
END;
四、总结对比表
| 维度 | 原笔记 | 补充说明 |
|---|---|---|
| 语法 | WHEN OTHERS THEN ... |
还可以按具体异常分类处理 |
| 异常信息 | SQLERRM |
还有 SQLCODE、DBMS_UTILITY.FORMAT_ERROR_BACKTRACE |
| 异常后行为 | 继续/回滚 | 可以通过 RAISE 中断并向上传播 |
| 自定义异常 | 未涉及 | RAISE_APPLICATION_ERROR 是最常用方式 |
| 日志记录 | 提到但未展开 | 工作中写日志表是关键实践 |
| 批量场景 | 未涉及 | 内层块独立异常处理,让循环继续 |
如果您需要,我可以进一步提供:
-
异常日志表的完整建表语句和插入模板
-
Spring Boot 调用 Oracle 存储过程时如何接收自定义异常
-
异常处理与事务边界的设计模式