Cursor và Exception Handling
Xem thửTình huống mở đầu
Cuối ngày, chị thủ kho của Nhà Sách Ông Đồ cho chạy vòng xả kho: đầu sách nào còn từ 5 cuốn trở xuống thì bán thử 2 cuốn vào sổ sales_log. Có ba đầu sách thỏa điều kiện, xếp theo giá tăng dần: Data Modeling tồn 5, PL/SQL Intro tồn 0 và Java Core tồn 3. Anh Sáu viết một vòng lặp ngắn gọn và chạy. Màn hình in ngay dòng Data Modeling: sold 2, rồi hiện một lỗi đỏ. Sáng hôm sau sổ trống và tồn kho không đổi, dòng đã in "bán xong" biến mất như chưa từng có. Chị thủ kho hỏi: sao một cuốn hỏng lại kéo theo cuốn tốt, và có cách nào để cuốn hỏng bị bỏ qua mà hai cuốn kia vẫn bán được không?
Hãy đoán trước hai điều. Một, nếu chỉ có một khối EXCEPTION bao ngoài cả vòng lặp thì Data Modeling đã bán có còn trong sổ không, và Java Core có được bán không? Hai, nếu không bắt lỗi ở đâu cả thì sao?
Ôn nhanh bài trước: muốn đặt một record r_book về trạng thái trống thì làm thế nào?
Đáp án: gán cho nó một record cùng kiểu chưa được điền. Bài trước kết thúc bằng câu hỏi đi từng dòng của một truy vấn và xử lý lỗi nửa chừng: bài này trả lời bằng cursor và exception.
Mục tiêu bài học
Sau bài này bạn có thể:
- Dùng cursor ẩn (
SQL%FOUND,SQL%ROWCOUNT) và cursor tường minh (OPEN,FETCH,CLOSE) với%FOUND,%NOTFOUND,%ISOPEN,%ROWCOUNT, tham số và vòngFOR. - Bắt lỗi bằng khối
EXCEPTIONvới exception dựng sẵn, exception tự khai báo,PRAGMA EXCEPTION_INITvàRAISE_APPLICATION_ERROR, rồi đọcSQLCODE,SQLERRM. - Giải thích lỗi lan truyền qua khối lồng, vì sao lỗi trong phần khai báo hoặc trong handler đi ra khỏi khối.
- Phân biệt việc đã làm được giữ lại khi lỗi được bắt, và bị hoàn tác khi lỗi không ai bắt; dùng khối con từng dòng hoặc
SAVEPOINTđể chọn kết cục.
Bức tranh lớn
Xuyên suốt bài, cursor là băng chuyền ở quầy thu ngân: mỗi lần FETCH băng chuyền đưa một cuốn sách ra trước mặt chị thu ngân, hết sách thì băng chuyền báo hết. Exception là chuông báo sự cố ở từng quầy: ca trưởng của quầy (khối EXCEPTION) xử lý nếu biết cách, không thì chuông reo lên quầy cao hơn, và cuối cùng là cả cửa hàng. Dự án xuyên suốt cả chương là xử lý phiếu xuất kho hàng loạt: bài đầu là giỏ chứa dữ liệu, bài trước là phiếu nhiều trường, bài này là duyệt từng dòng và xử lý lỗi. Mọi kết quả chạy trên Oracle AI Database 26ai Free (release 23.26.3).
Bạn đã học exception ở PL/pgSQL, nên bài này không nhắc lại ý chung mà đi vào những chỗ Oracle riêng: lỗi có mã ORA- và SQLCODE là số âm, chỉ một số ít tên dựng sẵn nên thường phải tự đặt tên cho mã lỗi, và khối EXCEPTION không tự hoàn tác phần việc đã làm. Hình dưới trả lời một câu hỏi: vòng đời của một cursor tường minh đi qua những bước nào?
Đang tải sơ đồ…
Cách đọc hình: đọc từ ô xanh dương: OPEN chạy truy vấn và đặt băng chuyền vào vị trí đầu. Ô tím tiếp theo là FETCH, đọc một dòng vào r_book. Ô cam hỏi %NOTFOUND: nếu chưa hết dòng (nhánh no) thì xử lý dòng rồi quay lại FETCH, nếu hết (nhánh yes) thì đi tới ô xanh lá CLOSE giải phóng cursor.
Chỗ ẩn dụ không còn đúng: băng chuyền thật chạy một lượt rồi thôi. Cursor Oracle đã đóng thì phải OPEN lại từ đầu, và đọc một cursor chưa mở là lỗi, như phần Thử phá nó đi sẽ cho thấy.
Khái niệm cốt lõi
Bài này có năm khái niệm.
1. Cursor ẩn: dấu vết của câu SQL vừa chạy
Mỗi câu INSERT, UPDATE, DELETE hay SELECT INTO chạy qua một cursor ẩn tên SQL. Ngay sau câu lệnh, bạn hỏi nó qua bốn thuộc tính:
| Thuộc tính | Cho biết | Ghi chú |
|---|---|---|
SQL%FOUND | Câu vừa rồi có tác động ít nhất một dòng | UPDATE không dòng nào khớp thì sai, không lỗi |
SQL%NOTFOUND | Ngược lại với FOUND | Với SELECT INTO, không có dòng thì lỗi trước khi bạn kịp hỏi |
SQL%ROWCOUNT | Số dòng bị tác động | SELECT COUNT(*) INTO luôn là một dòng, kể cả khi đếm ra 0 |
SQL%ISOPEN | Cursor ẩn còn mở không | Luôn sai, vì Oracle đóng nó ngay khi câu lệnh xong |
2. Cursor tường minh: bốn bước và bốn thuộc tính
Khi truy vấn trả nhiều dòng, bạn khai báo cursor ở phần DECLARE, rồi OPEN, FETCH từng dòng vào biến hoặc record, và CLOSE. Cursor có thể nhận tham số: CURSOR c_low (p_max NUMBER) IS ... rồi OPEN c_low(5).
| Thuộc tính | Cho biết | Ghi chú |
|---|---|---|
c%ISOPEN | Cursor đang mở | Sai trước OPEN và sau CLOSE |
c%FOUND | FETCH vừa rồi có dòng | Đúng khi còn dòng để đọc |
c%NOTFOUND | FETCH vừa rồi hết dòng | Dùng làm điều kiện thoát vòng lặp |
c%ROWCOUNT | Số dòng đã FETCH | Tăng sau mỗi lần FETCH thành công |
Vòng FOR r IN c LOOP ... END LOOP làm cả ba việc mở, đọc, đóng và tự khai báo record r. Nó ngắn hơn và không quên CLOSE, nên là cách mặc định trừ khi bạn cần điều khiển từng FETCH.
3. Exception: dựng sẵn, tự khai báo, đặt tên cho mã lỗi
Một khối BEGIN ... EXCEPTION WHEN tên THEN ... END bắt lỗi theo tên. Có ba nguồn tên:
| Loại | Cách có tên | Ví dụ |
|---|---|---|
| Dựng sẵn | Có sẵn, không khai báo | NO_DATA_FOUND, TOO_MANY_ROWS, ZERO_DIVIDE, DUP_VAL_ON_INDEX |
| Tự khai báo | e_out EXCEPTION; rồi RAISE e_out; | SQLCODE là 1 và SQLERRM là User-Defined Exception |
| Đặt tên cho mã lỗi | PRAGMA EXCEPTION_INIT(e_check, -2290); | Bắt được lỗi vi phạm ràng buộc CHECK |
Lỗi của chính bạn dùng RAISE_APPLICATION_ERROR(mã, 'nội dung') với mã từ -20999 đến -20000. SQLCODE trả mã số, SQLERRM trả cả dòng như ORA-20010: Not enough stock. WHEN OTHERS bắt mọi thứ còn lại và phải đứng cuối cùng.
4. Lan truyền: lỗi đi lên khối bao ngoài
Hình dưới trả lời một câu hỏi: một lỗi vừa xảy ra, nó đi về đâu?
Đang tải sơ đồ…
Cách đọc hình: ô đỏ đầu là lỗi vừa xảy ra. Ô cam hỏi khối hiện tại có handler khớp không: có thì handler chạy và khối kết thúc bình thường (ô xanh lá). Không thì ô cam thứ hai hỏi có khối bao ngoài không: có thì lặp lại câu hỏi ở khối đó (ô tím quay về ô cam đầu), không thì lỗi tới nơi gọi (ô đỏ cuối) và lời gọi bị hoàn tác.
Hai ngoại lệ quan trọng: lỗi xảy ra trong phần khai báo của khối không được handler của chính khối đó bắt, và lỗi xảy ra bên trong một handler cũng không được các handler anh em cùng khối bắt. Cả hai đều đi lên khối bao ngoài.
5. Giao dịch khi có lỗi
Oracle hoàn tác theo hai tầng. Câu lệnh gây lỗi luôn bị hoàn tác riêng nó. Phần việc trước đó trong khối thì khác nhau: nếu lỗi được bắt và khối kết thúc bình thường thì các câu trước đó được giữ lại, Oracle không tự ROLLBACK. Nếu lỗi không ai bắt và tới nơi gọi thì lời gọi bị hoàn tác cả phần việc của nó. Muốn kết cục khác thì bạn chọn: khối con trong thân vòng lặp để giữ dòng tốt, hoặc SAVEPOINT rồi ROLLBACK TO để hủy cả lô.
Ví dụ đầu tiên
Dựng bảng books và sales_log. Trước hết là cursor tường minh có tham số, đọc các đầu sách tồn kho từ 5 cuốn trở xuống [Đã chạy: Oracle 23.26.3]:
DECLARE
CURSOR c_low (p_max NUMBER DEFAULT 5) IS
SELECT id, title, stock FROM books WHERE stock <= p_max ORDER BY id;
r_book c_low%ROWTYPE;
BEGIN
DBMS_OUTPUT.PUT_LINE('before open: isopen=' || CASE WHEN c_low%ISOPEN THEN 'Y' ELSE 'N' END);
OPEN c_low(5);
LOOP
FETCH c_low INTO r_book;
EXIT WHEN c_low%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('row ' || c_low%ROWCOUNT || ': ' || r_book.title || ', stock ' || r_book.stock);
END LOOP;
DBMS_OUTPUT.PUT_LINE('after last fetch: rowcount=' || c_low%ROWCOUNT || ' isopen=' || CASE WHEN c_low%ISOPEN THEN 'Y' ELSE 'N' END);
CLOSE c_low;
DBMS_OUTPUT.PUT_LINE('after close: isopen=' || CASE WHEN c_low%ISOPEN THEN 'Y' ELSE 'N' END);
END;
/
before open: isopen=N
row 1: PL/SQL Intro, stock 0
row 2: Data Modeling, stock 5
row 3: Java Core, stock 3
after last fetch: rowcount=3 isopen=Y
after close: isopen=N
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
CURSOR c_low (p_max NUMBER DEFAULT 5) IS ... | Khai báo cursor, chưa chạy truy vấn | Tham số cho phép dùng lại cursor với ngưỡng khác |
r_book c_low%ROWTYPE | Record có các trường id, title, stock theo câu SELECT | Không cần khai báo từng biến |
OPEN c_low(5) | Chạy truy vấn với p_max bằng 5 | Sau lệnh này c_low%ISOPEN mới đúng |
EXIT WHEN c_low%NOTFOUND | Thoát khi FETCH không còn dòng | Phải đặt ngay sau FETCH, trước khi dùng r_book |
c_low%ROWCOUNT | Số dòng đã đọc, từ 1 đến 3 | Sau dòng cuối nó vẫn bằng 3 chứ không về 0 |
CLOSE c_low | Giải phóng cursor | Sau đó %ISOPEN sai; đóng thiếu thì cursor chiếm tài nguyên cho đến hết phiên |
Giờ vòng xả kho của anh Sáu, viết lại có khối con trong thân vòng lặp [Đã chạy: Oracle 23.26.3]:
DECLARE
CURSOR c_low (p_max NUMBER) IS
SELECT id, title, price FROM books WHERE stock <= p_max ORDER BY price;
e_check EXCEPTION;
PRAGMA EXCEPTION_INIT(e_check, -2290); -- tên cho lỗi vi phạm ràng buộc CHECK
v_left books.stock%TYPE;
BEGIN
FOR r_book IN c_low(5) LOOP
BEGIN -- khối con: lỗi của một dòng không lan ra vòng lặp
UPDATE books SET stock = stock - 2 WHERE id = r_book.id RETURNING stock INTO v_left;
INSERT INTO sales_log (book_id, qty, total) VALUES (r_book.id, 2, r_book.price * 2);
DBMS_OUTPUT.PUT_LINE(r_book.title || ': sold 2, stock left ' || v_left);
EXCEPTION
WHEN e_check THEN
DBMS_OUTPUT.PUT_LINE(r_book.title || ': skipped, stock would go negative');
END;
END LOOP;
END;
/
SELECT id, stock FROM books ORDER BY id;
SELECT COUNT(*) AS sales_rows FROM sales_log;
Data Modeling: sold 2, stock left 3
PL/SQL Intro: skipped, stock would go negative
Java Core: sold 2, stock left 1
ID STOCK
--- -----
1 10
2 0
3 3
4 1
SALES_ROWS
----------
2
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
PRAGMA EXCEPTION_INIT(e_check, -2290) | Gắn tên e_check cho mã lỗi vi phạm ràng buộc CHECK | Oracle không có tên dựng sẵn cho lỗi này |
FOR r_book IN c_low(5) LOOP | Mở cursor, đọc từng dòng vào r_book, tự đóng khi hết | Không cần OPEN, FETCH, CLOSE |
BEGIN ... EXCEPTION ... END trong vòng lặp | Khối con bao riêng việc của một dòng | Lỗi của dòng đó bị chặn tại đây, vòng lặp không biết |
RETURNING stock INTO v_left | Lấy tồn kho mới ngay trong câu UPDATE | Không cần SELECT lại |
WHEN e_check THEN | In dòng báo bỏ qua rồi kết thúc khối con | Khối con kết thúc bình thường nên vòng lặp đi tiếp |
PL/SQL Intro bị bỏ qua, hai sách còn lại bán được: tồn kho 3 và 1, sổ có hai dòng. Còn nếu không có khối con và cũng không có handler nào [Đã chạy: Oracle 23.26.3]:
DECLARE
CURSOR c_low (p_max NUMBER) IS
SELECT id, title, price FROM books WHERE stock <= p_max ORDER BY price;
e_check EXCEPTION;
PRAGMA EXCEPTION_INIT(e_check, -2290); -- tên cho lỗi vi phạm ràng buộc CHECK
v_left books.stock%TYPE;
BEGIN
FOR r_book IN c_low(5) LOOP
UPDATE books SET stock = stock - 2 WHERE id = r_book.id RETURNING stock INTO v_left;
INSERT INTO sales_log (book_id, qty, total) VALUES (r_book.id, 2, r_book.price * 2);
DBMS_OUTPUT.PUT_LINE(r_book.title || ': sold 2, stock left ' || v_left);
END LOOP;
END;
/
SELECT id, stock FROM books ORDER BY id;
SELECT COUNT(*) AS sales_rows FROM sales_log;
Data Modeling: sold 2, stock left 3
DECLARE
*
ERROR at line 1:
ORA-02290: check constraint (SHOP.BOOKS_STOCK_CK) violated
ORA-06512: at line 10
ORA-06512: at line 10
Help: https://docs.oracle.com/error-help/db/ora-02290/
ID STOCK
--- -----
1 10
2 0
3 5
4 3
SALES_ROWS
----------
0
Màn hình vẫn in Data Modeling: sold 2, nhưng sổ trống và tồn kho nguyên như cũ. Lỗi chưa bắt đi tới nơi gọi nên Oracle hoàn tác cả phần việc của lời gọi, còn dòng chữ in ra thì không hoàn tác được vì DBMS_OUTPUT không thuộc giao dịch. Nếu đặt một handler bao ngoài cả vòng lặp [Đã chạy: Oracle 23.26.3]:
DECLARE
CURSOR c_low (p_max NUMBER) IS
SELECT id, title, price FROM books WHERE stock <= p_max ORDER BY price;
e_check EXCEPTION;
PRAGMA EXCEPTION_INIT(e_check, -2290); -- tên cho lỗi vi phạm ràng buộc CHECK
v_left books.stock%TYPE;
BEGIN
FOR r_book IN c_low(5) LOOP
UPDATE books SET stock = stock - 2 WHERE id = r_book.id RETURNING stock INTO v_left;
INSERT INTO sales_log (book_id, qty, total) VALUES (r_book.id, 2, r_book.price * 2);
DBMS_OUTPUT.PUT_LINE(r_book.title || ': sold 2, stock left ' || v_left);
END LOOP;
EXCEPTION
WHEN e_check THEN
DBMS_OUTPUT.PUT_LINE('batch stopped: a stock went negative');
END;
/
SELECT id, stock FROM books ORDER BY id;
SELECT COUNT(*) AS sales_rows FROM sales_log;
Data Modeling: sold 2, stock left 3
batch stopped: a stock went negative
ID STOCK
--- -----
1 10
2 0
3 3
4 3
SALES_ROWS
----------
1
Lần này Data Modeling được giữ, vì lỗi đã bắt thì Oracle không hoàn tác phần trước, nhưng vòng lặp dừng ngay ở dòng lỗi nên Java Core không bao giờ được thử.
Xem từng bước
Animation dưới đây đi theo đúng ba sách ấy. Hãy trả lời câu dự đoán trước, rồi bấm nút đổi kịch bản để so sánh lỗi chưa bắt với khối con bắt từng dòng.
Đang tải Animation…
Bạn vừa làm được: duyệt từng dòng bằng cursor, bắt lỗi của một dòng bằng khối con, và giải thích vì sao vị trí của khối EXCEPTION quyết định dòng tốt sống hay chết.
Mã lỗi của exception dựng sẵn
Mỗi exception dựng sẵn đi kèm một mã SQLCODE cố định. Chạy thật để thấy [Đã chạy: Oracle 23.26.3]:
DECLARE
v_n NUMBER;
v_t VARCHAR2(30);
v_s VARCHAR2(3);
CURSOR c_all IS SELECT id FROM books;
PROCEDURE show (p_name VARCHAR2, p_code NUMBER) IS
BEGIN
DBMS_OUTPUT.PUT_LINE(RPAD(p_name, 20) || p_code);
END;
BEGIN
BEGIN SELECT title INTO v_t FROM books WHERE id = 99;
EXCEPTION WHEN NO_DATA_FOUND THEN show('NO_DATA_FOUND', SQLCODE); END;
BEGIN SELECT title INTO v_t FROM books;
EXCEPTION WHEN TOO_MANY_ROWS THEN show('TOO_MANY_ROWS', SQLCODE); END;
BEGIN v_n := 1 / 0;
EXCEPTION WHEN ZERO_DIVIDE THEN show('ZERO_DIVIDE', SQLCODE); END;
BEGIN INSERT INTO books VALUES (1, 'Dup', 1, 1);
EXCEPTION WHEN DUP_VAL_ON_INDEX THEN show('DUP_VAL_ON_INDEX', SQLCODE); END;
BEGIN v_s := 'abcdef';
EXCEPTION WHEN VALUE_ERROR THEN show('VALUE_ERROR', SQLCODE); END;
BEGIN SELECT TO_NUMBER('12x') INTO v_n FROM dual;
EXCEPTION WHEN INVALID_NUMBER THEN show('INVALID_NUMBER', SQLCODE); END;
BEGIN OPEN c_all; OPEN c_all;
EXCEPTION WHEN CURSOR_ALREADY_OPEN THEN show('CURSOR_ALREADY_OPEN', SQLCODE); END;
END;
/
NO_DATA_FOUND 100
TOO_MANY_ROWS -1422
ZERO_DIVIDE -1476
DUP_VAL_ON_INDEX -1
VALUE_ERROR -6502
INVALID_NUMBER -1722
CURSOR_ALREADY_OPEN -6511
NO_DATA_FOUND là trường hợp riêng với mã dương 100, các lỗi khác là số âm trùng với số trong ORA-xxxxx. Bạn dùng bảng này để đoán lỗi nào tương ứng handler nào.
Concept Check
Câu 1. SELECT title INTO v_title FROM books WHERE id = 99; không có dòng nào khớp. Exception nào được ném và SQLCODE là gì?
- A.
TOO_MANY_ROWS,SQLCODElà -1422. - B.
NO_DATA_FOUND,SQLCODElà 100. - C.
NO_DATA_FOUND,SQLCODElà -1403. - D. Không có exception,
v_titlechỉ làNULL.
Câu 2. Khối BEGIN UPDATE books SET stock = stock + 1 WHERE id = 1; UPDATE books SET stock = stock - 99 WHERE id = 2; EXCEPTION WHEN OTHERS THEN NULL; END; chạy xong. Tồn kho sách 1 thế nào?
- A. Giữ nguyên, vì lỗi hoàn tác cả khối.
- B. Tăng 1, vì lỗi đã bắt nên phần việc trước đó được giữ lại.
- C. Tăng 1, nhưng chỉ khi sau đó có
COMMIT. - D. Giảm 99, vì
UPDATEthứ hai chạy trước khi lỗi.
Câu 3. Cursor c đã mở, vòng lặp viết FETCH c INTO r; DBMS_OUTPUT.PUT_LINE(r.title); EXIT WHEN c%NOTFOUND;. Điều gì xảy ra với dòng cuối?
- A. Dòng cuối bị bỏ qua, vì
EXITchạy trước. - B. Dòng cuối được in một lần, đúng như mong muốn.
- C. Dòng cuối được in hai lần, vì lần
FETCHhết dòng không đổir. - D. Lỗi
ORA-01001, vì cursor tự đóng sau dòng cuối.
Đáp án và giải thích:
- Câu 1: B.
SELECT INTOkhông có dòng thì némNO_DATA_FOUND, vàSQLCODEcủa nó là +100 chứ không phải -1403 như số trong thông báo. - Câu 2: B. Oracle chỉ hoàn tác câu lỗi, các câu trước đó được giữ khi lỗi đã được bắt và khối kết thúc bình thường.
- Câu 3: C.
FETCHkhông còn dòng thìrgiữ nguyên giá trị lần trước, nên dòng cuối được xử lý thêm một lần. ĐặtEXIT WHENngay sauFETCH.
Làm cùng mình
Chị thủ kho muốn kiểm tra nhanh các đầu sách tồn từ 3 cuốn trở xuống: sách hết hàng (tồn 0) thì bị báo lỗi mã -20020 và bỏ qua, các sách còn lại tính là xử lý được. Cuối cùng in số sách đã xử lý và số bị bỏ qua.
Bước 1. Vẽ ra giấy
Vẽ một băng chuyền chứa các sách tồn từ 3 trở xuống và một cái chuông ở cuối. Với tồn kho 10, 0, 5, 3 thì băng chuyền có những sách nào, theo thứ tự mã sách? Có bao nhiêu sách làm chuông reo? Ghi dự đoán.
Bước 2. Điền chỗ trống
DECLARE
CURSOR c_low (p_max NUMBER) IS
SELECT id, title, stock FROM books WHERE stock <= p_max ORDER BY id;
e_empty EXCEPTION;
-- TODO 1: gắn e_empty với mã lỗi -20020 bằng PRAGMA EXCEPTION_INIT
v_ok PLS_INTEGER := 0;
v_skipped PLS_INTEGER := 0;
BEGIN
FOR r_book IN c_low(3) LOOP
BEGIN
IF r_book.stock = 0 THEN
-- TODO 2: ném lỗi -20020 với nội dung 'Out of stock: ' nối với tên sách
NULL;
END IF;
v_ok := v_ok + 1;
EXCEPTION
-- TODO 3: bắt e_empty, tăng v_skipped và in tên sách bị bỏ qua
WHEN OTHERS THEN RAISE;
END;
END LOOP;
DBMS_OUTPUT.PUT_LINE('ok=' || v_ok || ' skipped=' || v_skipped);
END;
/
Gợi ý: PRAGMA EXCEPTION_INIT(tên, mã) đặt ngay sau khai báo exception, và RAISE_APPLICATION_ERROR(mã, nội dung) ném lỗi mang mã đó.
Bước 3. Dự đoán rồi chạy
Dòng cuối in ra ok và skipped bằng bao nhiêu? Đối chiếu với mục Lời giải và đáp án.
Bước 4. Vẽ lại ra giấy
Ghi một câu: nếu bạn bỏ khối con BEGIN ... EXCEPTION ... END và chỉ giữ một handler ngoài vòng lặp, kết quả ok và skipped đổi thế nào, và vì sao?
Thử phá nó đi
Bảy tình huống hay gặp, đều chạy thật [Đã chạy: Oracle 23.26.3]. Tình huống một là mở cursor hai lần:
DECLARE
CURSOR c_all IS SELECT id FROM books;
BEGIN
OPEN c_all;
OPEN c_all;
END;
/
DECLARE
*
ERROR at line 1:
ORA-06511: PL/SQL: cursor already open
ORA-06512: at line 2
ORA-06512: at line 5
Help: https://docs.oracle.com/error-help/db/ora-06511/
Cursor đã mở thì không OPEN lại được: lỗi ORA-06511, tên dựng sẵn là CURSOR_ALREADY_OPEN. Tình huống hai là đọc cursor chưa mở:
DECLARE
CURSOR c_all IS SELECT id FROM books;
r_book c_all%ROWTYPE;
BEGIN
FETCH c_all INTO r_book;
END;
/
DECLARE
*
ERROR at line 1:
ORA-01001: cursor number is invalid or does not exist
ORA-06512: at line 5
Help: https://docs.oracle.com/error-help/db/ora-01001/
ORA-01001 cũng xuất hiện khi bạn FETCH từ cursor đã CLOSE. Tình huống ba là đặt điều kiện thoát sau khi đã xử lý dòng:
DECLARE
CURSOR c_low IS SELECT id, title FROM books WHERE stock <= 3 ORDER BY id;
r_book c_low%ROWTYPE;
BEGIN
OPEN c_low;
LOOP
FETCH c_low INTO r_book;
DBMS_OUTPUT.PUT_LINE('processing ' || r_book.title);
EXIT WHEN c_low%NOTFOUND;
END LOOP;
CLOSE c_low;
END;
/
processing PL/SQL Intro
processing Java Core
processing Java Core
Java Core được xử lý hai lần vì lần FETCH hết dòng không ghi đè r_book. Tình huống bốn là đặt WHEN OTHERS không ở cuối:
BEGIN
NULL;
EXCEPTION
WHEN OTHERS THEN
NULL;
WHEN NO_DATA_FOUND THEN
NULL;
END;
/
WHEN OTHERS THEN
*
ERROR at line 4:
ORA-06550: line 4, column 3:
PLS-00370: OTHERS handler must be last among the exception handlers of a block
ORA-06550: line 0, column 0:
PL/SQL: Compilation unit analysis terminated
Help: https://docs.oracle.com/error-help/db/ora-06550/
Tình huống năm là bẫy nuốt lỗi. Handler rỗng làm lỗi biến mất:
DECLARE
PROCEDURE take_order (p_id NUMBER, p_qty NUMBER) IS
BEGIN
UPDATE books SET stock = stock - p_qty WHERE id = p_id;
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
BEGIN
take_order(3, 99);
DBMS_OUTPUT.PUT_LINE('order accepted');
END;
/
SELECT stock FROM books WHERE id = 3;
order accepted
STOCK
-----
5
Hệ thống báo order accepted dù tồn kho không đổi, vì WHEN OTHERS THEN NULL đã nuốt mất lỗi. Cách làm đúng là ghi lại rồi ném tiếp lỗi lên trên bằng RAISE; [Đã chạy: Oracle 23.26.3]:
DECLARE
PROCEDURE take_order (p_id NUMBER, p_qty NUMBER) IS
BEGIN
UPDATE books SET stock = stock - p_qty WHERE id = p_id;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('take_order failed, code ' || SQLCODE);
RAISE;
END;
BEGIN
take_order(3, 99);
DBMS_OUTPUT.PUT_LINE('order accepted');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('caller knows: ' || SQLCODE);
END;
/
take_order failed, code -2290
caller knows: -2290
Tình huống sáu là lỗi trong phần khai báo. Handler của cùng khối không bắt được nó:
DECLARE
v_small NUMBER(2) := 100;
BEGIN
NULL;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('own handler caught it');
END;
/
DECLARE
*
ERROR at line 1:
ORA-06502: PL/SQL: value or conversion error: number precision too large
ORA-06512: at line 2
Help: https://docs.oracle.com/error-help/db/ora-06502/
Tình huống bảy cho thấy vị trí của lỗi quyết định ai bắt. Khi biến khai báo lỗi nằm trong khối con, handler của khối con cũng không bắt, nhưng khối bao ngoài thì bắt được [Đã chạy: Oracle 23.26.3]:
BEGIN
BEGIN
DECLARE
v_small NUMBER(2) := 100;
BEGIN
NULL;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('inner handler caught it');
END;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('outer handler caught it, code ' || SQLCODE);
END;
END;
/
outer handler caught it, code -6502
| Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
ORA-06511 | OPEN cursor đã mở | Kiểm tra %ISOPEN, hoặc dùng vòng FOR |
ORA-01001 | FETCH hoặc CLOSE cursor chưa mở hoặc đã đóng | Mở trước khi đọc, đừng dùng lại sau CLOSE |
| Dòng cuối xử lý hai lần | EXIT WHEN %NOTFOUND đặt sau phần xử lý | Đặt ngay sau FETCH |
PLS-00370 | WHEN OTHERS không đứng cuối | Đưa WHEN OTHERS xuống cuối cùng |
| Lỗi biến mất không dấu vết | WHEN OTHERS THEN NULL | Ghi SQLCODE rồi RAISE;, hoặc chỉ bắt lỗi đã biết cách xử lý |
ORA-06502 trong DECLARE | Lỗi khởi tạo không được bắt bởi handler cùng khối | Chuyển phần khởi tạo vào BEGIN, hoặc bọc trong khối ngoài |
Tự tay làm
Dùng chung bảng books và sales_log của bài.
Cơ bản
Bài 1. Dự đoán cursor ẩn. Chạy lần lượt UPDATE books SET price = price WHERE stock < 5, UPDATE books SET price = price WHERE id = 99, rồi SELECT COUNT(*) INTO v_count FROM books WHERE id = 99. Tiêu chí đạt: sau mỗi câu, ghi trước SQL%ROWCOUNT và SQL%FOUND. Gợi ý: câu cuối đếm ra 0 nhưng COUNT(*) luôn trả đúng một dòng.
Vận dụng
Bài 2. Hai sách rẻ nhất. Dùng cursor tường minh đọc các đầu sách theo giá tăng dần và chỉ in hai sách đầu. Tiêu chí đạt: in hai dòng, rồi dòng stopped at rowcount N với N do bạn dự đoán trước. Gợi ý: điều kiện thoát c%NOTFOUND OR c%ROWCOUNT > 2 được kiểm tra sau mỗi FETCH.
Thử thách (tùy chọn)
Bài 3. Hủy cả cặp. Có procedure reserve(p_id, p_qty) ném ORA-20010 khi không đủ tồn kho. Viết reserve_pair(p_id1, p_id2) giữ chỗ 2 cuốn sách thứ nhất và 1 cuốn sách thứ hai, nếu bước nào lỗi thì hủy cả hai và ném lỗi lên tiếp. Tiêu chí đạt: reserve_pair(3, 2) báo mã -20010 và tồn kho cả 4 sách giữ nguyên 10, 0, 5, 3. Gợi ý: SAVEPOINT đầu procedure, ROLLBACK TO rồi RAISE; trong handler.
Tự kiểm tra
- (Mục tiêu 1, nhớ lại) Thuộc tính nào của cursor ẩn
SQLluôn bằng sai, và vì sao? - (Mục tiêu 1, giải thích) Vì sao
SELECT COUNT(*) INTO v FROM books WHERE id = 99không némNO_DATA_FOUNDcònSELECT title INTO ... WHERE id = 99thì có? - (Mục tiêu 2, dự đoán)
RAISE e_out_of_stock;vớie_out_of_stocktự khai báo và được bắt.SQLCODEvàSQLERRMtrong handler là gì? - (Mục tiêu 3, biến thể) Khối con trong thân vòng lặp có handler cho
NO_DATA_FOUND, nhưng lỗi xảy ra lại làORA-02290. Lỗi đi về đâu? - (Mục tiêu 4, tìm lỗi) Handler
WHEN OTHERS THEN NULLở cuối một procedure thanh toán gây hậu quả gì cho nơi gọi?
Điều cần nhớ
- Cursor ẩn
SQL%ROWCOUNTvàSQL%FOUNDcho biết câuDMLvừa rồi tác động bao nhiêu dòng;UPDATEkhông khớp dòng nào thì không lỗi,SELECT INTOkhông khớp thì lỗi. - Cursor tường minh cần
OPEN,FETCH,CLOSE. Điều kiện thoát%NOTFOUNDphải ngay sauFETCH. VòngFORtự lo cả ba bước. - Exception có ba nguồn tên: dựng sẵn, tự khai báo, và
PRAGMA EXCEPTION_INITcho mãORA-. Lỗi tự đặt dùngRAISE_APPLICATION_ERRORvới mã từ -20999 đến -20000. - Lỗi không bắt thì đi lên mọi khối bao ngoài tới nơi gọi. Lỗi trong khai báo hoặc trong handler không được handler cùng khối bắt.
- Bắt được lỗi thì việc trước đó được giữ lại, không bắt thì lời gọi bị hoàn tác. Khối con trong vòng lặp giữ dòng tốt,
SAVEPOINThủy cả lô.
Cheat sheet:
- Cursor ẩn:
SQL%FOUND,SQL%NOTFOUND,SQL%ROWCOUNT,SQL%ISOPENngay sau câu lệnh. - Cursor tường minh:
OPEN c(...),FETCH c INTO r,EXIT WHEN c%NOTFOUND,CLOSE c; hoặcFOR r IN c(...) LOOP. - Bắt lỗi:
EXCEPTION WHEN tên THEN ... WHEN OTHERS THEN ...vớiWHEN OTHERSở cuối. - Đặt tên mã lỗi:
PRAGMA EXCEPTION_INIT(e_name, -2290);. Ném lỗi riêng:RAISE_APPLICATION_ERROR(-20010, 'nội dung'). - Ném lại:
RAISE;trong handler. Hoàn tác từng phần:SAVEPOINT s;rồiROLLBACK TO s;.
Hình tóm tắt của bài là sơ đồ vòng đời cursor ở mục Bức tranh lớn và sơ đồ lan truyền lỗi ở mục Khái niệm cốt lõi.
Câu hỏi để dành cho ngày mai: trong chương này, bạn đã quen với dữ liệu nằm trong bảng có cột cố định, phiếu có trường cố định, và ràng buộc CHECK chặn dữ liệu sai trước khi nó vào. Chương sau chuyển sang MongoDB, nơi một dòng thông tin là một document có thể thêm bớt trường tùy ý. Khi không có bảng và ràng buộc chặn sẵn, ai sẽ là người phát hiện dòng dữ liệu sai?
Đào sâu
- Cơ chế bên trong. Oracle hoàn tác theo câu lệnh: câu gây lỗi luôn bị hủy riêng nó, kể cả khi nó tác động nhiều dòng. Khối
EXCEPTIONkhông tự thêm điểm hoàn tác như khốiEXCEPTIONcủa PL/pgSQL mà bạn đã học ở phần PostgreSQL: việc đã làm vẫn còn nếu bạn bắt lỗi [Đã chạy: Oracle 23.26.3]:
BEGIN
UPDATE books SET stock = stock + 1 WHERE id = 1;
UPDATE books SET stock = stock - 99 WHERE id = 2;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('handled, code ' || SQLCODE);
END;
/
SELECT id, stock FROM books ORDER BY id;
handled, code -2290
ID STOCK
--- -----
1 11
2 0
3 5
4 3
Cùng hai câu nhưng không có handler thì cả lời gọi bị hoàn tác [Đã chạy: Oracle 23.26.3]:
BEGIN
UPDATE books SET stock = stock + 1 WHERE id = 1;
UPDATE books SET stock = stock - 99 WHERE id = 2;
END;
/
SELECT id, stock FROM books ORDER BY id;
BEGIN
*
ERROR at line 1:
ORA-02290: check constraint (SHOP.BOOKS_STOCK_CK) violated
ORA-06512: at line 3
Help: https://docs.oracle.com/error-help/db/ora-02290/
ID STOCK
--- -----
1 10
2 0
3 5
4 3
Muốn hủy cả lô khi đã bắt lỗi thì dùng SAVEPOINT và ROLLBACK TO [Đã chạy: Oracle 23.26.3]:
BEGIN
SAVEPOINT before_batch;
UPDATE books SET stock = stock + 1 WHERE id = 1;
UPDATE books SET stock = stock - 99 WHERE id = 2;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO before_batch;
DBMS_OUTPUT.PUT_LINE('rolled back to savepoint, code ' || SQLCODE);
END;
/
SELECT id, stock FROM books ORDER BY id;
rolled back to savepoint, code -2290
ID STOCK
--- -----
1 10
2 0
3 5
4 3
- Exception tự khai báo và mã lỗi riêng. Exception tự khai báo có
SQLCODEbằng 1 vàSQLERRMlàUser-Defined Exception. Nếu không ai bắt thì lỗi tới nơi gọi dưới dạngORA-06510[Đã chạy: Oracle 23.26.3]:
DECLARE
e_out_of_stock EXCEPTION;
v_stock books.stock%TYPE;
BEGIN
SELECT stock INTO v_stock FROM books WHERE id = 2;
IF v_stock = 0 THEN
RAISE e_out_of_stock;
END IF;
DBMS_OUTPUT.PUT_LINE('never printed');
EXCEPTION
WHEN e_out_of_stock THEN
DBMS_OUTPUT.PUT_LINE('handled: code=' || SQLCODE || ', message=' || SQLERRM);
END;
/
handled: code=1, message=User-Defined Exception
DECLARE
e_out_of_stock EXCEPTION;
BEGIN
RAISE e_out_of_stock;
END;
/
DECLARE
*
ERROR at line 1:
ORA-06510: PL/SQL: unhandled user-defined exception
ORA-06512: at line 4
Help: https://docs.oracle.com/error-help/db/ora-06510/
Với lỗi mang mã riêng từ -20999 đến -20000, người gọi bắt được theo tên sau khi dùng EXCEPTION_INIT. Procedure reserve ném ORA-20010 khi không đủ hàng [Đã chạy: Oracle 23.26.3]:
DECLARE
e_short EXCEPTION;
PRAGMA EXCEPTION_INIT(e_short, -20010); -- gắn tên cho mã lỗi tự đặt
PROCEDURE try_reserve (p_id NUMBER, p_qty NUMBER) IS
BEGIN
reserve(p_id, p_qty);
DBMS_OUTPUT.PUT_LINE('book ' || p_id || ': reserved ' || p_qty);
EXCEPTION
WHEN e_short THEN
DBMS_OUTPUT.PUT_LINE('book ' || p_id || ': ' || SQLERRM);
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('book ' || p_id || ': unknown book, code ' || SQLCODE);
END;
BEGIN
try_reserve(3, 2);
try_reserve(2, 1);
try_reserve(99, 1);
END;
/
book 3: reserved 2
book 2: ORA-20010: Not enough stock for book 2
book 99: unknown book, code 100
Mã ngoài khoảng cho phép bị từ chối [Đã chạy: Oracle 23.26.3]:
BEGIN
RAISE_APPLICATION_ERROR(-19999, 'too low');
END;
/
BEGIN
*
ERROR at line 1:
ORA-21000: error number argument to raise_application_error of -19999 is out of range
ORA-06512: at line 2
Help: https://docs.oracle.com/error-help/db/ora-21000/
- Đọc dấu vết lỗi.
DBMS_UTILITY.FORMAT_ERROR_STACKtrả thông báo lỗi, cònFORMAT_ERROR_BACKTRACEtrả đường đi qua các chương trình, mỗi dòngORA-06512là một lớp gọi. Với hai procedureclose_daygọicheck_stock[Đã chạy: Oracle 23.26.3]:
BEGIN
close_day;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('--- error stack');
DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_STACK);
DBMS_OUTPUT.PUT_LINE('--- backtrace');
DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END;
/
--- error stack
ORA-20011: Book 2 is out of stock
ORA-06512: at "SHOP.CHECK_STOCK", line 6
ORA-06512: at "SHOP.CLOSE_DAY", line 4
--- backtrace
ORA-06512: at "SHOP.CHECK_STOCK", line 6
ORA-06512: at "SHOP.CLOSE_DAY", line 4
ORA-06512: at line 2
Hai thông tin này chỉ có trong handler. Nếu không có handler thì SQL*Plus tự in cả hai [Đã chạy: Oracle 23.26.3]:
BEGIN
close_day;
END;
/
BEGIN
*
ERROR at line 1:
ORA-20011: Book 2 is out of stock
ORA-06512: at "SHOP.CHECK_STOCK", line 6
ORA-06512: at "SHOP.CLOSE_DAY", line 4
ORA-06512: at line 2
- Lỗi trong handler. Một lỗi ném ra từ trong handler không rơi vào handler anh em cùng khối, mà đi lên khối bao ngoài [Đã chạy: Oracle 23.26.3]:
BEGIN
BEGIN
RAISE NO_DATA_FOUND;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE TOO_MANY_ROWS;
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('sibling handler runs');
END;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('outer block caught TOO_MANY_ROWS');
END;
/
outer block caught TOO_MANY_ROWS
- Mã lỗi phụ thuộc ngữ cảnh. Chuyển chuỗi không phải số sang số cho ra hai exception khác nhau tùy chỗ chạy [Đã chạy: Oracle 23.26.3]:
DECLARE
v_n NUMBER;
BEGIN
BEGIN
v_n := TO_NUMBER('12x');
EXCEPTION
WHEN INVALID_NUMBER THEN DBMS_OUTPUT.PUT_LINE('PL/SQL assignment: INVALID_NUMBER');
WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('PL/SQL assignment: VALUE_ERROR');
END;
BEGIN
SELECT TO_NUMBER('12x') INTO v_n FROM dual;
EXCEPTION
WHEN INVALID_NUMBER THEN DBMS_OUTPUT.PUT_LINE('inside SQL: INVALID_NUMBER');
WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('inside SQL: VALUE_ERROR');
END;
END;
/
PL/SQL assignment: VALUE_ERROR
inside SQL: INVALID_NUMBER
Trong phép gán của PL/SQL là VALUE_ERROR, bên trong câu SQL là INVALID_NUMBER. Handler phải khớp đúng ngữ cảnh.
- REF CURSOR. Khi truy vấn quyết định lúc chạy hoặc cần trả cursor cho nơi gọi, dùng
SYS_REFCURSOR. Function trả cursor chưa đọc, nơi gọi tựFETCHhoặcPRINT[Đã chạy: Oracle 23.26.3]:
CREATE OR REPLACE FUNCTION books_below (p_stock NUMBER) RETURN SYS_REFCURSOR AS
c_out SYS_REFCURSOR;
BEGIN
OPEN c_out FOR SELECT id, title FROM books WHERE stock < p_stock ORDER BY id;
RETURN c_out;
END books_below;
/
VARIABLE rc REFCURSOR
EXEC :rc := books_below(6)
PRINT rc
ID TITLE
--- ---------------
2 PL/SQL Intro
3 Data Modeling
4 Java Core
DECLARE
c_dyn SYS_REFCURSOR;
v_title books.title%TYPE;
v_price books.price%TYPE;
BEGIN
OPEN c_dyn FOR SELECT title, price FROM books WHERE price > 150000 ORDER BY price;
LOOP
FETCH c_dyn INTO v_title, v_price;
EXIT WHEN c_dyn%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_title || ' ' || v_price);
END LOOP;
CLOSE c_dyn;
END;
/
PL/SQL Intro 180000
Java Core 200000
FOR UPDATEvàWHERE CURRENT OF. Cursor cóFOR UPDATEkhóa các dòng được đọc, vàWHERE CURRENT OF csửa đúng dòng đang đứng [Đã chạy: Oracle 23.26.3]:
DECLARE
CURSOR c_low IS SELECT id, stock FROM books WHERE stock < 6 FOR UPDATE OF stock NOWAIT;
v_rows PLS_INTEGER := 0;
BEGIN
FOR r_book IN c_low LOOP
UPDATE books SET stock = stock + 10 WHERE CURRENT OF c_low;
v_rows := v_rows + 1;
END LOOP;
DBMS_OUTPUT.PUT_LINE('restocked ' || v_rows || ' rows');
END;
/
SELECT id, stock FROM books ORDER BY id;
restocked 3 rows
ID STOCK
--- -----
1 10
2 10
3 15
4 13
COMMIT giữa chừng làm mất khóa nên lần FETCH sau lỗi ORA-01002 [Đã chạy: Oracle 23.26.3]:
DECLARE
CURSOR c_low IS SELECT id, stock FROM books WHERE stock < 6 FOR UPDATE;
r_book c_low%ROWTYPE;
BEGIN
OPEN c_low;
FETCH c_low INTO r_book;
COMMIT;
FETCH c_low INTO r_book;
END;
/
DECLARE
*
ERROR at line 1:
ORA-01002: fetch out of sequence
ORA-06512: at line 8
Help: https://docs.oracle.com/error-help/db/ora-01002/
- Hiểu lầm nâng cao. Nhiều người tưởng
ROLLBACKhoàn tác cả dòng in ra củaDBMS_OUTPUT. Thực tế nó không thuộc giao dịch, nên màn hình có thể insold 2cho một việc đã bị hoàn tác, như phần Ví dụ đầu tiên đã cho thấy. Đừng coi dòng in ra là bằng chứng dữ liệu đã được ghi. - Giới hạn của bài. Bài chưa nói về
PRAGMA AUTONOMOUS_TRANSACTION(ghi nhật ký lỗi sống sót quaROLLBACK),LOG ERRORScủa DML, cursor expression, hayDBMS_SQL. - Đọc thêm. Oracle Database PL/SQL Language Reference, các mục "PL/SQL Error Handling" và "Static SQL": https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/
Bài tiếp theo
Chương sau là Giới thiệu MongoDB: cơ sở dữ liệu lưu dữ liệu dưới dạng document thay vì bảng có cột cố định. Mini-dự án nối tiếp: mô tả lại phiếu xuất kho của Nhà Sách Ông Đồ như một document và xem điều gì thay thế ràng buộc CHECK khi không còn bảng.
Lời giải và đáp án
Làm cùng mình, Bước 2. Điền ba chỗ trống:
DECLARE
CURSOR c_low (p_max NUMBER) IS
SELECT id, title, stock FROM books WHERE stock <= p_max ORDER BY id;
e_empty EXCEPTION;
PRAGMA EXCEPTION_INIT(e_empty, -20020);
v_ok PLS_INTEGER := 0;
v_skipped PLS_INTEGER := 0;
BEGIN
FOR r_book IN c_low(3) LOOP
BEGIN
IF r_book.stock = 0 THEN
RAISE_APPLICATION_ERROR(-20020, 'Out of stock: ' || r_book.title);
END IF;
v_ok := v_ok + 1;
EXCEPTION
WHEN e_empty THEN
v_skipped := v_skipped + 1;
DBMS_OUTPUT.PUT_LINE('skipped: ' || r_book.title);
END;
END LOOP;
DBMS_OUTPUT.PUT_LINE('ok=' || v_ok || ' skipped=' || v_skipped);
END;
/
skipped: PL/SQL Intro
ok=1 skipped=1
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
PRAGMA EXCEPTION_INIT(e_empty, -20020) | Gắn tên e_empty cho mã -20020 | Để handler bắt bằng tên thay vì so SQLCODE |
RAISE_APPLICATION_ERROR(-20020, ...) | Ném lỗi mang mã tự đặt, nội dung có tên sách | Khối con kết thúc ngay tại dòng này |
WHEN e_empty THEN | Tăng số bị bỏ qua và in tên sách | Khối con kết thúc bình thường nên vòng lặp tiếp tục |
v_ok := v_ok + 1 | Chỉ chạy khi không có lỗi | Nằm sau IF nên dòng lỗi không đếm vào |
Băng chuyền có hai sách theo mã: sách 2 (tồn 0) và sách 4 (tồn 3). Sách 2 làm chuông reo nên bị bỏ qua, sách 4 xử lý được, nên in ok=1 skipped=1. Nếu bỏ khối con và chỉ giữ một handler ngoài vòng lặp thì sách 2 làm cả vòng dừng ngay, nên ok là 0 và sách 4 không bao giờ được thử.
Bài 1.
DECLARE
v_count NUMBER;
BEGIN
UPDATE books SET price = price WHERE stock < 5;
DBMS_OUTPUT.PUT_LINE('rowcount=' || SQL%ROWCOUNT || ' found=' || CASE WHEN SQL%FOUND THEN 'Y' ELSE 'N' END);
UPDATE books SET price = price WHERE id = 99;
DBMS_OUTPUT.PUT_LINE('rowcount=' || SQL%ROWCOUNT || ' found=' || CASE WHEN SQL%FOUND THEN 'Y' ELSE 'N' END);
SELECT COUNT(*) INTO v_count FROM books WHERE id = 99;
DBMS_OUTPUT.PUT_LINE('count=' || v_count || ' rowcount=' || SQL%ROWCOUNT || ' found=' || CASE WHEN SQL%FOUND THEN 'Y' ELSE 'N' END);
END;
/
rowcount=2 found=Y
rowcount=0 found=N
count=0 rowcount=1 found=Y
Câu UPDATE đầu khớp 2 dòng (tồn 0 và 3). Câu thứ hai không khớp dòng nào nên ROWCOUNT là 0 và FOUND sai, nhưng không lỗi. Câu thứ ba đếm ra 0 và ROWCOUNT là 1, vì COUNT(*) luôn trả đúng một dòng.
Bài 2.
DECLARE
CURSOR c_cheap IS SELECT title, price FROM books ORDER BY price;
r_book c_cheap%ROWTYPE;
BEGIN
OPEN c_cheap;
LOOP
FETCH c_cheap INTO r_book;
EXIT WHEN c_cheap%NOTFOUND OR c_cheap%ROWCOUNT > 2;
DBMS_OUTPUT.PUT_LINE(c_cheap%ROWCOUNT || '. ' || r_book.title || ' ' || r_book.price);
END LOOP;
DBMS_OUTPUT.PUT_LINE('stopped at rowcount ' || c_cheap%ROWCOUNT);
CLOSE c_cheap;
END;
/
1. SQL Basics 120000
2. Data Modeling 150000
stopped at rowcount 3
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
EXIT WHEN c_cheap%NOTFOUND OR c_cheap%ROWCOUNT > 2 | Thoát khi hết dòng hoặc đã đọc quá 2 dòng | Kiểm tra ngay sau FETCH |
c_cheap%ROWCOUNT | Đếm số dòng đã FETCH | Lần đọc thứ ba đã tăng nó lên 3 trước khi thoát |
Dòng cuối là stopped at rowcount 3 vì dòng thứ ba đã được FETCH (nên ROWCOUNT bằng 3) rồi mới bị loại bởi điều kiện thoát.
Bài 3.
DECLARE
PROCEDURE reserve_pair (p_id1 NUMBER, p_id2 NUMBER) IS
BEGIN
SAVEPOINT before_pair;
reserve(p_id1, 2);
reserve(p_id2, 1);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO before_pair;
RAISE;
END;
BEGIN
reserve_pair(3, 2);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('pair failed, code ' || SQLCODE);
END;
/
SELECT id, stock FROM books ORDER BY id;
pair failed, code -20010
ID STOCK
--- -----
1 10
2 0
3 5
4 3
SAVEPOINT before_pair đánh dấu trước khi giữ chỗ sách đầu. Sách 2 hết hàng nên reserve ném -20010, handler ROLLBACK TO before_pair trả tồn kho sách 3 về 5, rồi RAISE; ném lại đúng lỗi cũ để nơi gọi biết cả cặp thất bại.
Tự kiểm tra.
SQL%ISOPEN, vì Oracle tự mở và đóng cursor ẩn ngay trong lúc chạy câu lệnh.COUNT(*)là hàm tập hợp nên luôn trả đúng một dòng (giá trị 0), cònSELECT titlekhông có dòng thìSELECT INTOnémNO_DATA_FOUND.SQLCODEbằng 1 vàSQLERRMlàUser-Defined Exception.- Handler của khối con không khớp nên lỗi đi lên khối bao ngoài, rồi lên nơi gọi nếu không ai bắt, và vòng lặp bị cắt ngang.
- Mọi lỗi thanh toán bị nuốt, nơi gọi nhận kết quả thành công dù tiền chưa được ghi, nên dữ liệu và thực tế lệch nhau mà không ai biết.