异常处理

本章节主要介绍在 DM 数据库中异常的处理方式。

适用场景

软件 版本
操作系统 Redhat 7 及以上版本
DM 数据库 DM 8.0 及以上版本
CPU 架构 x86、ARM、龙芯、飞腾等国内外主流 CPU

概述

异常可能来自于设计缺陷、代码失误、用户动作导致的错误、硬件错误等,通常我们都无法预知所有可能出现的异常。

异常处理的作用

你不需要知道在任何地方可能发生的任何错误,你只需要使用异常处理器来处理特定的以及未知的错误,异常处理代码与业务代码分离,所以程序容易阅读。

异常的结构

任何 PL/SQL 块都可以有一个异常处理块,其中可以包含一个或多个异常处理器。

# ex_name_n :异常的名称, statements_1 : 一条或者多条异常处理语句

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;

异常的分类

  • 达梦数据库内部定义的异常 (Internally defined),隐式的抛出。通常有对应的错误代码,但没有对应的异常名称(但可自行关联)。
# 以下代码会抛出数字精度超限的异常,手动将其与一个自定义的异常名称绑定。
# 该异常报错如下 [-6170]:Precision of column [ID] is out of range.

CREATE TABLE y1(id NUMBER(2));
  
DECLARE
  E_PRECISION_OVERFLOW EXCEPTION;
  PRAGMA EXCEPTION_INIT (E_PRECISION_OVERFLOW, -6170);
  tmp NUMBER(3) := 999;
BEGIN
  INSERT INTO y1(id) VALUES(tmp);
EXCEPTION
  WHEN E_PRECISION_OVERFLOW THEN
    DBMS_OUTPUT.PUT_LINE('Precision of column [ID] is out of range.');
END;
/
  • 达梦数据库系统内部预定义的异常 (Predefined),在 DMSQL 运行时引擎发现某个错误,隐式的抛出异常。比如 ZERO_DIVIDE、NO_DATA_FOUND。
# 引发除数为零的内部定义的异常。

DECLARE 
  x number := 20;
  y number := 0; 
BEGIN
  DBMS_OUTPUT.PUT_LINE(x/y);
END;
/
  
[-6103]:Divide zero.
----------------------------------
  
# 捕获并处理除数为零异常

DECLARE 
  x number := 20;
  y number := 0; 
BEGIN
  DBMS_OUTPUT.PUT_LINE(x/y);
  EXCEPTION 
      WHEN ZERO_DIVIDE THEN
          DBMS_OUTPUT.PUT_LINE('除数为零');
END;
/
  
# 处理一个 NO_DATA_FOUND 异常

DECLARE
  v_name nation.n_name%TYPE;
BEGIN
  SELECT n_name INTO v_name FROM nation WHERE n_nationkey=100;
  DBMS_OUTPUT.PUT_LINE('Nation_key is 100    ' || 'Nation_name is ' || v_name);
  EXCEPTION
      WHEN NO_DATA_FOUND THEN
          DBMS_OUTPUT.PUT_LINE('There is no nation that key is 100');
END;
  
# 上面的示例捕获的就是 [-7065]:No data found 异常
注意

达梦服务器内部异常可参考《DM 程序员手册》附录 1 和 V$ERR_INFO 视图,手册位于数据库安装路径 /dmdbms/doc 文件夹。

用户自定义的异常 (User-defined): 针对当前的应用程序。比如计算员工的工资,如果出现零和负数,那就是异常,需要被抛出。此种异常系统不会自动抛出。

异常的声明

  • 目的:对于你能够预知的可能发生的错误,为其定义名称供异常处理使用即(when ex_name then 结构)。对于已近预定义的异常,无需声明,可直接使用已定义的异常名称。
  • 声明语法
ex_name EXCEPTION
  • 关联异常与错误代码
pragma exception_init(ex_name, error_code)

以上语法是与 Oracle 兼容的,达梦数据库还提供了一种声明语法, 使用 EXCEPTION FOR 关键字将异常名称与错误号绑定。

ex_name EXCEPTION FOR ex_code , ex_description;
  • 举例说明如下:
DECLARE
  E_PRECISION_OVERFLOW EXCEPTION;
  PRAGMA EXCEPTION_INIT (E_PRECISION_OVERFLOW, -1438);
  tmp_num NUMBER(5) :=12345;
