ITPub博客

首页 > 数据库 > Oracle > 利用Oracle函数保留业务异常的原始位置

利用Oracle函数保留业务异常的原始位置

原创 Oracle 作者:a960549548 时间:2026-10-29 08:37:34 0 删除 编辑

大部分 PL/SQL 程序的异常处理都长这样:WHEN OTHERS THEN 记一条日志,再抛出或吞掉。日志里通常存的是 SQLERRM,问题是 SQLERRM 只有一句错误文本,比如 ORA-20001: 账户 7 余额不足——业务信息有了,但出错位置完全没有。几十层调用深的过程链,同一句错误可能从任何一处抛出,只凭 SQLERRM 排查,基本等于在源代码里大海捞针。

Oracle 从 10g 起在 DBMS_UTILITY 包里提供了三个格式化函数,用于处理错误发生了,但不知道发生在哪。

一、三个格式化函数

函数 返回内容 典型用途
FORMAT_ERROR_BACKTRACE 异常抛出点的调用栈,一串 ORA-06512 行号 定位原始出错行
FORMAT_ERROR_STACK 当前完整的错误栈(最长 2000 字节)

替代有长度限制的 SQLERRM

记日志

FORMAT_CALL_STACK 当前时刻的调用栈 任意位置的调用路径调试

三者都在 DBMS_UTILITY 包下,该包 EXECUTE 授予了 PUBLIC,无需额外授权。FORMAT_ERROR_BACKTRACE 的关键性质是: 即使异常已经传播到外层的处理块,它返回的仍是异常最初抛出点开始的调用链 ;若当前没有正在处理的异常,返回 NULL。

二、实测:一次不带位置与带位置的日志对比

1. 建日志表(TEST 用户)


SQL
> 
create 
table err_log
(

  2    ts       date ,
  3    err_msg  varchar2 ( 500 ) ,
  4    err_trace varchar2 ( 4000 )
  5   ) ;

Table created.

2. 构造三层调用链,在最内层抛错


SQL
> 
create 
or 
replace 
procedure p_inner
(p_id number
) 
as

  2   begin
  3     if p_id > 0 then
  4      raise_application_error ( - 20001 , '账户 ' || p_id || ' 余额不足' ) ;
  5     end if ;
  6   end ;
  7  /

Procedure created.

SQL > create or replace procedure p_mid (p_id number ) as
  2   begin
  3    p_inner (p_id ) ;
  4   end ;
  5  /

Procedure created.

SQL > create or replace procedure p_outer (p_id number ) as
  2   begin
  3    p_mid (p_id ) ;
  4  exception
  5     when others then
  6       insert into err_log (ts , err_msg , err_trace )
  7       values (sysdate ,
  8              sqlerrm ,
  9              dbms_utility .format_error_backtrace ) ;
10       commit ;
11      raise ;
12   end ;
13  /

Procedure created.

注意顺序:先记日志、后 re-raise。这一点后面解释。

3. 触发异常


SQL
> exec p_outer
(
7
)

BEGIN p_outer ( 7 ) ; END ;
*
ERROR at line 1 :
ORA - 20001 : 账户 7 余额不足
ORA - 06512 : at "TEST.P_OUTER" , line 11
ORA - 06512 : at line 1

4. 对比日志里两列的信息量


SQL
> 
select err_msg
, err_trace 
from err_log
;


ERR_MSG
----------------------------------------------------------------
ORA - 20001 : 账户 7 余额不足

ERR_TRACE
----------------------------------------------------------------
ORA - 06512 : at "TEST.P_INNER" , line 4
ORA - 06512 : at "TEST.P_MID" , line 3
ORA - 06512 : at "TEST.P_OUTER" , line 3

差别一目了然:SQLERRM 只有错误文本;FORMAT_ERROR_BACKTRACE 给出完整调用链——错误在 P_INNER 第 4 行抛出(正是 raise_application_error 那行),经 P_MID 第 3 行、P_OUTER 第 3 行层层传上来。行号与 USER_SOURCE 的 LINE# 一一对应:


SQL
> 
select 
text 
from user_source

  2   where name = 'P_INNER' and line = 4 ;

TEXT
----------------------------------------------------------------
     raise_application_error ( - 20001 , '账户 ' || p_id || ' 余额不足' ) ;

定位到此结束。没有这个函数时,这条链路上任何一处 raise 都可能是元凶;有了它,一行 SQL 指到具体行号。

5. FORMAT_CALL_STACK 的补充视角

把第 2 步 P_OUTER 的插入语句再加一列 dbms_utility.format_call_stack,日志中会看到另一种形态的输出:

----- PL/SQL Call Stack -----

 object      line  object
 handle    number  name
...
15f3a2c74         6  procedure TEST.P_OUTER
15e8c1ab2         1  anonymous block

区别在于:FORMAT_ERROR_BACKTRACE 记录的是异常抛出瞬间的栈,FORMAT_CALL_STACK 记录的是调用它那一刻的栈(这里是异常处理块内)。前者用于还原错误路径,后者适合在任意想看调用路径的地方埋点(不限于异常场景)。

三、注意事项

  1. 先取 backtrace 再 re-raise。raise; 本身会在栈上追加一个新帧:第 3 步客户端显示的 ORA-06512 只剩 P_OUTER 第 11 行(re-raise 位置)和匿名块,原始链条被截断了。这正是在 re-raise 之前先记录的原因,也让为什么日志里的栈和客户端看到的不一样这个常见疑惑有了答案;

  2. FORMAT_ERROR_STACK 相对 SQLERRM 的价值在于长度:SQLERRM 受长度限制,FORMAT_ERROR_STACK 返回最长 2000 字节的完整错误栈,长错误消息(多行堆叠的触发器错误等)不会被截断,日志表字段建议至少 4000 字节;

  3. backtrace 只反映最近一次抛出的异常;如果处理块里又引发了新异常,之前的链条就取不回来了;

  4. 没有 IN 参数,任何会话直接调用即可;不在异常处理块中调用 FORMAT_ERROR_BACKTRACE 时返回 NULL,不要在正常路径里把它当函数用;

  5. 老版本(9i 及以前)没有这个函数,只能靠每个处理块逐层 raise_application_error(-20001, '位置:...') 人工传位置,升级后这类遗产代码可以逐步清理。

规范做法可以固化成一行:insert into err_log(...) values (sysdate, dbms_utility.format_error_stack, dbms_utility.format_error_backtrace)。错误内容与错误位置一次取全,这一行值得进所有程序的公共异常处理模板。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013625/viewspace-3186996/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
Oracle ACE,MySQL 8.0 OCP,PMP,拥有OceanBase OBCP,TiDB PCTP,GoldenDB GDCP等多项认证,ITPUB认证专家,微信公众号:DBA之友。

注册时间:2022-02-08

  • 博文量
    134
  • 访问量
    172675