Tổng Quan Về Exception Trong Oracle PL/SQL

AD
Admin
Junior

Tổng Quan Về Exception Trong Oracle PL/SQL & Thực Trạng Tại Các Dự Án Enterprise

Trong quá trình phát triển ứng dụng cơ sở dữ liệu Oracle từ các hệ thống Core Banking, ERP đến Billing Telecom, Stored Procedure là thành phần cốt lõi đảm nhận logic nghiệp vụ nặng (Heavy Business Logic). Tuy nhiên, một Stored Procedure viết ra chỉ chạy tốt trên "Happy Path" (khi dữ liệu đầu vào luôn đúng) mới chỉ hoàn thành 30% công việc. 70% độ bền vững của hệ thống phụ thuộc hoàn toàn vào cách chương trình xử lý khi phát sinh sự cố bất ngờ (Exception Handling).

Exception trong PL/SQL là một điều kiện cảnh báo hoặc một lỗi xuất hiện trong quá trình thực thi (Runtime Error). Khi một lỗi xảy ra (ví dụ: vi phạm ràng buộc Unique Constraint ORA-00001, chia cho số 0 ORA-01476, hoặc tài khoản hết tiền), Oracle Engine sẽ lập tức ngắt luồng thực thi bình thường và chuyển quyền điều khiển sang phần khối lệnh EXCEPTION.

Luồng xử lý tiêu chuẩn trong một PL/SQL Block:


+-------------------------------------------------------+
|  DECLARE: Khai báo biến, cursor, user exception       |
+-------------------------------------------------------+
                           |
                           v
+-------------------------------------------------------+
|  BEGIN: Thực thi SQL, DML, Logic nghiệp vụ            |
|  ---> Xảy ra sự cố (Run-time Error / RAISE)            |
+-------------------------------------------------------+
                           | (Ngắt luồng bình thường)
                           v
+-------------------------------------------------------+
|  EXCEPTION: Bắt lỗi, Rollback, Log error, Re-raise    |
+-------------------------------------------------------+
                           |
                           v
+-------------------------------------------------------+
|  END; Kết thúc an toàn / Trả lỗi về ứng dụng gọi      |
+-------------------------------------------------------+

Trên thực tế tại các dự án enterprise, thiếu hụt hoặc thiết kế sai chiến lược bắt lỗi dẫn đến hàng loạt hệ lụy nghiêm trọng:

  • Dữ liệu không nhất quán (Data Inconsistency): Một phần giao dịch DML được commit trong khi phần còn lại thất bại.
  • Che giấu nguyên nhân gốc (Swallowing Exceptions): Việc dùng các câu lệnh "bắt sạch cho qua" khiến team SysAdmin / DBA không thể biết tại sao bảng dữ liệu lại bị sai lệch.
  • Treo tài nguyên (Resource Leakage): Cursor mở ra nhưng không được đóng khi bị ném Exception giữa chừng, gây tràn bộ nhớ PGA.

Phân Loại Exception Trong Oracle PL/SQL

Oracle phân loại Exception thành 3 nhóm chính. Việc nắm rõ từng nhóm giúp lập trình viên viết code ngắn gọn, đúng cú pháp và dễ bảo trì.


                  +-----------------------------------+
                  |   Các Loại Exception Trong PL/SQL |
                  +-----------------------------------+
                                    |
        +---------------------------+---------------------------+
        |                           |                           |
        v                           v                           v