BEGIN
  INSERT INTO scott.emp (empno, ename) VALUES (tmp_num, 'new_emp');
  DBMS_OUTPUT.PUT_LINE('tmp_num is:' || tmp_num);
EXCEPTION
  WHEN E_PRECISION_OVERFLOW THEN
    DBMS_OUTPUT.PUT_LINE(q'[Tmp_num's presicsion is largert than EMPNO's!]');
END;
/

用户自定义的异常,与其关联的错误码必须是 -20000 到 -30000 间的负数值。

抛出异常

  • raise:将当前捕获的异常原样抛出。不需要异常名称,只能在异常单元的 when 语句中使用,传播异常,在异常单元中再次抛出一个相同的异常。
  • raise ex_name:抛出名为 ex_name 的异常。

raise_application_error

raise_application_error(error_code, message)

只能在命名子程序内使用此过程,系统内部异常有系统自动隐式抛出,也可以使用 raise 显示抛出,用 raise 抛出预定义的异常。

# 主动监测并手动抛出内部异常

DECLARE 
  x number := 20;
  y number := 0; 
BEGIN
  IF y == 0 THEN
    RAISE(ZERO_DIVIDE);
  ELSE
    DBMS_OUTPUT.PUT_LINE(x/y);
  END IF;
  EXCEPTION 
      WHEN ZERO_DIVIDE THEN
          DBMS_OUTPUT.PUT_LINE('除数为零');
END;
/

用 raise 抛出用户自定义的异常:

DECLARE
  TYPE Worker IS TABLE OF NUMBER INDEX BY VARCHAR(20);
  salary_too_high EXCEPTION;
  max_salary NUMBER := 20000;
  emp Worker;
  ind VARCHAR(20);
BEGIN
  FOR i IN (SELECT * FROM employee) LOOP
    IF i.salary > max_salary THEN
      emp(i.employee_name) := i.salary;
    END IF; 
  END LOOP;
  IF emp.count > 0 THEN
    RAISE salary_too_high;
  END IF; 
EXCEPTION
  WHEN salary_too_high THEN
      ind := emp.FIRST;
      WHILE ind IS NOT NULL LOOP
         DBMS_OUTPUT.PUT_LINE(ind || ' Salary ' || emp(ind) ||' is out of range.');
         ind := emp.NEXT(ind);
      END LOOP;
END;
/ 

用 raise_application_error 抛出异常:

# 只能在一个存储子程序中调用,调用这个过程抛出一个用户自定义的异常并返回错误代码和信息给调用方

CREATE OR REPLACE PROCEDURE account_status (
  due_date DATE,
  today    DATE
)
IS
BEGIN
  IF due_date < today THEN
    RAISE_APPLICATION_ERROR(-20001, 'Account past due.');
  END IF;
END;
/
 
DECLARE
  past_due  EXCEPTION;
  PRAGMA EXCEPTION_INIT (past_due, -20001);
BEGIN
  account_status (TO_DATE('01-JUL-2010', 'DD-MON-YYYY'),
                  TO_DATE('09-JUL-2010', 'DD-MON-YYYY'));

EXCEPTION
  WHEN past_due THEN
    DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM));
END;
/

编译的时候就会报错,异常捕获的是运行时异常。

异常的传递

  • 异常 A 发生时,会在当前块中处理,不会传播到外层。

异常A

  • 异常 B 发生时,当前块中没有对应的处理器,则会传播到外层。

异常B

  • 异常 C 发生时,当前块和所有的外层块都没有与之匹配的异常处理器,则异常会传递给主机主机环境。

异常C

  • 声明中发生异常,块中的异常捕获模块是无法捕获到的,那么如何捕获声明中的异常呢?
# 下面的方式值错误异常无法被异常处理器捕获

DECLARE
  salary_limit CONSTANT NUMBER(3) := 5000;  -- Maximum value is 999
BEGIN
  NULL;

EXCEPTION
  WHEN VALUE_ERROR THEN
    DBMS_OUTPUT.PUT_LINE('Exception raised in declaration.');
END;
/
# 改下上面的额过程,可以捕获到值错误异常。

BEGIN 
  DECLARE
    salary_limit CONSTANT NUMBER(3) := 5000;
  BEGIN
    NULL;
  END;

EXCEPTION
  WHEN VALUE_ERROR THEN
    DBMS_OUTPUT.PUT_LINE('Exception raised in declaration.');
END;
/

异常跟踪

DMSQL 块中发生异常时,使用如下方法可以定位发生异常的行。

DECLARE
  v_num NUMBER(11,10) := 1;
BEGIN
  BEGIN
    v_num := 55;
  EXCEPTION
    WHEN others THEN
      DBMS_OUTPUT.PUT_LINE('Error line=' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
      v_num := v_num / 0;
  END; 
EXCEPTION
  WHEN others THEN
    DBMS_OUTPUT.PUT_LINE('Error line=' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END;
/

# 输出如下

Error line=-6149: anonymous block line 5
Error line=-6103: anonymous block line 9

FORALL 中的异常

  • 立即处理 FORALL 异常
drop table emp;
create table emp as select employee_id emp_no, employee_name emp_name, department_id dept_id from employee where rownum<=5;
alter table emp modify dept_id number(3);
select * from emp;
commit;

DECLARE
  TYPE NumList IS TABLE OF NUMBER;
  dno NumList := NumList(201,202,2003,204,2005);
  eno NumList := NumList(1001,1002,1003,1004,1005);
BEGIN
  FORALL v IN eno.FIRST .. eno.LAST
    UPDATE emp SET dept_id=dno(v) WHERE emp_no=eno(v);
EXCEPTION
  WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE(SQLERRM);
      DBMS_OUTPUT.PUT_LINE('Total rows updated:' || SQL%ROWCOUNT);
      COMMIT;
      RAISE;
END;
/

# 自定义报错

Precision of column [DEPT_ID] is out of range
Total rows updated:0

# 抛出到上层系统报错

[-6170]:Precision of column [DEPT_ID] is out of range.

# 由于员工号为 1003 的部门号对应的需要把员工号设置成 2003,超过了数字的设定精度报错,直接处理退出了。结果如下:

EMP_NO      EMP_NAME  DEPT_ID
----------- --------- -------
1001        马学铭     201
1002        程擎武     202
1003        郑吉群     103
1004        陈仙       104
1005        金纬       105
  • 利用 SAVE EXCEPTIONS 关键字,跳过异常可继续执行。
drop table emp;
create table emp as select employee_id emp_no, employee_name emp_name, department_id dept_id from employee where rownum<=5;
alter table emp modify dept_id number(3);
select * from emp;
commit;

# 执行上述代码还原初始环境

DECLARE
  TYPE NumList IS TABLE OF NUMBER;
  dno NumList := NumList(201,202,2003,204,2005);
  eno NumList := NumList(1001,1002,1003,1004,1005);
BEGIN
  FORALL v IN eno.FIRST .. eno.LAST SAVE EXCEPTIONS
    UPDATE emp SET dept_id=dno(v) WHERE emp_no=eno(v);
EXCEPTION
  WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE(SQLERRM);
      DBMS_OUTPUT.PUT_LINE('Total rows updated:' || SQL%ROWCOUNT);
      COMMIT;
      RAISE;
END;
/

# 报错如下,与不加 SAVE EXCEPTIONS 关键字有区别
数组 DML 出错
Total rows updated:3  # 提示更新的行数

[-25000]:数组DML出错.


# 员工号 1003 引发的异常并没有影响到 1004 号员工的更新,结果如下:

EMP_NO      EMP_NAME  DEPT_ID
----------- --------- -------
1001        马学铭     201
1002        程擎武     202
1003        郑吉群     103
1004        陈仙       204
1005        金纬       105
  • SQL%BULK_EXCEPTIONS 配合SAVE EXCEPTIONS 关键字输出详细信息
drop table emp;
create table emp as select employee_id emp_no, employee_name emp_name, department_id dept_id from employee where rownum<=5;
alter table emp modify dept_id number(3);
select * from emp;
commit;

# 执行上述代码还原初始环境

DECLARE
  TYPE NumList IS TABLE OF NUMBER;
  dno NumList := NumList(201,202,2003,204,2005);
  eno NumList := NumList(1001,1002,1003,1004,1005);
  dml_err EXCEPTION;
  PRAGMA EXCEPTION_INIT(dml_err, -25000);
BEGIN
  FORALL v IN eno.FIRST .. eno.LAST SAVE EXCEPTIONS
    UPDATE emp SET dept_id=dno(v) WHERE emp_no=eno(v);
EXCEPTION
  WHEN dml_err THEN
    FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
    LOOP
      DBMS_OUTPUT.PUT_LINE(SQLERRM (-1*(SQL%BULK_EXCEPTIONS(i).ERROR_CODE)));
      DBMS_OUTPUT.PUT_LINE('Wrong statement : ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX);
      DBMS_OUTPUT.PUT_LINE('Wrong dept : ' || dno(SQL%BULK_EXCEPTIONS(i).ERROR_INDEX));
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Total rows updated:' || SQL%ROWCOUNT);
    COMMIT;
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('UNKNOW');
    DBMS_OUTPUT.PUT_LINE(SQLERRM);
    RAISE;
END;
/

# 执行后报错信息如下:
[-6170]:Precision of column [] is out of range  # 出错原因
Wrong statement : 3  # 第几行出错
Wrong dept : 2003    # 出错行对应的dept_id值
[-6170]:Precision of column [] is out of range
Wrong statement : 5
Wrong dept : 2005
Total rows updated:3 # 完成了 3 行的更新任务

使用 SAVE EXCEPTIONS 关键字并结合 SQL%BULK_EXCEPTIONS 游标属性,可以在 FORALL 语句结束后处理异常,而不是立刻终止,这种方式更加的友好。

异常处理的思考

  • 构建一个有效的错误管理架构。
  • 确定异常管理策略。
  • 是否需要在每个 PL/SQL 块中都包含一个异常处理单元。
  • 是否需要在最外层或者最顶层块中包含一个异常处理单元。
  • 当错误发生是,如何管理事务。
  • 对不同类的异常的标准化处理 deliberate,unfortunate,unexcepted。
  • 组织好对应用而言专有的错误代码的使用,可以考虑使用配置表配置。
  • 使用标准化的错误管理程序。通过一个 package 包,里面包含标准化的错误处理方法。
  • 统一的日志表加上统一的日志处理接口。
  • 创建通用错误处理的标准模板。
  • 既使用错误检查代码,也使用异常处理器。
  • 在异常可能出现的地方,使用异常处理器。
  • 为错误记录日志,从而是开发人员能够分析是什么造成了这个问题。
  • 当数据库处于你所期望的状态之外时,尽量使得你的程序照样能正常工作。
  • 尽可能在有明确命名的异常处理中处理异常,而不是 OTHERS 中去处理。
  • 异常处理器中应输出调试信息。假如调试信息要保存到另外独立的表中,使用自治事务来编写。
  • 无论在那个异常处理中,是提交、回滚还是继续,都应该谨慎。
  • OTHERS 分支的最后一个语句,应当抛出错误(RAISE_APPLICATION_ERROR 或者 RAISE)。

内置的错误函数和游标属性

  • SQLCODE:返回代码中最后一次抛出的错误代码。如果没有任何错误,则返回 0。
  • SQLERRM:返回某个错误代码对应的错误信息。如果没有给 SQLERRM 传递错误代码,就会返回 SQLCODE 代码对应的消息,最大长度 512 个字节。
  • FORALL 语句隐含的 SQL 游标属性。
SQL%FOUND 
如果 SQL 语句的最后一次执行修改了一行或者多行,则返回 TRUE
  
SQL%NOTFOUND
如果 DML 语句没有修改任何行则返回 TRUE
  
SQL%ROWCOUNT
返回 SQL 语句全部执行所处理的行数,不仅仅是最后一条语句
  
SQL%BULK_ROWCOUNT 
针对 FORALL 设计的,在语义上属于关联数组。数据库在个集合的第N个元素所保存的是 FORALL 的插入、更新、删除  或者合并的第 N 次执行所处理的行数,如果没有处理任何行,则这个集合的第 N 行包含的值是 0。

SQL%BULK_EXCEPTIONS
是带有 SAVE EXCEPTIONS 子句的 FORALL 语句引发的每个异常信息。

参考文档

更多 SQL 语言使用说明,请参考《DM_SQL 语言使用手册》,手册位于数据库安装路径 /dmdbms/doc 文件夹下。如有其他问题,请在社区内咨询。

微信扫码
分享文档
扫一扫
联系客服