41.13. 错误处理

41.13.1. 异常处理概述
41.13.2. 预定义异常
41.13.3. 显式抛出异常
41.13.4. 支持DBMS_UTILITY内置包format_error_backtrace函数
41.13.5. 支持 SQLCODE
41.13.6. 支持 SQLERRM

本章讲解如何处理 pl/sql 编译时警告和 pl/sql 运行时错误。后者被称为异常。

41.13.1. 异常处理概述

异常(pl/sql 运行时错误)可能由于设计缺陷、编码错误、硬件故障等多种原因而引起。 您无法预见所有可能的异常,但可以编写异常处理程序,使您的程序能够在出现异常时继续运行。

任何 pl/sql 块都可以有异常处理部分,它可以有一个或多个异常处理程序。例如, 异常处理部分可以具有以下语法:

EXCEPTION
  WHEN ex_name_1 THEN statements_1                 -- 异常处理程序
  WHEN ex_name_2 OR ex_name_3 THEN statements_2    -- 异常处理程序
  WHEN OTHERS THEN statements_3                    -- 异常处理程序
END;

当块的可执行部分中引发异常时,可执行部分停止并将控件转移到异常处理部分。 如果引发 ex_name_1,则运行 statements_1。如果引发 ex_name_2 或 ex_name_3, 则运行 statements_2。如果引发其他异常,则运行 statements_3。

在异常处理程序运行后,控制转移到封闭块的下一个语句。 如果没有封闭块,则:

  • 如果异常处理程序在子程序中,则控制返回到调用者,在调用语句之后

  • 如果异常处理程序在匿名块中,则控制转移到主机环境

41.13.1.1. 异常类别

异常类别包括:

  • 内部定义 运行时系统隐式(自动)引发内部定义的异常

  • 预定义 预定义异常是内部定义的异常,由 pl/sql 赋予名称

  • 用户定义 您可以在任何 pl/sql 匿名块、子程序或包的声明部分中声明自己的异常

41.13.1.2. 异常处理程序的优点

使用异常处理程序进行错误处理使程序更易编写和理解,并减少未处理异常的可能性。 如果没有异常处理程序,您必须在可能发生错误的每个地方检查每个可能的错误,然后处理它。 特别是如果错误不能立即检测到,很容易忽略可能的错误或可能发生错误的地方。 错误处理代码分散在整个程序中。 有了异常处理程序,您不需要知道每个可能的错误或可能发生错误的地方。 您只需要在可能发生错误的每个块中包含一个异常处理部分。 在异常处理部分中,您可以包括针对特定和未知错误的异常处理程序。 如果块中的任何位置(包括子块内部)发生错误,则异常处理程序将处理它。 错误处理代码隔离在块的异常处理部分中。

目前 pl/sql 支持预定义错误和用户定义错误。

41.13.2. 预定义异常

预定义异常是具有预定义名称的内部定义异常。运行时系统隐式(自动)引发预定义异常。 由于预定义异常具有名称,因此可以专门为它们编写异常处理程序。

目前支持 7 种预定义错误:

  • INVALID_CURSOR

  • CASE_NOT_FOUND

  • DUP_VAL_ON_INDEX

  • NOT_DATA_FOUND

  • TOO_MANY_ROWS

  • ZERO_DIVIDE

  • LOGIN_DENIED

41.13.2.1. INVALID_CURSOR(无效游标)

INVALID_CURSOR 主要针对两种情况:显式游标和游标变量。

41.13.2.1.1. 显式游标

  • 使用 CLOSE 语句关闭打开的显式游标,从而允许重用其资源。 关闭游标后,不能从其结果集中获取记录或引用其属性。 如果尝试这样做,则 pl/sql 引发预定义的异常 INVALID_CURSOR。

  • 如果显式游标未打开,则除 %ISOPEN 之外的任何属性引用都会引发预定义的异常 INVALID_CURSOR。

例如,关闭游标变量后,不能从其结果集中获取记录或引用其属性。

--preconditional
create table employees(id int, name varchar2(100), job varchar2(100));
insert into employees values (22091401, 'sunli', 'actress');
insert into employees values (22091402, 'dengchao', 'actor');
insert into employees values (22091403, 'lishizhen', 'doctor');
insert into employees values (22091404, 'zhangsanfeng', 'martial artist');

create or replace function invalid_cursor_f return int as
   cursor c1 is select * from  employees;
   emp_cur employees%rowtype;
begin
    open c1;
    loop
      fetch c1 into emp_cur;
      exit when c1%notfound;
      close c1;
    end loop;
    close c1;
EXCEPTION
  WHEN invalid_cursor THEN
    DBMS_OUTPUT.PUT_LINE('invalid cursor');
    return 1;
end;
/

select invalid_cursor_f();
    

例如,如果显式游标未打开,则除 %ISOPEN 之外的任何属性引用都会引发预定义的异常 INVALID_CURSOR。

create or replace function invalid_cursor_f return int as
   cursor c1 is select * from  employees;
   emp_cur employees%rowtype;
   ora_found boolean := false;
begin
   ora_found := c1%found;
EXCEPTION
  WHEN invalid_cursor THEN
    DBMS_OUTPUT.PUT_LINE('invalid cursor');
   return 1;
end;
/

select invalid_cursor_f();
    

Tip

使用 pl/sql 进行错误处理,且 lightdb_dblevel_syntax_compatible_type 的值为 oracle。

41.13.2.1.2. 游标变量

  • 当你不再需要一个游标变量时,用CLOSE语句关闭它,从而允许其资源被重用。在关闭游标变量之后,你不能从其结果集中获取记录或引用其属性。如果你尝试这样做,pl/sql会引发预定义的异常INVALID_CURSOR。

  • 游标变量具有与显式游标相同的属性。游标变量属性的值语法为cursor_variable_name紧接着attribute(例如,cv%ISOPEN)。如果一个游标变量没有打开,引用除%ISOPEN以外的任何属性都会引发预定义的异常INVALID_CURSOR。

例如,在关闭游标变量之后,你不能从其结果集中获取记录或引用其属性。

create or replace function invalid_cursor_f return int as
   c1 sys_refcursor;
   emp_cur employees%rowtype;
begin
    open c1 for select * from  employees;
    loop
      fetch c1 into emp_cur;
      exit when c1%notfound;
      close c1;
    end loop;
    close c1;
EXCEPTION
  WHEN invalid_cursor THEN
    DBMS_OUTPUT.PUT_LINE('invalid cursor');
    return 1;
end;
/

select invalid_cursor_f();
    

例如,如果一个游标变量没有打开,引用除%ISOPEN以外的任何属性都会引发预定义的异常INVALID_CURSOR。

create or replace function invalid_cursor_f return int as
   c1 sys_refcursor;
   emp_cur employees%rowtype;
   ora_isopen boolean := false;
begin
  ora_isopen := c1%isopen;
  return 1;
  exception 
    when INVALID_CURSOR then
      dbms_output.put_line('this is a normal error');
end;
/

select invalid_cursor_f();
    

41.13.2.2. CASE_NOT_FOUND

case语句,如果when子句中没有包含必要的条件分支,就会触发CASE_NOT_FOUND异常。简单情况下有simple_case_statement,搜索情况下有search_case_statement。

例如,简单情况下的simple_case_statement。

create or replace function case_not_found_f return int as
  grade CHAR(1);
BEGIN
  grade := 'G';
  
  CASE grade
    WHEN 'A' THEN DBMS_OUTPUT.PUT_LINE('Excellent');
    WHEN 'B' THEN DBMS_OUTPUT.PUT_LINE('Very Good');
    WHEN 'C' THEN DBMS_OUTPUT.PUT_LINE('Good');
    WHEN 'D' THEN DBMS_OUTPUT.PUT_LINE('Fair');
    WHEN 'F' THEN DBMS_OUTPUT.PUT_LINE('Poor');
  END CASE;
EXCEPTION
  WHEN CASE_NOT_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('No such grade');
  return 1;
END;
/

select case_not_found_f();
    

例如,搜索情况下的search_case_statement。

create or replace function case_not_found_f return int as
  grade CHAR(1);
BEGIN
  grade := 'G';
  
  CASE
    WHEN grade = 'A' THEN DBMS_OUTPUT.PUT_LINE('Excellent');
    WHEN grade = 'B' THEN DBMS_OUTPUT.PUT_LINE('Very Good');
    WHEN grade = 'C' THEN DBMS_OUTPUT.PUT_LINE('Good');
    WHEN grade = 'D' THEN DBMS_OUTPUT.PUT_LINE('Fair');
    WHEN grade = 'F' THEN DBMS_OUTPUT.PUT_LINE('Poor');
  END CASE;
EXCEPTION
  WHEN CASE_NOT_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('No such grade');
  return 1;
END;
/

select case_not_found_f();
    

41.13.2.3. DUP_VAL_ON_INDEX

当向唯一索引对应的列插入重复值时触发。

例如:

--preconditional
CREATE TABLE emp_name AS SELECT id, name FROM employees;

CREATE UNIQUE INDEX empname_ix ON emp_name (id);

create or replace procedure dup_val_on_index_p as
  emp_id        NUMBER(10);
  emp_name  VARCHAR2(100);
BEGIN
  SELECT id, name  INTO emp_id, emp_name FROM employees WHERE id = 22091401;
 
  INSERT INTO emp_name (id, name) VALUES (emp_id, emp_name);
 
EXCEPTION
  WHEN DUP_VAL_ON_INDEX THEN
    ROLLBACK;
    DBMS_OUTPUT.PUT_LINE('Inserts were rolled back');
END;
/

call dup_val_on_index_p();
    

41.13.2.4. NOT_DATA_FOUND

当执行select into时没有返回任何行时触发。

例如:

-- no_data_found
-- select into
-- number of record selected is zero
create or replace function no_data_found_f return int as
  v_emp int;
begin
  select id into v_emp from employees where id = 22091501;
EXCEPTION
  WHEN no_data_found THEN
    DBMS_OUTPUT.PUT_LINE('No data found');
  return 1;
end;
/

select no_data_found_f();
    

41.13.2.5. TOO_MANY_ROWS

当执行select into语句时,如果返回超过一行,则会触发TOO_MANY_ROWS异常。

例如:

create or replace function too_many_rows_f return int as
  v_emp int;
begin
  select id into v_emp from employees;

EXCEPTION
  WHEN no_data_found THEN
    DBMS_OUTPUT.PUT_LINE('No data found');
  WHEN too_many_rows THEN
    DBMS_OUTPUT.PUT_LINE('Too Many Rows');

  return 1;
end;
/

select too_many_rows_f();
    

41.13.2.6. 登录被拒绝

你可以使用raise来抛出预定义的异常,并在异常后处理问题。(在pl/sql中不会触发,但在LightDB中会触发)

例如:

create or replace function login_denied_f return int as
begin
  raise login_denied;
exception
  when login_denied then
    dbms_output.put_line('this is a login_denied message.. good luck the next time');
  return 1;
end;
/

select login_denied_f();
    

41.13.2.7. 零除错误

当除数为0时,被除数不为0的数字执行除法的事件就会触发该异常。

例如:

create or replace function zero_divide_f return int as
    a int := 10;
    b int := 0;
begin
    a := a / b;
    exception 
      when zero_divide then
        dbms_output.put_line('this is a normal error');
    return 1;
end;
/

select zero_divide_f();

Tip

1. 错误处理不支持处理问题摘要:

  • 匿名块中的动态SQL包含提交和回滚的匿名块,不支持;

  • 存储过程中的动态SQL包含提交和回滚的匿名块,不支持;

  • 包中的存储过程以及匿名块中的动态SQL包含提交和回滚,不支持。

2. 在包中,函数、存储过程和匿名块的执行顺序有问题;

3. 目前pl/sql不支持保存点。

41.13.3. 显式抛出异常

为了显式抛出异常,请使用RAISE语句或RAISE_APPLICATION_ERROR过程。

RAISE语句不仅支持抛出内置标准异常,而且支持抛出经EXCEPTION_INIT命令初始化过的自定义异常变量。

你只能从存储子程序或方法中调用RAISE_APPLICATION_ERROR过程。 通常,你调用这个过程来抛出一个用户定义的异常,并将它的错误代码和错误消息返回给调用者。

调用RAISE_APPLICATION_ERROR时,请使用以下语法:

RAISE_APPLICATION_ERROR (error_code, message[, {TRUE | FALSE}]);

错误代码是-20000..-20999范围内的整数。如果指定TRUE,PL/oraSQL将不会打印消息。否则,PL/oraSQL将打印消息。默认为FALSE

41.13.4. 支持DBMS_UTILITY内置包format_error_backtrace函数

DBMS_UTILITY.format_error_backtrace 函数是记录异常时必须调用的关键子程序。 它返回一个字符串,将错误追溯到引发错误的行! 追溯函数总是只追溯到最近引发的异常。 错误允许返回的最大长度为10000。

使用DBMS_UTILITY.format_error_backtrace函数的示例:

--预处理条件
create table test_error_stack(id number primary key);
insert into test_error_stack values (1);
create table t_errlog(operate_error_msg varchar(1000));

create or replace procedure pr_error_code_test1 is
    v_backtrace    varchar(1000);
    v_error        varchar(1000); 
begin

    INSERT INTO test_error_stack values (2);
    INSERT INTO test_error_stack values (1);
    commit;
    exception when others then
    insert into t_errlog(operate_error_msg) select to_char(dbms_utility.format_error_backtrace);
    commit;
    rollback;
    end pr_error_code_test1;
/

call pr_error_code_test1();
select * from t_errlog;

--仅使用函数调用
select to_char(dbms_utility.format_error_backtrace)
    

41.13.5. 支持 SQLCODE

支持在抛出异常时获取整数错误代码。 非异常块下获取的错误代码为数值0。 该整数的错误代码可以转换为长度为5的字符串错误代码。 转换函数 SQLCODE_TO_SQLSTATEorafce. 长度为5的字符串错误码在 Appendix F.

例如:

declare
    a text;
    b int;
begin
    raise exception 'sqlcode test';
exception
    when others then
    a := sqlstate;
    b := sqlcode;
    raise notice 'sqlstate: %, sqlcode: %, sqlcode: %', a, b, sqlcode;
end;
/
select SQLCODE_TO_SQLSTATE(16777248);
    

41.13.6. 支持 SQLERRM

支持在抛出异常时获取错误消息。 非异常块下当获取的错误代码为数值0时,错误消息为ORA-0000。