+---------------+           +---------------+           +---------------+
| System        |           | Non-Predefined|           | User-Defined  |
| Predefined    |           | System        |           | Custom        |
| (Có tên sẵn)  |           | (Mã ORA ẩn)   |           | (Nghiệp vụ)   |
+---------------+           +---------------+           +---------------+
| ORA-01403     |           | ORA-02292     |           | -20001        |
| (NO_DATA_FOUND|           | (FK constraint|           | Insufficient  |
| NO_DATA_NEEDED|           |  violation)   |           | Balance       |
+---------------+           +---------------+           +---------------+

System Predefined Exceptions (Ngoại Lệ Định Nghĩa Sẵn)

Oracle cung cấp sẵn khoảng hai mươi mã lỗi phổ biến nhất và gắn sẵn các tên định danh (Named Exceptions) trong package STANDARD. Khi các lỗi này xảy ra, bạn có thể bắt trực tiếp theo tên mà không cần khai báo thêm.

Tên ExceptionMã Lỗi ORANguyên Nhân Kích Hoạt
NO_DATA_FOUNDORA-01403Câu lệnh SELECT INTO không trả về dòng dữ liệu nào.
TOO_MANY_ROWSORA-01422Câu lệnh SELECT INTO trả về từ 2 dòng trở lên.
ZERO_DIVIDEORA-01476Thực hiện phép chia một số cho 0.
DUP_VAL_ON_INDEXORA-00001Chèn dữ liệu trùng lặp vào cột có Unique Index/Primary Key.
VALUE_ERRORORA-06502Lỗi kiểu dữ liệu, gán chuỗi quá dài vào biến có dung lượng nhỏ.
INVALID_CURSORORA-01001Thao tác trên một Cursor chưa được OPEN hoặc đã CLOSE.

Ví dụ thực tế:


CREATE OR REPLACE PROCEDURE sp_get_customer_phone (
    p_customer_id   IN  customers.customer_id%TYPE,
    v_phone_number  OUT customers.phone_number%TYPE
) AS
BEGIN
    SELECT phone_number 
    INTO v_phone_number
    FROM customers
    WHERE customer_id = p_customer_id;

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        v_phone_number := 'N/A';
        DBMS_OUTPUT.PUT_LINE('Cảnh báo: Không tìm thấy khách hàng ID: ' || p_customer_id);
    WHEN TOO_MANY_ROWS THEN
        v_phone_number := 'MULTIPLE';
        DBMS_OUTPUT.PUT_LINE('Lỗi dữ liệu: Khách hàng ID ' || p_customer_id || ' bị trùng bản ghi!');
END sp_get_customer_phone;
/

User-Defined Exceptions (Ngoại Lệ Tự Định Nghĩa Nghiệp Vụ)

Trong thực tế phát triển, có những quy tắc không phải là lỗi của hệ điều hành hay cú pháp SQL, mà là lỗi quy định nghiệp vụ (ví dụ: Số dư tài khoản < 50.000 VNĐ thì không cho rút, hoặc Tài khoản bị khóa không cho chuyển tiền).

Để xử lý, ta khai báo biến loại EXCEPTION ở phần DECLARE và dùng từ khóa RAISE để chủ động ném lỗi.


CREATE OR REPLACE PROCEDURE sp_withdraw_money (
    p_account_id  IN accounts.account_id%TYPE,
    p_amount      IN NUMBER
) AS
    v_balance           accounts.balance%TYPE;
    ex_insufficient_funds EXCEPTION; -- 1. Khai báo Exception nghiệp vụ
    ex_account_locked     EXCEPTION;
    v_status            accounts.status%TYPE;
BEGIN
    SELECT balance, status 
    INTO v_balance, v_status
    FROM accounts
    WHERE account_id = p_account_id
    FOR UPDATE; -- Khóa dòng để tránh Race Condition

    IF v_status = 'LOCKED' THEN
        RAISE ex_account_locked; -- 2. Kích hoạt ném lỗi
    END IF;

    IF v_balance < p_amount THEN
        RAISE ex_insufficient_funds; -- 2. Kích hoạt ném lỗi
    END IF;

    -- Trừ tiền nếu thỏa mãn điều kiện
    UPDATE accounts 
    SET balance = balance - p_amount 
    WHERE account_id = p_account_id;

    COMMIT;

EXCEPTION
    WHEN ex_account_locked THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20001, 'Tài khoản đang bị khóa. Không thể thực hiện giao dịch.');
        
    WHEN ex_insufficient_funds THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20002, 'Số dư không đủ để thực hiện giao dịch rút tiền.');
