CodeGym /Các khóa học /SQL SELF /Xử lý lỗi và quay về trạng thái ban đầu: EXCEPTION, RAISE...

Xử lý lỗi và quay về trạng thái ban đầu: EXCEPTION, RAISE

SQL SELF
Mức độ, Bài học
Có sẵn

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

  1. Đừng dùng ROLLBACK TO SAVEPOINT trong PL/pgSQL! Sẽ bị lỗi cú pháp.
  2. Để rollback một phần, luôn dùng block lồng BEGIN ... EXCEPTION ... END.
  3. Nhớ rằng COMMIT và ROLLBACK trong procedure sẽ khởi động lại transaction — chỉ dùng khi procedure chạy ở chế độ autocommit!
  4. Log lỗi vào bảng riêng để không mất thông tin về dòng lỗi.
  5. 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.

2
Nhiệm vụ
SQL SELF, mức độ, bài học
Đã khóa
Sử dụng EXCEPTION và trả về kết quả
Sử dụng EXCEPTION và trả về kết quả
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION