Skip to content

PL/SQL 异常处理:预定义异常、用户自定义异常

程序总会出错。

网络断了、数据不存在、用户输入非法……这些"意外"如果处理不好,轻则报错,重则崩溃。

Oracle 的异常处理机制,让你的 PL/SQL 程序即使遇到错误,也能优雅地应对。

今天,我们来深入学习 Oracle 的异常处理。


什么是异常?

异常是程序执行过程中发生的中断正常流程的错误

┌─────────────────────────────────────────────────────────────┐
│                    异常处理流程                              │
│                                                             │
│  BEGIN                                                      │
│      -- 正常执行                                            │
│      SELECT ... INTO ...                                    │
│                                                             │
│      -- 异常发生                                            │
│      DBMS_OUTPUT.PUT_LINE(x / 0);  -- 除零错误             │
│                                                             │
│  EXCEPTION                                                  │
│      WHEN ZERO_DIVIDE THEN  -- 捕获异常                     │
│          -- 处理异常                                        │
│          DBMS_OUTPUT.PUT_LINE('除数不能为零');              │
│                                                             │
│      WHEN OTHERS THEN      -- 兜底处理                      │
│          -- 其他所有异常                                    │
│  END;                                                       │
└─────────────────────────────────────────────────────────────┘

异常处理机制

EXCEPTION 的作用

EXCEPTION 块用于捕获和处理异常,防止程序崩溃,并提供有意义的错误信息。

sql
BEGIN
    -- 可能出错的代码
    SELECT salary INTO v_salary
    FROM employees
    WHERE employee_id = 9999;  -- 员工不存在
    
    DBMS_OUTPUT.PUT_LINE('工资: ' || v_salary);
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('员工不存在');
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('发生错误: ' || SQLERRM);
END;
/

异常处理的原则

原则说明
精确捕获尽量捕获具体异常,不要只用 WHEN OTHERS
记录日志捕获异常后记录详细信息,便于排查
不要静默不要 WHEN OTHERS THEN NULL,会隐藏错误
适当抛出必要时重新抛出异常,让调用者处理

预定义异常

Oracle 提供了一系列预定义异常,无需声明即可使用。

常用预定义异常

sql
-- 1. NO_DATA_FOUND:SELECT INTO 没有返回数据
BEGIN
    SELECT salary INTO v_salary
    FROM employees
    WHERE employee_id = 9999;  -- 不存在
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('员工不存在');
END;
/

-- 2. TOO_MANY_ROWS:SELECT INTO 返回多行
BEGIN
    SELECT first_name INTO v_name
    FROM employees
    WHERE department_id = 50;  -- 返回多人
EXCEPTION
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('返回数据过多,请使用游标');
END;
/

-- 3. DUP_VAL_ON_INDEX:唯一索引冲突
BEGIN
    INSERT INTO employees (employee_id, email) VALUES (100, 'test@test.com');
    INSERT INTO employees (employee_id, email) VALUES (101, 'test@test.com');  -- 邮箱重复
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        DBMS_OUTPUT.PUT_LINE('邮箱已被使用');
END;
/

-- 4. INVALID_NUMBER:类型转换失败
BEGIN
    SELECT salary INTO v_salary
    FROM employees
    WHERE employee_id = TO_NUMBER('ABC');  -- 不是数字
EXCEPTION
    WHEN INVALID_NUMBER THEN
        DBMS_OUTPUT.PUT_LINE('无效的数字');
END;
/

-- 5. ZERO_DIVIDE:除数为零
BEGIN
    v_result := 100 / 0;
EXCEPTION
    WHEN ZERO_DIVIDE THEN
        DBMS_OUTPUT.PUT_LINE('除数不能为零');
END;
/

-- 6. VALUE_ERROR:数值或字符串错误
BEGIN
    v_name VARCHAR2(5);
    v_name := '这是一个很长的字符串';  -- 超过长度
EXCEPTION
    WHEN VALUE_ERROR THEN
        DBMS_OUTPUT.PUT_LINE('值超出范围');
END;
/

-- 7. LOGIN_DENIED:登录失败
BEGIN
    NULL;  -- 模拟登录验证
EXCEPTION
    WHEN LOGIN_DENIED THEN
        DBMS_OUTPUT.PUT_LINE('用户名或密码错误');
END;
/

预定义异常一览

异常名称错误代码说明
NO_DATA_FOUNDORA-01403SELECT INTO 没返回数据
TOO_MANY_ROWSORA-01422SELECT INTO 返回多行
DUP_VAL_ON_INDEXORA-00001唯一索引冲突
INVALID_NUMBERORA-01722字符串转数字失败
ZERO_DIVIDEORA-01476除数为零
VALUE_ERRORORA-06502数值或字符串错误
LOGIN_DENIEDORA-01017登录失败
NOT_LOGGED_ONORA-01012未连接数据库
STORAGE_ERRORORA-06500内存错误
PROGRAM_ERRORORA-06501内部程序错误
CURSOR_ALREADY_OPENORA-06511游标已打开
INVALID_CURSORORA-01001无效的游标操作

