Giới thiệu PL/SQL và khối anonymous
Xem thửTình huống mở đầu
Quầy sách của bạn có 3 đầu sách trong bảng books. Mỗi tối bạn muốn một script nhỏ tính xem tồn kho mỗi đầu sách đủ bán trong bao nhiêu ngày: lấy số sách còn lại chia cho số cuốn bán trung bình mỗi ngày. Tối nay script chạy đến một cuốn mới nhập, chưa bán được cuốn nào, nên số bán mỗi ngày là 0.
Script đã in được một dòng tên sách rồi dừng. Hai câu hỏi cần trả lời: dòng tên sách đã in rồi có bị "rút lại" không, và những đầu sách còn lại có được xử lý không? Hãy đoán trước, cuối bài bạn sẽ có câu trả lời kèm lý do.
Mục tiêu bài học
Sau bài này bạn có thể:
- Viết một khối PL/SQL ẩn danh (anonymous block) có đủ ba phần
DECLARE,BEGIN,EXCEPTIONvà chạy được trong SQL*Plus. - Giải thích dấu
/vàSET SERVEROUTPUT ONlàm gì, và vì sao thiếu một trong hai thì "không thấy gì xảy ra". - Dự đoán dòng nào được in khi một lỗi xảy ra giữa phần
BEGIN, với và không có phầnEXCEPTION. - Nhận ra những lỗi cú pháp người mới hay gặp ở khối đầu tiên (thiếu
INTO, khối rỗng, thiếu dấu chấm phẩy).
Bức tranh lớn
Xuyên suốt bài này, một khối PL/SQL là một tờ phiếu giao việc có ba ô: ô chuẩn bị đồ nghề (DECLARE), ô làm việc (BEGIN), và ô ghi cách xử lý khi có sự cố (EXCEPTION). Dự án xuyên suốt chương 34 là quầy sách books, chạy trên Oracle AI Database 26ai Free (release 23.26.3).
Hình dưới trả lời một câu hỏi: máy đi qua ba ô của tờ phiếu theo thứ tự nào, và lỗi làm đường đi đổi ra sao?
Đang tải sơ đồ…
Cách đọc hình: đọc từ ô xanh dương ở trên cùng xuống. DECLARE chuẩn bị, BEGIN chạy từng dòng. Nếu không có lỗi, máy đi thẳng xuống END. Nếu có lỗi, máy rẽ vào ô cam: tìm một nhánh WHEN khớp với lỗi. Khớp thì chạy bộ xử lý rồi kết thúc êm ở END. Không khớp thì lỗi bị đẩy ra cho nơi gọi khối (ô đỏ). Chữ trong ô mới là nội dung chính, màu chỉ là tín hiệu phụ.
Chỗ ẩn dụ không còn đúng: tờ phiếu giấy thì ai cũng đọc được cả ba ô trước khi làm, còn khối PL/SQL chỉ nhìn ô EXCEPTION khi lỗi xảy ra. Ô đó không phải lúc nào cũng được dùng.
Khái niệm cốt lõi
Bài này có ba khái niệm.
1. Khối ẩn danh và ba phần của nó
Mã PL/SQL luôn nằm trong một khối (block). Khối ẩn danh (anonymous) là khối không có tên và không được lưu lại trong database: bạn gõ, chạy một lần, xong. Ba phần của khối:
DECLARE: khai báo biến, hằng, kiểu. Phần này tùy chọn, bỏ cũng được.BEGIN ... END;: các câu lệnh được thực thi. Đây là phần duy nhất bắt buộc, và nó phải có ít nhất một câu lệnh.EXCEPTION: xử lý lỗi xảy ra trong phầnBEGIN. Phần này tùy chọn và nằm trướcEND.
2. Gửi khối lên server: dấu / và DBMS_OUTPUT
SQLPlus đọc từng dòng và gom chúng vào một vùng đệm. Câu SQL thường kết thúc bằng dấu chấm phẩy, nhưng dấu chấm phẩy cuối END; là một phần của khối. Muốn gửi cả khối lên server, bạn gõ một dòng chỉ có dấu /. DBMS_OUTPUT.PUT_LINE không in thẳng ra màn hình: nó đưa chữ vào một bộ đệm trên server, và SQLPlus chỉ in bộ đệm đó ra khi SET SERVEROUTPUT ON.
3. Lỗi và EXCEPTION
Khi một dòng trong BEGIN gây lỗi, máy dừng chạy các dòng còn lại của BEGIN ngay lập tức và nhảy xuống EXCEPTION. Những gì đã làm trước chỗ lỗi (như đã in một dòng) không bị rút lại. Khái niệm này là khái niệm quan trọng nhất của bài.
Ví dụ đầu tiên
Khối nhỏ nhất đáng chạy: một biến, một dòng in.
SET SERVEROUTPUT ON
DECLARE
v_name VARCHAR2(50) := 'An';
BEGIN
DBMS_OUTPUT.PUT_LINE('Hello ' || v_name);
END;
/
Dự đoán trước khi xem kết quả: máy in ra gì, và còn dòng nào khác xuất hiện không?
Kết quả [Đã chạy: Oracle AI Database 26ai Free 23.26.3, SQL*Plus]:
Hello An
PL/SQL procedure successfully completed.
Dòng thứ hai là thông báo của chính SQL*Plus ("feedback"), không phải của khối. Từ đây trở đi bài tắt thông báo này bằng SET FEEDBACK OFF cho gọn.
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
SET SERVEROUTPUT ON | Bảo SQL*Plus in ra những gì khối đẩy vào bộ đệm DBMS_OUTPUT | Thiếu dòng này khối vẫn chạy nhưng bạn không thấy gì |
v_name VARCHAR2(50) := 'An'; | Tạo biến chuỗi dài tối đa 50 ký tự và gán An ngay lúc khai báo | Phần DECLARE là nơi duy nhất khai báo biến của khối |
DBMS_OUTPUT.PUT_LINE('Hello ' ... | Nối chuỗi Hello với biến v_name bằng toán tử nối chuỗi rồi đẩy vào bộ đệm đầu ra | Đây là cách đơn giản nhất để quan sát khối đang làm gì |
END; rồi dòng / | Đóng khối, rồi gửi khối lên server để chạy | Dấu / phải nằm một mình trên dòng của nó |
Bây giờ đến khối thật của quầy sách: có đủ ba phần, đọc dữ liệu từ bảng và gặp đúng lỗi chia cho 0 của tình huống mở đầu.
SET FEEDBACK OFF
DECLARE
v_title VARCHAR2(60);
v_stock NUMBER;
v_sold_per_day NUMBER := 0;
v_days NUMBER;
BEGIN
SELECT title, stock INTO v_title, v_stock FROM books WHERE id = 1;
DBMS_OUTPUT.PUT_LINE('Book: ' || v_title);
v_days := v_stock / v_sold_per_day; -- sách chưa bán cuốn nào nên chia cho 0
DBMS_OUTPUT.PUT_LINE('Days of stock: ' || v_days);
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('No sales yet for this title');
END;
/
Dự đoán trước khi xem: có bao nhiêu dòng được in, và dòng Days of stock có xuất hiện không?
Kết quả [Đã chạy: Oracle 23.26.3]:
Book: SQL Basics
No sales yet for this title
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
v_sold_per_day NUMBER := 0; | Tạo biến số và gán 0, giả lập cuốn sách chưa bán được cuốn nào | Cố ý đặt 0 để có lỗi thật, dễ quan sát |
SELECT title, stock INTO v_title, v_stock FROM books WHERE id = 1; | Chạy truy vấn, chép hai cột của đúng một dòng vào hai biến | INTO là chỗ SQL gặp PL/SQL: trong khối, SELECT bắt buộc phải có nơi để đổ kết quả |
v_days := v_stock / v_sold_per_day; | Chia 10 cho 0, Oracle ném lỗi ZERO_DIVIDE | Dòng này là chỗ lỗi xảy ra, mọi dòng sau nó trong BEGIN bị bỏ qua |
WHEN ZERO_DIVIDE THEN | Bắt đúng loại lỗi chia cho 0 và chạy các dòng bên dưới | Chỉ bắt đúng lỗi mình lường trước, không nuốt mọi lỗi |
Còn nếu bỏ phần EXCEPTION, lỗi không ai bắt và đi thẳng ra ngoài:
SET FEEDBACK OFF
DECLARE
v_title VARCHAR2(60);
v_stock NUMBER;
v_sold_per_day NUMBER := 0;
v_days NUMBER;
BEGIN
SELECT title, stock INTO v_title, v_stock FROM books WHERE id = 1;
DBMS_OUTPUT.PUT_LINE('Book: ' || v_title);
v_days := v_stock / v_sold_per_day;
DBMS_OUTPUT.PUT_LINE('Days of stock: ' || v_days);
END;
/
Book: SQL Basics
DECLARE
*
ERROR at line 1:
ORA-01476: divisor is equal to zero
ORA-06512: at line 9
Help: https://docs.oracle.com/error-help/db/ora-01476/
Dòng Book: SQL Basics vẫn được in vì lệnh in chạy trước chỗ lỗi, nhưng khối kết thúc bằng thông báo lỗi ORA-01476 thay vì kết thúc êm. Dòng ORA-06512: at line 9 trỏ vào vị trí lỗi trong khối: bạn đếm dòng theo khối vừa gõ.
Xem từng bước
Animation dưới đây chạy lại khối shop, từng ô một. Dùng nút chuyển để so sánh với lần chạy không có lỗi. Hãy trả lời câu dự đoán trước khi bấm bước đầu tiên.
Đang tải Animation…
Bảng trace bằng chữ [Đã chạy: hai lần chạy trên Oracle 23.26.3, cùng bảng books]:
| Bước | v_sold_per_day = 0 | v_sold_per_day = 2 |
|---|---|---|
| Dòng in thứ nhất | Book: SQL Basics | Book: SQL Basics |
| Dòng tính v_days | Lỗi ZERO_DIVIDE | v_days bằng 5 |
| Dòng in thứ hai | Không chạy | Days of stock: 5 |
| Phần EXCEPTION | Chạy, in No sales yet for this title | Bị bỏ qua |
Bạn vừa làm được: nhìn một khối PL/SQL và nói chính xác dòng nào chạy, dòng nào bị bỏ qua, và vì sao.
Concept Check
Khối trong shop với v_sold_per_day bằng 0.
Câu 1. Máy ném lỗi ZERO_DIVIDE ở dòng tính v_days. Dòng Book: SQL Basics in trước đó thì sao?
- A. Vẫn nằm trong kết quả, vì lệnh in đã chạy xong trước khi lỗi xảy ra.
- B. Biến mất, vì lỗi làm cả khối bị hoàn tác từ đầu.
- C. Bị in lại một lần nữa bên trong phần
EXCEPTION. - D. Chỉ hiện nếu khối kết thúc không có lỗi nào.
Câu 2. Dòng Days of stock nằm ngay sau dòng tính v_days. Nó có chạy không?
- A. Có, vì lỗi chỉ được ghi nhận rồi khối chạy tiếp.
- B. Có, nhưng in ra giá trị rỗng vì
v_dayschưa được gán. - C. Không, vì khi lỗi xảy ra máy bỏ các dòng còn lại của
BEGINvà nhảy sangEXCEPTION. - D. Không, vì phần
EXCEPTIONluôn thay thế toàn bộ kết quả của phầnBEGIN.
Câu 3. Bạn chạy khối và không thấy dòng nào, cũng không có lỗi. Nguyên nhân nào có thể xảy ra?
- A. Khối bị lỗi biên dịch và SQL*Plus giấu thông báo.
- B. Chưa gõ dòng
/để gửi khối, hoặcSERVEROUTPUTđang tắt. - C. Bảng
booksđang trống nên khối bỏ qua mọi câu lệnh. - D. Phần
DECLAREbị thiếu nên khối không được phép chạy.
Đáp án và giải thích:
- Câu 1: A. B sai vì PL/SQL không tự hoàn tác các lệnh in và gán đã chạy; hoàn tác chỉ liên quan đến thay đổi dữ liệu và có lệnh riêng. C sai vì không có cơ chế in lại. D sai vì lệnh in chạy ngay lúc nó được gọi, không phụ thuộc kết cục của cả khối.
- Câu 2: C. A sai vì lỗi không được "ghi nhận rồi bỏ qua": nó làm đổi luồng chạy. B sai vì dòng đó không bao giờ được thử. D sai vì
EXCEPTIONchỉ chạy thêm nhánh xử lý, không xóa kết quả đã có. - Câu 3: B. A sai vì lỗi biên dịch luôn hiện thông báo
PLS-. C sai vì bảng trống không làm khối im lặng, nó chỉ gây lỗiNO_DATA_FOUND. D sai vìDECLARElà phần tùy chọn.
Làm cùng mình
Quầy đang giảm giá 10% cho sách Data Modeling (id bằng 3). Bạn viết khối tính và in giá mới.
Bước 1. Đoán trên giấy
Giá gốc của sách có id 3 là 150000. Ghi trước dự đoán: giá sau giảm là bao nhiêu, và khối cần mấy biến.
Bước 2. Hoàn thành khối
SET FEEDBACK OFF
DECLARE
v_title VARCHAR2(60);
v_price NUMBER;
v_sale NUMBER;
BEGIN
-- TODO 1: lấy title và price của sách có id = 3 vào v_title và v_price
-- TODO 2: tính v_sale bằng giá gốc nhân 0.9
DBMS_OUTPUT.PUT_LINE(v_title || ': ' || v_sale);
END;
/
Gợi ý cho chỗ trống thứ nhất: dùng SELECT ... INTO ... FROM books WHERE id = 3; như khối shop. Gợi ý cho chỗ trống thứ hai: phép gán trong PL/SQL là :=.
Chạy xong bạn sẽ thấy [Đã chạy] một dòng có tên sách và giá mới. Đáp án ở mục Lời giải và đáp án.
Bước 3. Thêm một lỗi cố ý
Thêm vào cuối phần BEGIN một dòng v_sale := v_price / (v_price - 150000); rồi thêm phần EXCEPTION bắt ZERO_DIVIDE và in SQLERRM. Dự đoán trước: khối in mấy dòng, và dòng thứ hai nói gì?
Bước 4. Ghi lại
Viết một câu: vì sao dòng giá giảm vẫn được in dù khối gặp lỗi sau đó.
Thử phá nó đi
Bốn lỗi người mới thường gặp ở khối đầu tiên [Đã chạy: Oracle 23.26.3]:
| Code lỗi | Thông báo hoặc hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|---|
SELECT title FROM books WHERE id = 1; đặt trong BEGIN | PLS-00428: an INTO clause is expected in this SELECT statement | Trong PL/SQL, SELECT cần nơi chứa kết quả | Thêm INTO v_title vào trước FROM |
BEGIN END; không có câu lệnh nào | PLS-00103: Encountered the symbol "END" | Phần BEGIN phải có ít nhất một câu lệnh | Viết NULL; nếu cố ý để trống |
Quên dòng / sau END; | Không có thông báo nào, không in gì | Khối nằm trong vùng đệm, chưa được gửi lên server | Thêm dòng / đứng một mình |
SET SERVEROUTPUT OFF | Khối chạy xong nhưng không in gì | Bộ đệm DBMS_OUTPUT không được hiển thị | Gõ SET SERVEROUTPUT ON |
Hai trường hợp đầu có thông báo lỗi thật. Đây là lỗi PLS-00428:
SET FEEDBACK OFF
BEGIN
SELECT title FROM books WHERE id = 1;
END;
/
SELECT title FROM books WHERE id = 1;
*
ERROR at line 2:
ORA-06550: line 2, column 3:
PLS-00428: an INTO clause is expected in this SELECT statement
Help: https://docs.oracle.com/error-help/db/ora-06550/
Hai trường hợp sau nguy hiểm hơn vì im lặng. Đây là khối không có dòng /, chạy xong im lặng đúng như bảng nói:
SET FEEDBACK OFF
BEGIN
DBMS_OUTPUT.PUT_LINE('hello from a block without slash');
END;
Kết quả rỗng ở trên là kết quả thật: không có dòng nào được in. Còn đây là SERVEROUTPUT tắt rồi bật lại, với cùng một kiểu khối:
SET FEEDBACK OFF
SET SERVEROUTPUT OFF
BEGIN
DBMS_OUTPUT.PUT_LINE('can you see me?');
END;
/
SET SERVEROUTPUT ON
BEGIN
DBMS_OUTPUT.PUT_LINE('now you can');
END;
/
now you can
Chỉ lệnh in sau khi bật lại mới hiện ra. Hình dưới vẽ đường đi từ lúc bạn gõ đến lúc có chữ trên màn hình, để thấy hai chỗ có thể chặn: dòng / và SERVEROUTPUT.
Đang tải sơ đồ…
Cách đọc hình: đi từ ô xanh dương xuống. Ô tím đầu tiên là vùng đệm: khối chỉ đi tiếp khi gặp dòng /. Nếu server biên dịch lỗi, ô đỏ cho thấy không có dòng nào chạy. Nếu biên dịch xong, khối chạy và đẩy chữ vào bộ đệm đầu ra (ô cyan), nhưng chữ chỉ hiện lên màn hình khi SERVEROUTPUT bật (ô xanh lá).
Tự tay làm
Dùng chung bảng books của bài. Mỗi bài nêu đầu vào, kết quả mong đợi và tiêu chí đạt.
Cơ bản
Bài 1. Điền bảng trace. Đầu vào: khối dưới đây, chạy hai lần, một lần với v_qty bằng 5 và một lần với v_qty bằng 4.
DECLARE
v_qty NUMBER := 5;
BEGIN
DBMS_OUTPUT.PUT_LINE('A');
v_qty := 10 / (v_qty - 5);
DBMS_OUTPUT.PUT_LINE('B');
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('C');
END;
/
Điền bảng:
| v_qty ban đầu | Các dòng được in | Phần EXCEPTION có chạy không |
|---|---|---|
| 5 | ? | ? |
| 4 | ? | ? |
Tiêu chí đạt: điền đúng cả sáu ô. Gợi ý: tính v_qty - 5 trước, rồi xem phép chia có hợp lệ không.
Vận dụng
Bài 2. Giá trị tồn kho. Đầu vào: bảng books. Viết một khối đọc title và price * stock của sách có id bằng 3 vào hai biến rồi in Stock value of <tên sách>: <giá trị>. Tiêu chí đạt: in đúng một dòng với giá trị 750000. Gợi ý: bạn có thể đặt một biểu thức, như price * stock, ngay sau SELECT.
Thử thách (tùy chọn)
Bài 3. Lỗi không được phá cả báo cáo. Đầu vào: biến v_stock bằng 10 và v_sold bằng 0. Viết khối in Days of stock: ... bằng phép chia v_stock / v_sold, nhưng nếu chia cho 0 thì chỉ in Skip: no sales yet, và luôn in dòng Report finished ở cuối. Tiêu chí đạt: kết quả có đúng hai dòng là Skip: no sales yet và Report finished. Gợi ý: một khối BEGIN ... END; có thể nằm bên trong phần BEGIN của khối ngoài, và khối trong có thể có EXCEPTION riêng.
Tự kiểm tra
Trả lời bằng lời của bạn, không nhìn lại bài. Mỗi câu ghi mục tiêu được kiểm tra.
- (Mục tiêu 1, nhớ lại) Ba phần của khối ẩn danh là gì, phần nào bắt buộc và phần nào tùy chọn?
- (Mục tiêu 2, giải thích) Vì sao khối chạy mà không thấy chữ nào, dù không có lỗi? Nêu hai nguyên nhân.
- (Mục tiêu 3, dự đoán) Trong
BEGINcó ba lệnh in A, rồi một phép chia cho 0, rồi in B. Khối có phầnEXCEPTIONbắtZERO_DIVIDEvà in C. Những dòng nào được in? - (Mục tiêu 3, dự đoán) Cùng khối ở câu 3 nhưng bỏ hẳn phần
EXCEPTION. Người chạy thấy gì? - (Mục tiêu 4, tìm lỗi) Khối
BEGIN SELECT title FROM books WHERE id = 1; END;bị lỗi gì, và sửa thế nào?
Điều cần nhớ
- Khối PL/SQL có ba phần:
DECLARE(tùy chọn),BEGIN ... END(bắt buộc, có ít nhất một lệnh) vàEXCEPTION(tùy chọn). - Khối chỉ chạy khi nhận được dòng
/, và chữ từDBMS_OUTPUTchỉ hiện khiSERVEROUTPUTbật. - Khi có lỗi trong
BEGIN, các dòng đã chạy giữ nguyên kết quả, các dòng sau chỗ lỗi bị bỏ qua, và máy nhảy sangEXCEPTION. - Trong PL/SQL,
SELECTphải cóINTOđể đổ kết quả vào biến.
Cheat sheet:
| Muốn | Viết | Ghi chú |
|---|---|---|
| Khai báo biến | v_name VARCHAR2(50) := 'An'; trong DECLARE | Biến chuỗi nên có độ dài |
| Gán giá trị | v_days := v_stock / 2; | Dùng :=, không phải = |
| In ra màn hình | DBMS_OUTPUT.PUT_LINE('text'); | Cần SET SERVEROUTPUT ON |
| Đọc một dòng từ bảng | SELECT col INTO v_col FROM t WHERE ...; | Đúng một dòng |
| Bắt lỗi chia cho 0 | WHEN ZERO_DIVIDE THEN ... | Nằm trong phần EXCEPTION |
| Câu lệnh rỗng | NULL; | Thay cho chỗ phải có lệnh |
| Gửi khối chạy | dòng / đứng một mình | Sau END; |
Hình tóm tắt của bài là sơ đồ ở mục Bức tranh lớn.
Câu hỏi để dành cho ngày mai: khối ẩn danh chạy xong là biến mất. Nếu quầy sách muốn tái dùng cùng đoạn tính ngày tồn kho cho mọi đầu sách, bạn lưu nó ở đâu?
Đào sâu
Phần này dành cho bạn tò mò.
- Cơ chế bên trong. Oracle cho phép khai báo
VARCHAR2không có độ dài trongDECLARE. Trên 23.26.3 khối dưới đây chạy được [Đã chạy: Oracle 23.26.3], nhưng bài này luôn ghi độ dài để mã đọc rõ và chạy giống nhau ở mọi bản:
SET FEEDBACK OFF
DECLARE
v_x VARCHAR2;
BEGIN
v_x := 'no length declared';
DBMS_OUTPUT.PUT_LINE(v_x);
END;
/
no length declared
- So sánh với lựa chọn khác. Một câu
SELECTthuần chạy thẳng không cần khối, vì SQL không có biến hay lỗi cần bắt. Đây là điểm khác thật sự giữa SQL và PL/SQL: SQL mô tả kết quả muốn có, PL/SQL mô tả các bước phải làm.
SET FEEDBACK OFF
COLUMN title FORMAT A15
SELECT title, stock FROM books WHERE id = 1;
TITLE STOCK
--------------- ----------
SQL Basics 10
- Hiểu lầm nâng cao.
NULL;không phải giá trịNULL. Đây là một câu lệnh rỗng, dùng để "không làm gì" ở chỗ cú pháp đòi hỏi một lệnh. - Giới hạn của bài. Bài chưa nói đến các loại lỗi khác ngoài
ZERO_DIVIDE, tênWHEN OTHERS, hay procedure và function có tên. Các phần đó đến sau. - Đọc thêm. Oracle Database PL/SQL Language Reference, mục "PL/SQL Blocks": https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/block.html
Bài tiếp theo
Bài sau mở nắp xem cái gì chạy khối của bạn: PL/SQL engine, SQL engine, và vì sao mỗi lần khối "hỏi" bảng lại tốn một chuyến đi. Mini-dự án nối tiếp: so sánh khối ẩn danh với một procedure được lưu trong database.
Lời giải và đáp án
Làm cùng mình, Bước 2. Hai dòng cần điền:
SELECT title, price INTO v_title, v_price FROM books WHERE id = 3;
v_sale := v_price * 0.9;
SET FEEDBACK OFF
DECLARE
v_title VARCHAR2(60);
v_price NUMBER;
v_sale NUMBER;
BEGIN
SELECT title, price INTO v_title, v_price FROM books WHERE id = 3;
v_sale := v_price * 0.9;
DBMS_OUTPUT.PUT_LINE(v_title || ': ' || v_sale);
END;
/
Data Modeling: 135000
Giá sau giảm là 135000, đúng với phép tính 150000 nhân 0.9 trên giấy.
Làm cùng mình, Bước 3. Khối có thêm lỗi cố ý:
SET FEEDBACK OFF
DECLARE
v_title VARCHAR2(60);
v_price NUMBER;
v_sale NUMBER;
BEGIN
SELECT title, price INTO v_title, v_price FROM books WHERE id = 3;
v_sale := v_price * 0.9;
DBMS_OUTPUT.PUT_LINE(v_title || ': ' || v_sale);
v_sale := v_price / (v_price - 150000);
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('Caught: ' || SQLERRM);
END;
/
Data Modeling: 135000
Caught: ORA-01476: divisor is equal to zero
Có hai dòng: dòng giá giảm in trước, rồi lỗi ORA-01476 được bắt và in bằng SQLERRM. Dòng giá giảm vẫn có vì nó chạy xong trước khi lỗi xảy ra, và PL/SQL không rút lại những gì đã chạy.
Bài 1. Với v_qty bằng 5:
SET FEEDBACK OFF
DECLARE
v_qty NUMBER := 5;
BEGIN
DBMS_OUTPUT.PUT_LINE('A');
v_qty := 10 / (v_qty - 5);
DBMS_OUTPUT.PUT_LINE('B');
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('C');
END;
/
A
C
Với v_qty bằng 4, phép chia là 10 chia cho -1 nên không có lỗi:
SET FEEDBACK OFF
DECLARE
v_qty NUMBER := 4;
BEGIN
DBMS_OUTPUT.PUT_LINE('A');
v_qty := 10 / (v_qty - 5);
DBMS_OUTPUT.PUT_LINE('B');
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('C');
END;
/
A
B
| v_qty ban đầu | Các dòng được in | Phần EXCEPTION có chạy không |
|---|---|---|
| 5 | A rồi C | Có, vì 10 chia cho 0 |
| 4 | A rồi B | Không, vì phép chia hợp lệ |
Bài 2.
SET FEEDBACK OFF
DECLARE
v_title VARCHAR2(60);
v_value NUMBER;
BEGIN
SELECT title, price * stock INTO v_title, v_value FROM books WHERE id = 3;
DBMS_OUTPUT.PUT_LINE('Stock value of ' || v_title || ': ' || v_value);
END;
/
Stock value of Data Modeling: 750000
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
SELECT title, price * stock INTO v_title, v_value | Tính 150000 * 5 ngay trong truy vấn rồi chép vào v_value | Biểu thức sau SELECT được tính trước khi gán, nên INTO nhận kết quả đã tính |
FROM books WHERE id = 3 | Chỉ lấy đúng một dòng | INTO cần đúng một dòng, nếu không sẽ có lỗi (bài sau nói) |
Bài 3.
SET FEEDBACK OFF
DECLARE
v_stock NUMBER := 10;
v_sold NUMBER := 0;
BEGIN
BEGIN
DBMS_OUTPUT.PUT_LINE('Days of stock: ' || v_stock / v_sold);
EXCEPTION
WHEN ZERO_DIVIDE THEN
DBMS_OUTPUT.PUT_LINE('Skip: no sales yet');
END;
DBMS_OUTPUT.PUT_LINE('Report finished');
END;
/
Skip: no sales yet
Report finished
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
BEGIN thứ hai | Mở khối con bên trong khối ngoài | Khối con có EXCEPTION riêng nên lỗi bị chặn ngay tại đó |
WHEN ZERO_DIVIDE THEN trong khối con | In Skip: no sales yet rồi khối con kết thúc êm | Khối ngoài không biết có lỗi, nên vẫn chạy tiếp |
Report finished | Chạy sau khối con, vì khối ngoài không bị lỗi | Đây là lý do dùng khối con: giữ lỗi nhỏ không phá cả báo cáo |
Tự kiểm tra.
DECLARE(tùy chọn),BEGIN ... END(bắt buộc),EXCEPTION(tùy chọn).BEGINcần ít nhất một lệnh.- Một là chưa có dòng
/nên khối chưa được gửi lên server; hai làSERVEROUTPUTđang tắt nên chữ nằm trong bộ đệm mà không hiện. - A, rồi C. Dòng B bị bỏ qua vì phép chia đã gây lỗi.
- Dòng A vẫn hiện, sau đó là thông báo lỗi
ORA-01476: divisor is equal to zerovàORA-06512chỉ ra dòng lỗi. - Lỗi
PLS-00428: an INTO clause is expected in this SELECT statement. Sửa bằng cách khai báo một biến và viếtSELECT title INTO v_title FROM books WHERE id = 1;.