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 Exception
Mã Lỗi ORA
Nguyên Nhân Kích Hoạt
NO_DATA_FOUND
ORA-01403
Câu lệnh SELECT INTO không trả về dòng dữ liệu nào.
TOO_MANY_ROWS
ORA-01422
Câu lệnh SELECT INTO trả về từ 2 dòng trở lên.
ZERO_DIVIDE
ORA-01476
Thực hiện phép chia một số cho 0.
DUP_VAL_ON_INDEX
ORA-00001
Chèn dữ liệu trùng lặp vào cột có Unique Index/Primary Key.
VALUE_ERROR
ORA-06502
Lỗi kiểu dữ liệu, gán chuỗi quá dài vào biến có dung lượng nhỏ.
INVALID_CURSOR
ORA-01001
Thao 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):
Oracle tìm kiếm handler trong phần EXCEPTION của khối lệnh hiện tại.
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.
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:
SQLERRM mặc định chỉ lấy tối đa 255 bytes (ở các bản Oracle cũ).
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:
DBMS_UTILITY.FORMAT_ERROR_STACK: Trả về toàn bộ Stack danh sách các lỗi.
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.
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ế!
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:
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:
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ì.
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.
Ví dụ thực tế:
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.
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ể.
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):
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ác Hàm & Công Cụ Truy Vết Lỗi Chi Tiết Trong Oracle
Hạn chế của SQLERRM:
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:
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]);
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:
Tại sao đây là thảm họa?
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
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.
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:
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:
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?
Q2: Tại sao nên dùng RAISE; không tham số trong khối EXCEPTION?
Q3: Sự khác biệt giữa SQLERRM và DBMS_UTILITY.FORMAT_ERROR_STACK là gì?
Q4: Làm sao để bắt lỗi khi thực hiện Dynamic SQL với EXECUTE IMMEDIATE?
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ế!