非预定义异常

非预定义异常是 Oracle 错误,但没有预定义名称。需要先声明异常,再将异常与错误代码关联。

声明和使用

sql
DECLARE
    -- 声明异常(无参数)
    e_parent_not_found EXCEPTION;
    -- 将异常与错误代码关联
    PRAGMA EXCEPTION_INIT(e_parent_not_found, -02291);
BEGIN
    -- 外键约束错误
    INSERT INTO employees (employee_id, department_id)
    VALUES (9999, 9999);  -- 部门不存在
EXCEPTION
    WHEN e_parent_not_found THEN
        DBMS_OUTPUT.PUT_LINE('父表记录不存在');
END;
/

常用错误代码

错误代码说明
-02291外键约束不存在
-02292违反子表约束
-02421主键/唯一键冲突
-04091表正在被修改
-1441无效的日期

用户自定义异常

用户自定义异常是根据业务规则定义的异常,需要手动声明和抛出。

基本用法

sql
DECLARE
    -- 声明异常
    e_invalid_salary EXCEPTION;
    e_emp_not_found EXCEPTION;
    
    v_salary NUMBER(10, 2);
BEGIN
    -- 业务验证
    v_salary := -1000;
    
    IF v_salary < 0 THEN
        -- 抛出异常
        RAISE e_invalid_salary;
    END IF;
    
    UPDATE employees SET salary = v_salary WHERE employee_id = 100;
    
    IF SQL%ROWCOUNT = 0 THEN
        RAISE e_emp_not_found;
    END IF;
    
EXCEPTION
    WHEN e_invalid_salary THEN
        DBMS_OUTPUT.PUT_LINE('错误:工资不能为负数');
        ROLLBACK;
    WHEN e_emp_not_found THEN
        DBMS_OUTPUT.PUT_LINE('错误:员工不存在');
        ROLLBACK;
END;
/

RAISE vs RAISE_APPLICATION_ERROR

对比项RAISERAISE_APPLICATION_ERROR
适用场景用户自定义异常需要指定错误码的场景
错误代码异常声明决定手动指定(-20000 到 -20999)
错误消息异常消息手动指定
调用栈包含包含
sql
-- RAISE:抛出已声明的异常
DECLARE
    e_custom EXCEPTION;
BEGIN
    RAISE e_custom;
END;
/

-- RAISE_APPLICATION_ERROR:抛出带错误码的异常
BEGIN
    IF some_condition THEN
        RAISE_APPLICATION_ERROR(-20001, '业务错误:余额不足');
    END IF;
END;
/

完整示例:订单处理

sql
CREATE OR REPLACE PROCEDURE process_order(
    p_order_id IN NUMBER,
    p_customer_id IN NUMBER,
    p_product_id IN NUMBER,
    p_quantity IN NUMBER
) IS
    -- 声明异常
    e_insufficient_stock EXCEPTION;
    e_invalid_quantity EXCEPTION;
    e_order_not_found EXCEPTION;
    
    -- 变量
    v_stock NUMBER;
    v_price NUMBER;
    v_customer_balance NUMBER;
BEGIN
    -- 验证数量
    IF p_quantity <= 0 THEN
        RAISE e_invalid_quantity;
    END IF;
    
    -- 检查库存
    SELECT stock_quantity INTO v_stock
    FROM products
    WHERE product_id = p_product_id;
    
    IF v_stock < p_quantity THEN
        RAISE e_insufficient_stock;
    END IF;
    
    -- 检查客户余额
    SELECT balance INTO v_customer_balance
    FROM customers
    WHERE customer_id = p_customer_id;
    
    -- 获取价格
    SELECT price INTO v_price
    FROM products
    WHERE product_id = p_product_id;
    
    IF v_customer_balance < v_price * p_quantity THEN
        RAISE_APPLICATION_ERROR(-20001, '客户余额不足');
    END IF;
    
    -- 更新库存
    UPDATE products
    SET stock_quantity = stock_quantity - p_quantity
    WHERE product_id = p_product_id;
    
    -- 创建订单
    INSERT INTO orders (order_id, customer_id, product_id, quantity, order_date)
    VALUES (p_order_id, p_customer_id, p_product_id, p_quantity, SYSDATE);
    
    -- 更新客户余额
    UPDATE customers
    SET balance = balance - v_price * p_quantity
    WHERE customer_id = p_customer_id;
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('订单处理成功');
    
EXCEPTION
    WHEN e_invalid_quantity THEN
        DBMS_OUTPUT.PUT_LINE('错误:数量必须大于0');
        ROLLBACK;
    WHEN e_insufficient_stock THEN
        DBMS_OUTPUT.PUT_LINE('错误:库存不足,当前库存: ' || v_stock);
        ROLLBACK;
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('错误:产品或客户不存在');
        ROLLBACK;
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('未知错误: ' || SQLERRM);
        ROLLBACK;
        RAISE;  -- 重新抛出,让调用者知道
END process_order;
/

SQLCODE 和 SQLERRM

获取错误信息

sql
EXCEPTION
    WHEN OTHERS THEN
        -- 错误代码(负数)
        DBMS_OUTPUT.PUT_LINE('错误代码: ' || SQLCODE);
        
        -- 错误消息(最多 512 字符)
        DBMS_OUTPUT.PUT_LINE('错误消息: ' || SQLERRM);
        
        -- 完整错误消息(带正数错误代码)
        DBMS_OUTPUT.PUT_LINE('完整错误: ' || SQLERRM(SQLCODE));
END;
/

SQLERRM 详解

sql
BEGIN
    NULL;
EXCEPTION
    WHEN OTHERS THEN
        -- SQLCODE = 0:无错误
        -- SQLCODE = 1:用户自定义异常(RAISE)
        -- SQLCODE = 100:NO_DATA_FOUND
        -- SQLCODE < 0:系统错误
        
        -- SQLERRM(-942):获取特定错误码的消息
        DBMS_OUTPUT.PUT_LINE(SQLERRM(-942));  -- ORA-00942: table or view does not exist
END;
/

异常传播

子程序中的异常传播

sql
CREATE OR REPLACE PROCEDURE inner_proc IS
BEGIN
    RAISE_APPLICATION_ERROR(-20001, '内部错误');
EXCEPTION
    WHEN OTHERS THEN
        -- 处理后重新抛出
        RAISE;
END inner_proc;
/

CREATE OR REPLACE PROCEDURE outer_proc IS
BEGIN
    inner_proc;  -- 调用内部过程
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('捕获到异常: ' || SQLERRM);
END outer_proc;
/

RAISE 语句

sql
-- 在异常处理中重新抛出异常
EXCEPTION
    WHEN OTHERS THEN
        -- 记录日志
        INSERT INTO error_log (...) VALUES (...);
        COMMIT;
        -- 重新抛出
        RAISE;
END;
/

异常处理最佳实践

1. 不要吞掉异常

sql
-- 不好:静默处理
EXCEPTION
    WHEN OTHERS THEN NULL;  -- 错误被隐藏!

-- 好:记录并处理或抛出
EXCEPTION
    WHEN OTHERS THEN
        log_error(SQLCODE, SQLERRM);
        RAISE;
END;
/

2. 按顺序捕获

sql
-- 好:先具体后一般
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        -- 先处理具体异常
        DBMS_OUTPUT.PUT_LINE('找不到数据');
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('数据过多');
    WHEN OTHERS THEN
        -- 最后兜底
        DBMS_OUTPUT.PUT_LINE('其他错误');
END;
/

3. 使用 WHEN OTHERS THEN

sql
-- 好:在异常块中放在最后
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        handle_not_found;
    WHEN DUP_VAL_ON_INDEX THEN
        handle_duplicate;
    WHEN OTHERS THEN
        log_error;
        RAISE;
END;

4. 记录详细日志

sql
CREATE TABLE error_log (
    error_id NUMBER PRIMARY KEY,
    error_code NUMBER,
    error_message VARCHAR2(2000),
    error_backtrace VARCHAR2(4000),
    user_name VARCHAR2(50),
    program_name VARCHAR2(100),
    error_time DATE
);

EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO error_log (
            error_id, error_code, error_message, user_name, program_name, error_time
        ) VALUES (
            error_seq.NEXTVAL,
            SQLCODE,
            SQLERRM,
            USER,
            $$PLSQL_UNIT,
            SYSDATE
        );
        
        -- 获取调用栈
        DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
        
        COMMIT;
        RAISE;
END;
/

面试高频问题

Q1: 如何抛出自定义异常?

  1. 声明异常:e_my_exception EXCEPTION;
  2. 抛出异常:RAISE e_my_exception;
  3. 捕获异常:WHEN e_my_exception THEN ...

Q2: RAISE 和 RAISE_APPLICATION_ERROR 的区别?

  • RAISE:抛出已声明的异常,错误代码由声明决定
  • RAISE_APPLICATION_ERROR:抛出带自定义错误码(-20000~-20999)和消息的异常

Q3: WHEN OTHERS THEN NULL; 有什么问题?

会静默吞掉异常,隐藏错误,导致问题难以排查。应该记录日志或重新抛出。

Q4: SQLCODE 和 SQLERRM 的区别?

  • SQLCODE:返回错误代码(数字)
  • SQLERRM:返回错误消息(字符串)

总结

异常处理核心要点:

类型说明使用方式
预定义异常Oracle 内置直接使用
非预定义异常Oracle 错误,需关联PRAGMA EXCEPTION_INIT
用户自定义异常业务规则声明 + RAISE
sql
EXCEPTION
    WHEN specific_exception THEN
        -- 处理特定异常
    WHEN OTHERS THEN
        -- 记录日志
        log_error;
        -- 重新抛出
        RAISE;
END;

完善的异常处理是高质量 PL/SQL 代码的标志。


下一步

基于 VitePress 构建