CodeGym /Các khóa học /SQL SELF /Ví dụ thực tế về làm việc với giao dịch lồng nhau

Ví dụ thực tế về làm việc với giao dịch lồng nhau

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

Hôm nay nhiệm vụ của tụi mình là viết một function mà:

  1. 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.
  2. Trừ tiền từ số dư. Nếu đủ tiền thì mới trừ.
  3. 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:

  1. clients — lưu thông tin khách hàng và số dư.
  2. payments — lưu các giao dịch thành công.
  3. 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 SAVEPOINT bị cấm trong PL/pgSQL (sẽ báo lỗi), thay vào đó dùng block BEGIN ... 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 SAVEPOINTROLLBACK 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.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION