Không cho số lượng âm
Tạo Trigger: Không cho phép SOLUONG < 0 trong SANPHAM.
Mục tiêu: Hiểu cách SQL Server tự động thực hiện một nhóm câu lệnh khi dữ liệu trong bảng phát sinh các thao tác INSERT, UPDATE, DELETE.
01 · TRIGGER LÀ GÌ?
INSERT, UPDATE hoặc DELETE.Ví dụ: "không cho phép số lượng sản phẩm nhỏ hơn 0." Khi người dùng chạy UPDATE SANPHAM SET SOLUONG = -10 WHERE MASP = 'SP01';, Trigger phát hiện SOLUONG < 0 ➜ Báo lỗi ➜ ROLLBACK ➜ Hủy thao tác.
Trigger thường dùng để tự động kiểm soát dữ liệu ngay tại tầng cơ sở dữ liệu:
02 · CÁC LOẠI TRIGGER
SQL Server hỗ trợ hai loại Trigger chính. SQL Server không hỗ trợ BEFORE TRIGGER theo cách một số hệ quản trị CSDL khác có.
AFTER - Chạy sau khi thao tác DML hoàn tất thành công (AFTER INSERT, AFTER UPDATE, AFTER DELETE). INSTEAD OF - thay thế thao tác DML ban đầu bằng các câu lệnh định nghĩa bên trong Trigger.
| Loại | Cách hoạt động | Ví dụ |
|---|---|---|
AFTER | Thao tác gốc xảy ra trước, Trigger xử lý sau | Kiểm tra và cập nhật dữ liệu |
INSTEAD OF | Trigger thay thế thao tác gốc | Không cho phép xóa |
BEFORE | Không hỗ trợ trong SQL Server | - |
03 · CÚ PHÁP TRIGGER
CREATE TRIGGER <Tên_Trigger>
ON <Tên_Bảng>
AFTER | INSTEAD OF INSERT, UPDATE, DELETE
AS
BEGIN
-- Các câu lệnh xử lý
END;FOR cũng có thể được dùng thay cho AFTER.
CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
-- xử lý
END;CREATE TRIGGER tạo Trigger mới. Tên nên dễ hiểu, ví dụ trg_AfterInsert_KhachHang, trg_CheckQuantity_SanPham, trg_PreventDelete_NhanVien. ON KHACHHANG gắn Trigger vào bảng đó. AFTER INSERT chạy sau khi có INSERT. BEGIN...END chứa các câu lệnh thực hiện.
Khi thêm khách hàng mới, hiển thị thông báo:
CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
PRINT N'Khách hàng mới đã được thêm vào KHACHHANG.';
END;Thử nghiệm:
INSERT INTO KHACHHANG
VALUES (
'KH99',
N'Nguyễn Văn A',
N'Hà Nội',
'0909000000',
'2026-08-11',
0
);INSERT thành công.Không cho phép xóa khách hàng:
CREATE TRIGGER trg_PreventDelete_KhachHang
ON KHACHHANG
INSTEAD OF DELETE
AS
BEGIN
PRINT N'Không được xóa khách hàng.';
END;Khi chạy DELETE FROM KHACHHANG WHERE MAKH = 'KH01';, thay vì xóa, hệ thống chỉ in ra "Không được xóa khách hàng."
04 · KIỂM TRA VÀ XỬ LÝ LỖI
Bài toán: Số lượng sản phẩm trong SANPHAM không được nhỏ hơn 0.
CREATE TRIGGER trg_CheckQuantity_SanPham
ON SANPHAM
AFTER INSERT, UPDATE
AS
BEGIN
IF EXISTS (
SELECT *
FROM SANPHAM
WHERE SOLUONG < 0
)
BEGIN
RAISERROR (
N'Số lượng không thể âm.',
16,
1
);
ROLLBACK;
RETURN;
END
END;IF EXISTS(...) kiểm tra có tồn tại bản ghi vi phạm điều kiện hay không. RAISERROR(N'...', 16, 1) báo lỗi - Mức độ 16 là mức thường dùng để biểu diễn lỗi do người dùng gây ra (dữ liệu nhập sai, vi phạm quy tắc). ROLLBACK hủy giao dịch đang thực hiện, đưa dữ liệu về trạng thái trước đó.
RAISERROR và ROLLBACK nhưng quên RETURN ngay sau đó. Nếu Trigger còn câu lệnh nào phía sau, chúng vẫn sẽ tiếp tục chạy dù giao dịch đã bị hủy, có thể gây lỗi hoặc hành vi không mong muốn. Nên tập thói quen luôn viết đủ bộ ba RAISERROR ➜ ROLLBACK ➜ RETURN, không chỉ dừng ở ROLLBACK.| RAISERROR | ||
|---|---|---|
| Mục đích | Thông báo | Báo lỗi |
| Có thể dừng thao tác | Không | Có |
| Dùng với ROLLBACK | Có thể nhưng không tự động | Thường kết hợp |
| Ví dụ | "Đã thêm khách hàng" | "Số lượng không thể âm" |
KIỂM TRA NHANH
Muốn hủy hẳn một thao tác UPDATE khi phát hiện dữ liệu sai trong Trigger, cần dùng thêm lệnh nào ngoài RAISERROR?
RAISERROR chỉ báo lỗi, còn ROLLBACK mới thực sự hủy giao dịch và đưa dữ liệu về trạng thái trước đó.05 · INSERTED VÀ DELETED
INSERTED và DELETED. Chúng chỉ tồn tại trong phạm vi Trigger, chứa dữ liệu liên quan đến thao tác DML đang xảy ra.| Thao tác | INSERTED | DELETED |
|---|---|---|
INSERT | Bản ghi mới | - |
DELETE | - | Bản ghi bị xóa |
UPDATE | Giá trị mới | Giá trị cũ |
INSERTED = cái mới. DELETED = cái cũ / bị xóa.@, khai báo bằng DECLARE - Chỉ tồn tại trong phạm vi batch/Trigger đang chạy, dùng để lưu tạm 1 giá trị lấy ra từ INSERTED/DELETED trước khi so sánh hay xử lý tiếp.DECLARE @TenBien KieuDuLieu;Khai báo nhiều biến cùng lúc, ngăn cách bằng dấu phẩy:
DECLARE @MaKH CHAR(4), @NgayDK SMALLDATETIME;Biến mới khai báo mặc định mang giá trị NULL. Có 2 cách gán giá trị:
| Cách gán | Cú pháp | Khi nào dùng |
|---|---|---|
SET | SET @MaKH = 'KH01'; | Gán 1 giá trị đơn, biết trước hoặc lấy từ subquery đúng 1 dòng. |
SELECT | SELECT @MaKH = MAKH FROM inserted; | Gán trực tiếp từ kết quả 1 câu SELECT. |
DECLARE @MaKH CHAR(4);
SELECT @MaKH = MAKH
FROM inserted;
PRINT N'Khách hàng vừa thêm: ' + @MaKH;SET và SELECT: Nếu subquery/bảng nguồn trả về nhiều hơn 1 dòng, SET @x = (SELECT ...) sẽ báo lỗi ngay lập tức - Nhưng SELECT @x = cột FROM bảng thì không báo lỗi gì cả, nó chỉ âm thầm gán giá trị của dòng cuối cùng được xử lý (thứ tự không đảm bảo) rồi bỏ qua tất cả các dòng còn lại. Đây chính là nguồn gốc của lỗi "Trigger chỉ đúng với 1 dòng" sẽ nói kỹ ở mục 07 - Không có thông báo lỗi nào để nhận biết, code vẫn chạy "được" nhưng sai âm thầm.Bấm một nút để xem INSERTED/DELETED chứa gì với từng thao tác (ví dụ: Đổi tên khách hàng từ "Nguyễn Văn A" thành "Nguyễn Văn B"):
DEMO TRỰC QUAN
INSERT ➜ Chỉ INSERTED có dữ liệu · DELETE ➜ Chỉ DELETED có dữ liệu · UPDATE ➜ Cả hai đều có dữ liệu.
INSERT INTO KHACHHANG (MAKH, HOTEN)
VALUES ('KH01', N'Nguyễn Văn A');
CREATE TRIGGER trg_AfterInsert_KhachHang
ON KHACHHANG
AFTER INSERT
AS
BEGIN
DECLARE @MAKH CHAR(4);
SELECT @MAKH = MAKH
FROM INSERTED;
PRINT N'Khách hàng mới được thêm vào với MAKH: '
+ CAST(@MAKH AS VARCHAR);
END;CREATE TRIGGER trg_AfterDelete_KhachHang
ON KHACHHANG
AFTER DELETE
AS
BEGIN
SELECT *
FROM DELETED;
END;Khi UPDATE, DELETED chứa giá trị cũ và INSERTED chứa giá trị mới, nên có thể so sánh "giá trị cũ ➜ giá trị mới":
CREATE TRIGGER trg_AfterUpdate_KhachHang
ON KHACHHANG
AFTER UPDATE
AS
BEGIN
DECLARE
@OldName VARCHAR(40),
@NewName VARCHAR(40);
SELECT @OldName = HOTEN
FROM DELETED;
SELECT @NewName = HOTEN
FROM INSERTED;
PRINT N'Tên khách hàng đã thay đổi từ "'
+ @OldName
+ N'" sang "'
+ @NewName
+ N'".';
END;06 · ỨNG DỤNG THỰC TẾ
CHECK CONSTRAINT hoặc FOREIGN KEY đã làm được (ví dụ chỉ để chặn SOLUONG < 0, hoặc đặt giá trị mặc định). Trigger chạy sau khi thao tác DML đã xảy ra nên tốn thêm chi phí xử lý; chỉ nên dùng Trigger khi logic vượt quá khả năng của Constraint - Ví dụ cần tự động cập nhật dữ liệu ở bảng khác, hoặc kiểm tra điều kiện dựa trên dữ liệu ở nhiều bảng.Không cho phép thêm hóa đơn nếu khách hàng chưa tồn tại:
IF EXISTS kiểm tra có tồn tại dữ liệu thỏa điều kiện; IF NOT EXISTS kiểm tra ngược lại - Không tồn tại dữ liệu thỏa điều kiện.
Bảng HOADON(MAHD, TRIGIA) và CTHD(MAHD, MASP, SOLUONG, DONGIA). Khi INSERT INTO CTHD, Trigger tự động tính lại TRIGIA và UPDATE HOADON.
Bài toán nâng cao hơn: Thêm/sửa hóa đơn ➜ Trigger tính tổng trị giá ➜ Cập nhật DOANHSO trong KHACHHANG.
Không cho phép xóa nhân viên nếu nhân viên đã tham gia bán hàng:
Khi thêm khách hàng mới, nếu không nhập NGDK, hệ thống tự động lấy ngày hiện tại bằng GETDATE().
07 · TRIGGER XỬ LÝ NHIỀU DÒNG
Sinh viên thường viết:
DECLARE @MAKH CHAR(4);
SELECT @MAKH = MAKH
FROM INSERTED;Cách này dễ hiểu khi minh họa một bản ghi, nhưng một câu INSERT INTO ... SELECT ... có thể thêm nhiều dòng cùng lúc - Và như đã nói ở mục 05, SELECT @x = cột FROM inserted không hề báo lỗi khi có nhiều dòng, nó chỉ âm thầm bỏ sót.
Đây là 2 cách viết phổ biến cho cùng 1 bài toán kiểm tra dữ liệu trong Trigger:
Biến tạm (DECLARE + SELECT @x = ...) | IF EXISTS/NOT EXISTS + JOIN | |
|---|---|---|
| Đúng với nhiều dòng? | Chỉ đúng khi chắc chắn thao tác tác động đúng 1 dòng | Luôn đúng, bất kể 1 hay nhiều dòng |
| Cách kiểm tra | Gán giá trị ra biến rồi so sánh | Kiểm tra "có tồn tại dòng vi phạm không" trên toàn bộ inserted/deleted |
| Khi nào nên dùng | Bài minh họa đơn giản, hoặc chắc chắn 100% chỉ có 1 dòng | Nên dùng mặc định cho mọi Trigger kiểm tra dữ liệu |
-- Biến tạm - chỉ an toàn khi chắc chắn 1 dòng
DECLARE @SoLuong INT;
SELECT @SoLuong = SOLUONG FROM inserted;
IF (@SoLuong < 0)
BEGIN
RAISERROR (N'Số lượng không thể âm.', 16, 1);
ROLLBACK TRANSACTION;
RETURN;
END-- IF EXISTS - đúng với bất kỳ số dòng nào
IF EXISTS (SELECT * FROM inserted WHERE SOLUONG < 0)
BEGIN
RAISERROR (N'Số lượng không thể âm.', 16, 1);
ROLLBACK TRANSACTION;
RETURN;
ENDTrong đề bài BTTH5, đa số câu đều nên trình bày theo hướng IF EXISTS/NOT EXISTS làm đáp án chính - Cách dùng biến tạm vẫn được giữ lại trong đáp án như một "Cách khác" để tham khảo, kèm ghi chú rõ giới hạn chỉ đúng khi thao tác ảnh hưởng đúng 1 dòng.
08 · QUY TRÌNH XÂY TRIGGER
Khi gặp bài tập Trigger, có thể làm theo 6 bước:
Xóa Trigger:
DROP TRIGGER trg_AfterInsert_KhachHang;Xem thông tin Trigger:
SELECT *
FROM INFORMATION_SCHEMA.TRIGGERS;Tuần 1: Tạo CSDL. Tuần 2: Truy vấn. Tuần 3: Truy vấn nâng cao. Tuần 4: Xử lý dữ liệu (CAST/CASE/Subquery). Tuần 5: Tự động hóa & kiểm soát bằng Trigger, INSERTED/DELETED, RAISERROR/ROLLBACK. Tuần 6 theo khung môn học sẽ là ôn tập và thi thực hành, thời gian và hình thức thi được thông báo sau.
09 · BÀI TẬP THỰC HÀNH
Xem đề bài đầy đủ, file đính kèm và đáp án chi tiết (cần mã truy cập) của BTTH5.
Các bài tự luyện thêm về Trigger - Không thuộc bài nộp BTTH5. Mỗi bài có gợi ý ẩn - Bấm để mở.
Tạo Trigger: Không cho phép SOLUONG < 0 trong SANPHAM.
Khi thay đổi tên khách hàng, hiển thị: 'Tên khách hàng đã thay đổi từ "..." sang "..."'.
Không cho phép xóa nhân viên nếu nhân viên đã có hóa đơn.
Khi thêm chi tiết hóa đơn, tự động cập nhật trị giá hóa đơn.
Khi thêm hoặc cập nhật hóa đơn, tự động cập nhật doanh số khách hàng.