Trong các hệ quản trị cơ sở dữ liệu hiện đại, business logic thường được viết phía server — dùng procedure và function. Khi làm việc với PostgreSQL, bạn cần hiểu rõ sự khác biệt giữa function và procedure (đặc biệt từ version 11+ có procedure) và cách chúng tương tác với transaction.
Dưới đây mình sẽ nói về các điểm chính liên quan đến cơ chế transaction, gọi lồng nhau và rollback một phần thay đổi trong procedure/function của PostgreSQL 17, dựa trên tài liệu chính thức và các giới hạn hiện tại.
Khái niệm chính: function vs procedure
Function (CREATE FUNCTION) — luôn chạy trong một transaction bên ngoài; bên trong function không được dùng lệnh transaction rõ ràng (BEGIN, COMMIT, ROLLBACK, SAVEPOINT).
- Mọi thay đổi chỉ được commit hoặc rollback ở mức transaction bên ngoài.
- Để "rollback một phần" trong function thì dùng
BEGIN ... EXCEPTION ... END, nhưng không thể commit bên trong function.
Procedure (CREATE PROCEDURE) — xuất hiện để quản lý transaction trực tiếp trên server (ví dụ: commit từng phần, rollback từng bước, v.v.).
- Trong procedure (PL/pgSQL) có thể dùng
COMMIT,ROLLBACK,SAVEPOINT,RELEASE SAVEPOINT. - QUAN TRỌNG: không thể dùng
ROLLBACK TO SAVEPOINTtrong procedure PL/pgSQL (sẽ báo lỗi cú pháp). - Procedure chỉ gọi được bằng lệnh SQL riêng
CALL ..., không gọi quaSELECThay bên trong function khác.
Làm sao gọi procedure/function này từ cái khác?
Function có thể gọi function khác một cách "trong suốt" bằng tên như bình thường:
-- Ví dụ: function tính chiết khấu
CREATE OR REPLACE FUNCTION calculate_discount(order_total NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
IF order_total >= 100 THEN
RETURN order_total * 0.1;
ELSE
RETURN 0;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Function xử lý đơn hàng gọi function khác
CREATE OR REPLACE FUNCTION process_order(order_id INT, order_total NUMERIC)
RETURNS VOID AS $$
DECLARE
discount NUMERIC;
BEGIN
discount := calculate_discount(order_total);
RAISE NOTICE 'Chiết khấu: %', discount;
INSERT INTO orders_log (order_id, order_total, discount)
VALUES (order_id, order_total, discount);
END;
$$ LANGUAGE plpgsql;
Tất cả đều chạy trong một transaction bên ngoài! Nếu có lỗi ở bất kỳ function nào thì mọi thay đổi đều bị rollback.
Gọi procedure và transaction lồng nhau
Procedure có thể gọi trong procedure khác bằng lệnh CALL ... (PostgreSQL 17 cho phép stack gọi kiểu CALL proc1() -> CALL proc2()), nhưng quy tắc transaction vẫn giữ nguyên:
- Lệnh transaction (
COMMIT,ROLLBACK,SAVEPOINT,RELEASE SAVEPOINT) chỉ dùng được ở cấp cao nhất của procedure. - Nếu procedure có quản lý transaction được gọi bên trong một transaction đã active (ví dụ, qua client không autocommit), khi thực hiện
COMMIT/SAVEPOINTsẽ báo lỗi.
không thể chạy procedure bên trong function hoặc block ẩn danh (DO ...). Chỉ được gọi bằng lệnh CALL riêng biệt
Ví dụ procedure quản lý transaction
-- Procedure commit từng bước (chỉ chạy được khi kết nối ở chế độ autocommit)
CREATE PROCEDURE process_batch_orders()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT order_id, order_total FROM incoming_orders LOOP
BEGIN
-- Lưu từng batch dữ liệu riêng biệt
INSERT INTO orders (order_id, total) VALUES (rec.order_id, rec.order_total);
EXCEPTION WHEN OTHERS THEN
INSERT INTO order_errors(order_id, err_text) VALUES (rec.order_id, SQLERRM);
END;
COMMIT;
END LOOP;
END;
$$;
-- Gọi procedure
CALL process_batch_orders();
Sau mỗi COMMIT sẽ tự động bắt đầu một transaction mới.
Rollback một phần (savepoint-like behavior) trong PL/pgSQL
PL/pgSQL (cả trong function lẫn procedure) không hỗ trợ lệnh ROLLBACK TO SAVEPOINT.
Để rollback một phần code chỉ dùng block BEGIN ... EXCEPTION ... END:
BEGIN
-- một số thao tác
BEGIN
-- thao tác có thể lỗi
EXCEPTION WHEN OTHERS THEN
-- mọi thay đổi trong block này sẽ bị rollback
RAISE NOTICE 'Rollback trong block!';
END;
END;
Trong procedure cũng có thể dùng SAVEPOINT và RELEASE SAVEPOINT, nhưng không dùng ROLLBACK TO SAVEPOINT. Ý nghĩa là chia nhỏ các bước, nhưng chỉ quản lý được qua xử lý exception.
Giới hạn và best practice
- Function — chỉ cho thao tác nguyên tử: hoặc tất cả hoặc không có gì. Nếu có lỗi thì mọi thay đổi đều rollback.
- Procedure — chỉ gọi qua CALL: và chỉ bằng lệnh SQL riêng, không gọi từ SELECT/function. Quản lý transaction lồng nhau được, nhưng phải tuân thủ chặt chẽ giới hạn của PL/pgSQL.
- Rollback một phần — chỉ qua EXCEPTION: đây là cách chính thức và được hỗ trợ để rollback một phần (kiểu như SAVEPOINT).
- Procedure lồng nhau chỉ quản lý transaction khi gọi qua CALL: nếu không sẽ bị lỗi.
Câu hỏi về logic và transaction
Có thể tạo "transaction lồng nhau" trong function không?
Không. Tất cả đều chạy trong một transaction. Để rollback một phần — chỉ dùng block EXCEPTION.
Có thể COMMIT/ROLLBACK trong function hoặc block ẩn danh không?
Không, sẽ báo lỗi cú pháp. Hãy dùng procedure.
Có thể gọi procedure từ function không?
Không, chỉ được gọi bằng lệnh CALL. Không gọi từ function/SELECT.
Có thể dùng ROLLBACK TO SAVEPOINT trong procedure không?
Không! Trong PL/pgSQL thì cấm. Hãy dùng block EXCEPTION.
GO TO FULL VERSION