Lỗi trong PostgreSQL có thể xuất hiện vì nhiều lý do: vi phạm constraint (NOT NULL, UNIQUE, CHECK), lỗi cú pháp, giá trị trùng lặp v.v. Nếu không bắt và xử lý các lỗi này, toàn bộ transaction bên ngoài có thể bị rollback hết. Để nghiệp vụ chạy ổn định, xử lý lỗi đúng cách là rất quan trọng.
PL/pgSQL cung cấp cơ chế block BEGIN ... EXCEPTION ... END cực mạnh để bắt và xử lý lỗi trong function và procedure. Nó giống như try-catch trong Python hoặc Java, nhưng có đặc thù riêng khi chạy trong transaction của PostgreSQL.
Điểm quan trọng:
Mỗi block BEGIN ... EXCEPTION ... END hoạt động như một “savepoint ảo”. Nếu có exception — mọi thay đổi trong block đó sẽ tự động bị rollback. Đây là cách duy nhất đúng để rollback một phần trong function và procedure của PL/pgSQL
Cú pháp xử lý lỗi với EXCEPTION
BEGIN
-- mã chính
EXCEPTION
WHEN LOAI_LOI THEN
-- xử lý lỗi cụ thể
WHEN LOAI_LOI_KHAC THEN
-- handler khác
WHEN OTHERS THEN
-- xử lý mọi lỗi còn lại
END;
Minh họa trong function/procedure
DO $$
BEGIN
RAISE NOTICE 'Sắp có lỗi...';
PERFORM 1 / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Chia cho 0 đã được bắt!';
END;
$$;
Ví dụ: cập nhật với xử lý và rollback lỗi
Giả sử có bảng đơn hàng:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL,
status TEXT NOT NULL
);
Tạo function cập nhật trạng thái đơn hàng, nếu lỗi thì không thay đổi gì:
CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
BEGIN
UPDATE orders
SET status = new_status
WHERE id = order_id;
-- Giả lập lỗi
IF new_status = 'FAIL' THEN
RAISE EXCEPTION 'Giả lập lỗi!';
END IF;
RAISE NOTICE 'Trạng thái đơn hàng % đã cập nhật', order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Lỗi khi cập nhật đơn hàng %: %', order_id, SQLERRM;
-- Mọi thay đổi trong block này sẽ tự động rollback!
-- Nếu cần, có thể ném lại lỗi cho xử lý bên ngoài
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
Cách hoạt động trong procedure với quản lý transaction tường minh
Trong procedure (CREATE PROCEDURE) bạn có thể dùng COMMIT, ROLLBACK, SAVEPOINT, nhưng không được dùng ROLLBACK TO SAVEPOINT. Nếu muốn rollback một phần thao tác trong procedure, vẫn dùng BEGIN ... EXCEPTION ... END:
CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
-- Cả procedure có thể dùng COMMIT/ROLLBACK, nhưng để rollback từng bước:
BEGIN
UPDATE accounts
SET balance = balance - amount
WHERE id = (SELECT account_id FROM orders WHERE id = order_id);
-- lỗi
IF amount < 0 THEN
RAISE EXCEPTION 'Số tiền không được âm!';
END IF;
UPDATE orders SET status = 'PAID' WHERE id = order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Lỗi khi xử lý thanh toán đơn hàng %: %', order_id, SQLERRM;
-- thay đổi trong block này sẽ tự động rollback
END;
COMMIT; -- Chỉ procedure mới được kết thúc transaction tường minh!
END;
$$;
Log lỗi
Không chỉ bắt lỗi mà còn nên lưu lại để phân tích sau này.
CREATE TABLE error_log (
id SERIAL PRIMARY KEY,
order_id INT,
error_message TEXT,
error_time TIMESTAMP DEFAULT now()
);
Trong function hoặc procedure:
EXCEPTION
WHEN OTHERS THEN
INSERT INTO error_log (order_id, error_message)
VALUES (order_id, SQLERRM);
RAISE NOTICE 'Lỗi đã được ghi vào log: %', SQLERRM;
RAISE;
Những giới hạn và lưu ý quan trọng trong PostgreSQL 17
Trong function (CREATE FUNCTION ... ) không được dùng các lệnh quản lý transaction (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT)! Mọi function đều chạy toàn bộ trong transaction bên ngoài.
Trong procedure (CREATE PROCEDURE ... ) có thể dùng SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK. NHƯNG: ROLLBACK TO SAVEPOINT — CẤM! (Bạn sẽ gặp lỗi cú pháp nếu thử dùng ROLLBACK TO SAVEPOINT trong PL/pgSQL procedure).
Rollback "một phần code" trong function và procedure phải dùng block BEGIN ... EXCEPTION ... END. Khi có lỗi, mọi thứ trong block sẽ tự động rollback, và code có thể tiếp tục chạy tiếp.
Procedure (CREATE PROCEDURE) không thể gọi trong function hoặc qua SELECT — chỉ gọi riêng bằng lệnh CALL ....
Làm sao thực hiện "rollback một phần" thực sự trong PL/pgSQL?
Cách duy nhất hiệu quả — dùng xử lý lỗi qua block BEGIN ... EXCEPTION ... END. Block này tự động tạo savepoint, khi lỗi thì rollback mọi thay đổi trong block, không ảnh hưởng phần còn lại của procedure/function.
Ví dụ với EXCEPTION (cách nên dùng):
CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
-- Một số code
BEGIN
-- Lỗi ở đây sẽ không rollback toàn bộ procedure,
-- chỉ rollback block này thôi!
INSERT INTO demo VALUES ('dữ liệu lỗi'); -- có thể gây lỗi
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Lỗi đã được xử lý, thay đổi trong block đã bị hủy';
END;
-- Code tiếp tục chạy ở đây!
END;
$$;
Ví dụ: nạp batch dữ liệu với bảo vệ khỏi rollback toàn bộ
CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM import_table LOOP
BEGIN
INSERT INTO target_table (col1, col2)
VALUES (rec.col1, rec.col2);
EXCEPTION WHEN OTHERS THEN
INSERT INTO import_errors (err_msg)
VALUES ('Lỗi ở bản ghi: ' || rec.col1 || ': ' || SQLERRM);
-- thay đổi trong block này đã bị rollback!
END;
END LOOP;
COMMIT; -- chỉ dùng nếu procedure chạy ngoài transaction tường minh!
END;
$$;
-- Gọi procedure
CALL load_big_batch();
Lưu ý: nếu gọi procedure này từ client đã bắt đầu transaction (ví dụ Python với autocommit=False), việc COMMIT hoặc SAVEPOINT trong procedure sẽ gây lỗi.
Mẹo khi dùng savepoint lồng nhau và EXCEPTION
- Đừng dùng ROLLBACK TO SAVEPOINT trong PL/pgSQL! Sẽ bị lỗi cú pháp.
- Để rollback một phần, luôn dùng block lồng BEGIN ... EXCEPTION ... END.
- Nhớ rằng COMMIT và ROLLBACK trong procedure sẽ khởi động lại transaction — chỉ dùng khi procedure chạy ở chế độ autocommit!
- Log lỗi vào bảng riêng để không mất thông tin về dòng lỗi.
- Nếu thao tác nghiệp vụ cần đảm bảo toàn vẹn (tất cả-hoặc-không gì cả) — hãy dùng function, không dùng COMMIT/ROLLBACK bên trong; nếu cần xử lý từng bước — hãy dùng procedure.
Ví dụ: import theo batch với xử lý từng phần
CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM staging_table LOOP
BEGIN
INSERT INTO data_table (data)
VALUES (rec.data);
EXCEPTION
WHEN unique_violation THEN
INSERT INTO import_log (msg)
VALUES ('Trùng lặp: ' || rec.data);
WHEN OTHERS THEN
INSERT INTO import_log (msg)
VALUES ('Lỗi: ' || rec.data || ' — ' || SQLERRM);
END;
END LOOP;
END;
$$;
Điều quan trọng cần nhớ với PostgreSQL 17:
Trong PL/pgSQL procedure được phép dùng SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, nhưng ROLLBACK TO SAVEPOINT — không được.
"Rollback một phần" trong function và procedure chỉ làm được bằng block lồng BEGIN ... EXCEPTION ... END.
Nên quản lý transaction từ bên ngoài (qua tham số kết nối và autocommit), còn trong procedure thì dùng các cơ chế xử lý lỗi như trên.
GO TO FULL VERSION