END sp_withdraw_money;

Non-Predefined System Exceptions (Ánh Xạ Mã ORA Khỏi Tên Mặc Định)

Oracle có hàng ngàn mã lỗi ORA-xxxxx nhưng chỉ có một số ít được gán tên sẵn. Ví dụ, lỗi vi phạm Foreign Key Constraint là ORA-02292 (Child record found) nhưng không có tên chuẩn như NO_DATA_FOUND.

Để code sạch sẽ và tránh phải so sánh chuỗi mã lỗi thủ công, ta dùng chỉ thị trình biên dịch PRAGMA EXCEPTION_INIT để liên kết một tên Exception tự đặt với một mã lỗi Oracle cụ thể.

CREATE OR REPLACE PROCEDURE sp_delete_department (
    p_dept_id IN departments.department_id%TYPE
) AS
    -- Khai báo tên Exception
    ex_fk_violation EXCEPTION;
    
    -- Ánh xạ tên Exception với mã lỗi ORA-02292
    PRAGMA EXCEPTION_INIT(ex_fk_violation, -02292);
BEGIN
    DELETE FROM departments 
    WHERE department_id = p_dept_id;
    
    COMMIT;
EXCEPTION
    WHEN ex_fk_violation THEN
        ROLLBACK;
        DBMS_OUTPUT.PUT_LINE('Không thể xóa phòng ban ID ' || p_dept_id || ' vì vẫn còn nhân viên thuộc phòng này!');
END sp_delete_department;

Cơ Chế Lan Truyền Exception (Exception Propagation Mechanics)

Hiểu rõ cách thức Exception di chuyển qua các khối lệnh PL/SQL (Stack Trace Bubbling) là chìa khóa để thiết kế hệ thống báo lỗi không bị bỏ sót hoặc gây crash vô lý.

Hành Vi Của Block Lồng Nhau (Nested Blocks)

Khi một Exception xảy ra ở khối lệnh bên trong (Inner Block):

  1. Oracle tìm kiếm handler trong phần EXCEPTION của khối lệnh hiện tại.
  2. Nếu tìm thấy handler phù hợp (WHEN ... THEN), khối lệnh hiện tại xử lý lỗi đó thành công. Quyền điều khiển chuyển tiếp sang câu lệnh ngay sau khối lệnh đó ở Outer Block.
  3. Nếu không tìm thấy handler phù hợp, Exception sẽ bị đẩy ngược lên khối lệnh bao ngoài (Outer Block / Calling Procedure). Quá trình này lặp lại cho đến khi gặp handler hoặc văng ra khỏi môi trường gọi (Java, C#, SQL*Plus).

DECLARE
    v_outer_var NUMBER := 100;
BEGIN
    DBMS_OUTPUT.PUT_LINE('Bắt đầu Outer Block');
    
    -- INNER BLOCK
    BEGIN
        DBMS_OUTPUT.PUT_LINE('Bắt đầu Inner Block');
        v_outer_var := v_outer_var / 0; -- Phát sinh ORA-01476 ZERO_DIVIDE
    EXCEPTION
        WHEN VALUE_ERROR THEN
            DBMS_OUTPUT.PUT_LINE('Inner Block: Bắt lỗi Value Error');
            -- Lỗi ZERO_DIVIDE không khớp ở đây, sẽ bị đẩy lên Outer Block!
    END;
    
    DBMS_OUTPUT.PUT_LINE('Dòng này ở Outer Block sẽ KHÔNG BAO GIỜ được thực thi');

EXCEPTION
    WHEN ZERO_DIVIDE THEN
        DBMS_OUTPUT.PUT_LINE('Outer Block: Đã bắt thành công ZERO_DIVIDE lan truyền từ Inner Block!');
END;
/

Tác Động Của Exception Tới Giao Dịch DML (Transaction Control & Rollback)

Đây là điểm cực kỳ quan trọng mà nhiều Developer bị nhầm lẫn:

  • Câu lệnh đơn lẻ (Statement-level Rollback): Khi một câu lệnh SQL trong Procedure bị lỗi, Oracle sẽ tự động rollback duy nhất tác động của câu lệnh đó.
  • Giao dịch chung (Transaction-level Integrity): Các câu lệnh DML trước đó trong cùng Session CHƯA BỊ ROLLBACK ngoại trừ khi lập trình viên chủ động gọi câu lệnh ROLLBACK; trong phần EXCEPTION block!

Cảnh báo nguy hiểm: Nếu bạn không gọi ROLLBACK; trong EXCEPTION handler và để procedure kết thúc hoặc trả lỗi về client, các câu lệnh DML thành công trước đó vẫn nằm trong Transaction State (chờ Commit/Rollback). Điều này có thể dẫn đến việc ứng dụng Client gọi COMMIT ở một thao tác khác và vô tình lưu cả dữ liệu dở dang trước đó vào DB!

Các Hàm & Công Cụ Truy Vết Lỗi Chi Tiết Trong Oracle 

  • SQLCODE: Trả về mã số lỗi dưới dạng NUMBER (Ví dụ: -1403 cho NO_DATA_FOUND, 0 khi không có lỗi).
  • SQLERRM: Trả về chuỗi thông điệp mô tả lỗi.
DECLARE
    v_code NUMBER;
    v_errm VARCHAR2(500);
BEGIN
    INSERT INTO employees (employee_id, email) VALUES (NULL, NULL);
EXCEPTION
    WHEN OTHERS THEN
        v_code := SQLCODE;
        v_errm := SQLERRM;
        DBMS_OUTPUT.PUT_LINE('Mã lỗi: ' || v_code);
        DBMS_OUTPUT.PUT_LINE('Thông điệp: ' || v_errm);
END;
/

Hạn chế của SQLERRM:

  1. SQLERRM mặc định chỉ lấy tối đa 255 bytes (ở các bản Oracle cũ).
  2. SQLERRM chỉ cho biết lỗi là gì chứ KHÔNG CHO BIẾT LỖI Ở DÒNG NÀO trong Stored Procedure dài hàng nghìn dòng.

Truy Vết Vị Trí Dòng Lỗi Với DBMS_UTILITY.FORMAT_ERROR_BACKTRACE

Từ phiên bản Oracle 10g trở lên, Oracle giới thiệu hai hàm cứu tinh cho Database Developer trong package DBMS_UTILITY:

  1. DBMS_UTILITY.FORMAT_ERROR_STACK: Trả về toàn bộ Stack danh sách các lỗi.
  2. DBMS_UTILITY.FORMAT_ERROR_BACKTRACE: Trả về chính xác tên Procedure, Package và Số dòng (Line number) gây ra Exception!
CREATE OR REPLACE PROCEDURE sp_calculate_tax (
    p_salary IN NUMBER
) AS
    v_tax NUMBER;
BEGIN
    -- Dòng 8: Giả sử gặp lỗi chia 0
    v_tax := p_salary / (p_salary - 5000); 
    DBMS_OUTPUT.PUT_LINE('Thuế: ' || v_tax);
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('--- LỖI XẢY RA ---');
        DBMS_OUTPUT.PUT_LINE('Error Stack: ' || DBMS_UTILITY.FORMAT_ERROR_STACK);
        DBMS_OUTPUT.PUT_LINE('Backtrace (Vị trí dòng lỗi): ' || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END sp_calculate_tax;
/

Bắn Lỗi Về Client Bằng RAISE_APPLICATION_ERROR

Để trả lỗi tùy chỉnh từ Stored Procedure về cho các ngôn ngữ lập trình phía Frontend / Backend (Java, C#, Node.js, Python), Oracle cung cấp thủ tục RAISE_APPLICATION_ERROR.

Cú pháp:

RAISE_APPLICATION_ERROR(error_number, error_message [, keep_errors]);

  • error_number: Mã lỗi tùy chỉnh từ -20000 đến -20999 (Oracle dành riêng dải số này cho người dùng).
  • error_message: Chuỗi văn bản mô tả nguyên nhân (dài tối đa 2048 bytes).
  • keep_errors: Boolean (Default FALSE). Nếu TRUE, nối tiếp vào danh sách lỗi trước đó.

Anti-Pattern Tối Thượng: WHEN OTHERS THEN NULL

Trong quá trình Code Review tại các doanh nghiệp lớn, việc phát hiện đoạn code dưới đây được coi là "Tội đồ số 1" của lập trình viên Database:

 

-- ANTI-PATTERN: CỰC KỲ NGUY HIỂM! KHÔNG BAO GIỜ ĐƯỢC VIẾT THẾ NÀY!
EXCEPTION
    WHEN OTHERS THEN
        NULL; -- Che giấu mọi lỗi, âm thầm bỏ qua!
END;

Tại sao đây là thảm họa?

  1. Kill Silent Bug: Khi câu lệnh DML thất bại, chương trình vẫn chạy tiếp như không có chuyện gì xảy ra. Dữ liệu sai hỏng sẽ lây lan sang các bảng khác.
  2. Không thể debug: Khi DBA hoặc người dùng báo dữ liệu mất tích, log hoàn toàn sạch sẽ, không có bất kỳ dấu vết nào để truy tìm nguyên nhân.

Autonomous Transaction Log Engine (Ghi Log Độc Lập)

Khi một Stored Procedure gặp lỗi và thực hiện ROLLBACK;, mọi câu lệnh INSERT vào bảng Log thông thường cũng sẽ bị Rollback mất sạch!

Để giải quyết triệt để vấn đề này, giải pháp tiêu chuẩn Enterprise là sử dụng Pragma Autonomous Transaction (Giao dịch độc lập). Khối lệnh ghi log sẽ chạy trên một Transaction riêng biệt hoàn toàn với Transaction chính.

Tạo Bảng Log Chuẩn Audit Trail

CREATE TABLE sys_error_logs (
    log_id          NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    procedure_name  VARCHAR2(100),
    error_code      NUMBER,
    error_message   VARCHAR2(4000),
    error_backtrace VARCHAR2(4000),
    user_name       VARCHAR2(50),
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Bước 2: Viết Procedure Ghi Log Độc Lập
sql

CREATE OR REPLACE PROCEDURE sp_log_error (
    p_proc_name    IN VARCHAR2,
    p_error_code   IN NUMBER,
    p_error_msg    IN VARCHAR2,
    p_backtrace    IN VARCHAR2
) AS
    -- Chỉ thị cho Oracle biết đây là Giao dịch độc lập
    PRAGMA AUTONOMOUS_TRANSACTION; 
BEGIN
    INSERT INTO sys_error_logs (
        procedure_name,
        error_code,
        error_message,
        error_backtrace,
        user_name
    ) VALUES (
        p_proc_name,
        p_error_code,
        SUBSTR(p_error_msg, 1, 4000),
        SUBSTR(p_backtrace, 1, 4000),
        USER
    );
    
    -- Commit riêng cho transaction log mà KHÔNG ảnh hưởng tới main transaction
    COMMIT; 
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK; -- Nếu log lỗi thất bại cũng không ngắt ứng dụng chính
END sp_log_error;
/

Quản Lý Lỗi Trong Xử Lý Batch Data Với FORALL & SAVE EXCEPTIONS

Khi xử lý hàng triệu bản ghi bằng FORALL (Bulk Collect processing), nếu bản ghi thứ 500 bị lỗi (ví dụ: duplicate key), theo mặc định toàn bộ batch từ bản ghi 501 trở đi sẽ bị dừng lập tức.

Để cho phép vòng lặp tiếp tục xử lý các bản ghi hợp lệ còn lại và gom các dòng lỗi lại xử lý sau, ta dùng mệnh đề SAVE EXCEPTIONS.


CREATE OR REPLACE PROCEDURE sp_bulk_update_salaries (
    p_emp_ids   IN DBMS_SQL.NUMBER_TABLE,
    p_salaries  IN DBMS_SQL.NUMBER_TABLE
) AS
    ex_bulk_errors EXCEPTION;
    PRAGMA EXCEPTION_INIT(ex_bulk_errors, -24381); -- ORA-24381: error(s) in array DML
    
    v_error_count NUMBER;
BEGIN
    FORALL i IN 1..p_emp_ids.COUNT SAVE EXCEPTIONS
        UPDATE employees
        SET salary = p_salaries(i)
        WHERE employee_id = p_emp_ids(i);

EXCEPTION
    WHEN ex_bulk_errors THEN
        v_error_count := SQL%BULK_EXCEPTIONS.COUNT;
        DBMS_OUTPUT.PUT_LINE('Số lượng dòng bị lỗi trong Batch: ' || v_error_count);
        
        -- Lặp qua từng bản ghi bị lỗi để log lại chi tiết
        FOR i IN 1..v_error_count LOOP
            DBMS_OUTPUT.PUT_LINE(
                'Index bị lỗi: ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX ||
                ' | Mã lỗi ORA: ' || SQL%BULK_EXCEPTIONS(i).ERROR_CODE
            );
            
            -- Gọi hàm log lỗi ở đây
            sp_log_error(
                'sp_bulk_update_salaries',
                SQL%BULK_EXCEPTIONS(i).ERROR_CODE,
                'Lỗi Bulk Update tại Index ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX,
                'FORALL SAVE EXCEPTIONS'
            );
        END LOOP;
END sp_bulk_update_salaries;
/

Case Study Thực Tế: Stored Procedure Chuyển Tiền Ngân Hàng Tích Hợp Error Handling Complete

Dưới đây là một ví dụ thực tế hoàn chỉnh (Production-ready) ứng dụng toàn bộ kiến thức đã học vào nghiệp vụ Chuyển tiền tài khoản Ngân hàng:


CREATE OR REPLACE PROCEDURE sp_bank_transfer (
    p_from_acc_no IN VARCHAR2,
    p_to_acc_no   IN VARCHAR2,
    p_amount      IN NUMBER,
    p_ref_no      OUT VARCHAR2
) AS
    /*--------------------------------------------------------------------------
    * Purpose: Thực hiện giao dịch chuyển tiền giữa 2 tài khoản ngân hàng.
    * Features: Lock row tránh Race condition, Validate số dư, Autonomous log.
    *--------------------------------------------------------------------------*/
    v_proc_name       CONSTANT VARCHAR2(100) := 'sp_bank_transfer';
    v_from_balance    NUMBER;
    v_to_balance      NUMBER;
    v_from_status     VARCHAR2(20);
    v_to_status       VARCHAR2(20);
    
    -- Custom Exceptions
    ex_insufficient_funds EXCEPTION;
    ex_account_locked     EXCEPTION;
    ex_same_account       EXCEPTION;
    ex_invalid_amount     EXCEPTION;
    
    -- Non-Predefined Exception cho Deadlock
    ex_deadlock_detected  EXCEPTION;
    PRAGMA EXCEPTION_INIT(ex_deadlock_detected, -00060);

BEGIN
    -- 1. Validate tham số đầu vào
    IF p_amount <= 0 THEN
        RAISE ex_invalid_amount;
    END IF;
    
    IF p_from_acc_no = p_to_acc_no THEN
        RAISE ex_same_account;
    END IF;

    -- 2. Kiểm tra và Khóa dòng Tài khoản Nguồn (LOCK FOR UPDATE)
    BEGIN
        SELECT balance, status
        INTO v_from_balance, v_from_status
        FROM bank_accounts
        WHERE account_number = p_from_acc_no
        FOR UPDATE WAIT 10; -- Chờ tối đa 10 giây nếu tài khoản đang bị lock bởi session khác
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20001, 'Tài khoản nguồn không tồn tại trên hệ thống.');
    END;

    -- 3. Kiểm tra và Khóa dòng Tài khoản Đích
    BEGIN
        SELECT balance, status
        INTO v_to_balance, v_to_status
        FROM bank_accounts
        WHERE account_number = p_to_acc_no
        FOR UPDATE WAIT 10;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20002, 'Tài khoản đích không tồn tại trên hệ thống.');
    END;

    -- 4. Validate Logic Nghiệp Vụ
    IF v_from_status <> 'ACTIVE' OR v_to_status <> 'ACTIVE' THEN
        RAISE ex_account_locked;
    END IF;

    IF v_from_balance < p_amount THEN
        RAISE ex_insufficient_funds;
    END IF;

    -- 5. Thực hiện DML Trừ/Cộng tiền
    UPDATE bank_accounts 
    SET balance = balance - p_amount 
    WHERE account_number = p_from_acc_no;

    UPDATE bank_accounts 
    SET balance = balance + p_amount 
    WHERE account_number = p_to_acc_no;

    -- 6. Tạo Mã Giao Dịch & Ghi nhận bảng History
    p_ref_no := 'TXN' || TO_CHAR(SYSTIMESTAMP, 'YYYYMMDDHH24MISSFF3');

    INSERT INTO bank_transactions (
        transaction_ref, from_account, to_account, amount, status, created_date
    ) VALUES (
        p_ref_no, p_from_acc_no, p_to_acc_no, p_amount, 'SUCCESS', SYSDATE
    );

    -- 7. Hoàn tất Giao dịch chính
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('Giao dịch ' || p_ref_no || ' thành công!');

EXCEPTION
    WHEN ex_invalid_amount THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20003, 'Số tiền chuyển phải lớn hơn 0.');

    WHEN ex_same_account THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20004, 'Tài khoản nguồn và đích không được trùng nhau.');

    WHEN ex_insufficient_funds THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20005, 'Tài khoản nguồn không đủ số dư khả dụng.');

    WHEN ex_account_locked THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20006, 'Một trong hai tài khoản đang bị tạm khóa hoặc ngừng hoạt động.');

    WHEN ex_deadlock_detected THEN
        ROLLBACK;
        -- Log lỗi Deadlock khẩn cấp
        sp_log_error(v_proc_name, SQLCODE, 'ORA-00060: Deadlock detected', DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
        RAISE_APPLICATION_ERROR(-20007, 'Hệ thống đang quá tải giao dịch song song. Vui lòng thử lại sau giây lát.');

    WHEN OTHERS THEN
        -- Bắt tất cả các lỗi không lường trước được (Ví dụ: Mất kết nối mạng, Disk full, Bugs)
        ROLLBACK;
        
        -- Ghi Log độc lập vào bảng DB để DBA kiểm tra
        sp_log_error(
            v_proc_name, 
            SQLCODE, 
            SQLERRM, 
            DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
        );
        
        -- Re-raise lỗi chuẩn về phía Client app
        RAISE_APPLICATION_ERROR(-20099, 'Lỗi hệ thống nội bộ (' || SQLCODE || '). Mã truy vết đã được log.');
END sp_bank_transfer;
/

Checklist Code Review Xử Lý Exception Dành Cho Database Engineer

Trước khi Merge Code Procedure lên môi trường Production, hãy kiểm tra danh sách bên dưới:

  • Checklist 1: Có câu lệnh WHEN OTHERS THEN NULL; nào bị bỏ quên trong code không? (Nếu có => Reject Code ngay lập tức).
  • Checklist 2: Mọi nhánh Exception có thực hiện ROLLBACK; hợp lý trước khi ném lỗi lên tầng trên không?
  • Checklist 3: Các mã lỗi tự định nghĩa bằng RAISE_APPLICATION_ERROR có nằm trong dải chuẩn -20000 đến -20999 không?
  • Checklist 4: Đã sử dụng DBMS_UTILITY.FORMAT_ERROR_BACKTRACE thay cho SQLERRM đơn thuần khi ghi log sự cố chưa?
  • Checklist 5: Hàm ghi Log lỗi có được gắn chỉ thị PRAGMA AUTONOMOUS_TRANSACTION chưa?
  • Checklist 6: Với các câu lệnh SELECT INTO, đã xử lý triệt để 2 ngoại lệ NO_DATA_FOUND và TOO_MANY_ROWS chưa?
  • Checklist 7: Các Cursor hoặc tài nguyên Temp LOB có được đóng/giải phóng an toàn trong khối Exception handler chưa?

Câu Hỏi Thường Gặp (FAQ)

Q1: Sử dụng Exception trong Stored Procedure có làm giảm hiệu năng (Performance) không?

Trả lời: Không. Khác với một số ngôn ngữ như Java nơi việc khởi tạo Throwable Stacktrace tốn CPU, trong Oracle PL/SQL, cơ chế Exception được tối ưu ở mức Engine C-level. Việc sử dụng Exception chuẩn giúp code sạch hơn nhiều so me với việc viết hàng chục câu lệnh IF...ELSE lồng nhau phức tạp để check lỗi.

Q2: Tại sao nên dùng RAISE; không tham số trong khối EXCEPTION?

Trả lời: Khi bạn bắt một lỗi cụ thể (ví dụ trong nested block) để thực hiện một hành động trung gian (như giải phóng biến temporary), câu lệnh RAISE; viết ngắn gọn trong handler sẽ tiếp tục ném chính Exception ban đầu đó lên khối lệnh bao ngoài mà không làm mất đi Stack Backtrace ban đầu.

Q3: Sự khác biệt giữa SQLERRM và DBMS_UTILITY.FORMAT_ERROR_STACK là gì?

Trả lời: SQLERRM chỉ trả về thông điệp của lỗi trên cùng trong Stack. Trong khi FORMAT_ERROR_STACK trả về chuỗi kết hợp tất cả các lỗi được ném nối tiếp nhau qua nhiều tầng Stored Procedure gọi nhau.

Q4: Làm sao để bắt lỗi khi thực hiện Dynamic SQL với EXECUTE IMMEDIATE?

Trả lời: Bạn chỉ cần bao câu lệnh EXECUTE IMMEDIATE trong một khối BEGIN...END riêng lẻ với khối EXCEPTION handler tương ứng. Mọi lỗi cú pháp SQL hoặc runtime trong Dynamic SQL đều bắn ra mã lỗi ORA tương tự như Static SQL.

Tóm lại

Xử lý ngoại lệ (Exception Handling) trong Oracle PL/SQL Stored Procedure không đơn thuần là cú pháp lập trình, mà là tư duy kiến trúc hệ thống. Một Stored Procedure chuẩn Enterprise phải luôn sẵn sàng đương đầu với mọi kịch bản dữ liệu bất ngờ, bảo vệ tính toàn vẹn dữ liệu (Data Integrity) và cung cấp khả năng truy vết sự cố nhanh nhất cho đội ngũ vận hành.

Hi vọng bài viết chuyên sâu này giúp các bạn lập trình viên Oracle Database nắm vững bản chất, áp dụng đúng các Enterprise Design Patterns và nâng cao chất lượng mã nguồn cho các dự án thực tế!