Hôm nay nhiệm vụ của tụi mình là viết một function mà:
- Kiểm tra số dư của khách hàng. Trước khi trừ tiền phải check xem có đủ tiền không.
- Trừ tiền từ số dư. Nếu đủ tiền thì mới trừ.
- Ghi log các thao tác thành công và thất bại. Mọi hành động đều được ghi vào bảng logs để sau này phân tích.
Đây không chỉ là một function nhàm chán để trừ tiền đâu. Ở đây tụi mình sẽ dùng giao dịch lồng nhau để rollback nếu có gì sai (ví dụ không đủ tiền hoặc lỗi khi ghi log). Sẽ thấy được lợi ích của SAVEPOINT và học cách làm cho thủ tục chống lỗi tốt hơn.
Tạo bảng dữ liệu ban đầu
Trước khi viết function, tụi mình chuẩn bị database trước đã. Cần ba bảng:
clients— lưu thông tin khách hàng và số dư.payments— lưu các giao dịch thành công.logs— lưu thông tin về mọi lần thử thanh toán (thành công và thất bại).
-- Bảng khách hàng
CREATE TABLE clients (
client_id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
balance NUMERIC(10, 2) NOT NULL DEFAULT 0
);
-- Bảng thanh toán thành công
CREATE TABLE payments (
payment_id SERIAL PRIMARY KEY,
client_id INT NOT NULL REFERENCES clients(client_id),
amount NUMERIC(10, 2) NOT NULL,
payment_date TIMESTAMP DEFAULT NOW()
);
-- Bảng logs
CREATE TABLE logs (
log_id SERIAL PRIMARY KEY,
client_id INT NOT NULL REFERENCES clients(client_id),
message TEXT NOT NULL,
log_date TIMESTAMP DEFAULT NOW()
);
Thêm dữ liệu test vào bảng clients
INSERT INTO clients (full_name, balance)
VALUES
('Otto Song', 100.00),
('Maria Chi', 50.00),
('Anna Vel', 0.00);
Bây giờ tụi mình có ba khách: Otto có 100, Maria có 50, Anna có 0 trong tài khoản.
Triển khai business logic: PROCEDURE vs FUNCTION
Tóm tắt:
- Với thao tác business kiểu "tất cả hoặc không gì cả" thì function là đủ.
- Muốn kiểm soát giao dịch từng bước, commit/rollback từng phần, log lỗi — dùng procedure (
CREATE PROCEDURE).
Tại sao không dùng function? Vì trong PostgreSQL 17, trong function bạn KHÔNG dùng được COMMIT, SAVEPOINT, ROLLBACK. Mọi thay đổi đều atomic trong transaction bên ngoài.
Chỉ có procedure (CREATE PROCEDURE ... LANGUAGE plpgsql) mới dùng được SAVEPOINT, COMMIT, ROLLBACK — nhưng có mấy hạn chế quan trọng:
- Trong procedure được phép dùng
SAVEPOINT,COMMIT,RELEASE SAVEPOINT. ROLLBACK TO SAVEPOINTbị cấm trong PL/pgSQL (sẽ báo lỗi), thay vào đó dùng blockBEGIN ... EXCEPTION ... ENDđể tạo "savepoint ảo".
Kỹ thuật rollback một phần code:
BEGIN
-- code của bạn
EXCEPTION
WHEN OTHERS THEN
-- Block này khi có lỗi sẽ rollback TẤT CẢ thay đổi bên trong!
-- Có thể ghi log ở đây:
INSERT INTO logs (...) VALUES (...);
END;
Tạo procedure thanh toán với rollback một phần và ghi log
CREATE OR REPLACE PROCEDURE process_payment(
in_client_id INT,
in_payment_amount NUMERIC
)
LANGUAGE plpgsql
AS $$
DECLARE
current_balance NUMERIC;
BEGIN
-- Lấy số dư khách hàng
SELECT balance INTO current_balance
FROM clients
WHERE client_id = in_client_id;
IF NOT FOUND THEN
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Khách không tìm thấy, thao tác bị từ chối');
RAISE EXCEPTION 'Khách với ID % không tìm thấy', in_client_id;
END IF;
-- Kiểm tra đủ tiền không
IF current_balance < in_payment_amount THEN
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Không đủ tiền để trừ ' || in_payment_amount || ' đồng.');
-- Kết thúc procedure
RETURN;
END IF;
-- Block cho thay đổi atomic; nếu lỗi thì rollback (savepoint ảo)
BEGIN
-- Trừ tiền
UPDATE clients
SET balance = balance - in_payment_amount
WHERE client_id = in_client_id;
-- Thêm record thanh toán thành công
INSERT INTO payments (client_id, amount)
VALUES (in_client_id, in_payment_amount);
-- Ghi log thành công
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Trừ thành công ' || in_payment_amount || ' đồng.');
EXCEPTION
WHEN OTHERS THEN
-- Mọi thay đổi trong block này sẽ bị rollback
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Lỗi khi thanh toán: ' || SQLERRM);
-- (không cần ROLLBACK TO SAVEPOINT — bị cấm và cũng không cần)
END;
END;
$$;
Tóm tắt diễn biến:
- Nếu không đủ tiền/không có khách — ghi log và thoát.
- Toàn bộ code quan trọng nằm trong block
BEGIN ... EXCEPTION ... END. - Nếu có lỗi trong block này — mọi thay đổi sẽ tự động rollback; ghi lỗi vào logs.
- Không dùng trực tiếp
SAVEPOINTvàROLLBACK TO SAVEPOINT— đúng chuẩn, trong PL/pgSQL chỉ dùng block EXCEPTION thôi.
Gọi procedure
Lưu ý: gọi procedure phải dùng lệnh CALL ..., và kết nối với database phải ở chế độ autocommit hoặc ngoài transaction lớn!
CALL process_payment(1, 30.00); -- Thanh toán thành công
CALL process_payment(2, 100.00); -- Không đủ tiền
CALL process_payment(999, 50.00); -- Không có khách
Kiểm tra kết quả
- Số dư khách chỉ thay đổi nếu thanh toán thành công.
- Bảng payments — chỉ có record khi trừ tiền thành công.
- logs — lịch sử mọi lần thử (và lỗi).
SELECT * FROM clients;
SELECT * FROM payments;
SELECT * FROM logs;
Ứng dụng thực tế
Procedure xử lý giao dịch là một phần trung tâm của các hệ thống fintech, e-commerce, thậm chí cả nền tảng game. Hãy tưởng tượng một shop online phải quản lý số dư gift card và trừ khi mua hàng, hoặc hệ thống ngân hàng với hàng ngàn giao dịch mỗi giây.
Những kiến thức này sẽ cực kỳ hữu ích khi đi làm, giúp bạn bảo vệ dữ liệu khách hàng và tránh lỗi nghiêm trọng khi xử lý thanh toán.
GO TO FULL VERSION