Trigger tự động chạy SQL
Trigger là một khối SQL được lưu sẵn, chạy mỗi khi một sự kiện cụ thể xảy ra trên một bảng cụ thể. Bạn viết nó một lần. Còn chuyện "khi nào" thì SQLite lo.
Trông nó như thế này:
Ta chưa hề viết lệnh INSERT nào vào price_history. Trigger đã làm việc đó. Mọi thay đổi giá về sau cũng sẽ được ghi lại theo cách này, dù nó đến từ CLI, một script hay ứng dụng.
Cấu trúc của CREATE TRIGGER
Đọc cú pháp từng phần một:
CREATE TRIGGER trigger_name
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE [ OF column_list ] | DELETE }
ON table_name
[ FOR EACH ROW ]
[ WHEN condition ]
BEGIN
-- one or more statements
END;
- Thời điểm:
BEFOREchạy trước thay đổi,AFTERchạy sau,INSTEAD OFchạy thay cho nó (chỉ với view). - Sự kiện: thao tác kích hoạt trigger.
UPDATE OF col1, col2giới hạn việc update vào các cột cụ thể. - Bảng: bảng được theo dõi.
FOR EACH ROW: SQLite chỉ hỗ trợ trigger cấp hàng, nên điều này luôn được ngầm hiểu. Bạn có thể ghi ra cho rõ ràng; không có gì thay đổi.WHEN: điều kiện tùy chọn. Thân trigger chỉ chạy khi điều kiện đúng.- Thân: một hoặc nhiều câu lệnh nằm giữa
BEGINvàEND. Mỗi câu lệnh phải kết thúc bằng dấu chấm phẩy.
Ngữ pháp chỉ có vậy. Phần lớn trigger thực tế chỉ dài từ năm đến mười dòng.
OLD và NEW: hàng đang bị thay đổi
Bên trong thân trigger, hai hàng giả cho phép bạn nhìn vào dữ liệu:
NEW: hàng mới đi vào. Có trong triggerINSERTvàUPDATE.OLD: hàng đang có. Có trong triggerUPDATEvàDELETE.
Trigger DELETE chỉ có OLD. Trigger INSERT chỉ có NEW. Trigger UPDATE có cả hai.
Hàng bị xóa đã biến mất khỏi accounts, nhưng dữ liệu của nó kịp được ghi vào deletions trước khi mất đi.
BEFORE: kiểm tra hoặc chỉnh sửa hàng
Trigger BEFORE chạy trước khi thay đổi trên hàng được ghi xuống đĩa. Nó tiện để báo lỗi hoặc chuẩn hóa dữ liệu:
Lệnh INSERT thứ hai bị hủy trước khi có hàng nào được ghi. RAISE(ABORT, '...') hủy câu lệnh hiện tại và rollback về lúc câu lệnh bắt đầu. RAISE(FAIL, ...), RAISE(ROLLBACK, ...) và RAISE(IGNORE) cho bạn kiểm soát chi tiết hơn những gì xảy ra.
Với việc kiểm tra dữ liệu thuần túy, hãy ưu tiên ràng buộc CHECK: nó mang tính khai báo và trình tối ưu hiểu được nó. Hãy dùng trigger BEFORE khi quy tắc cần nhìn sang bảng khác hoặc làm điều mà CHECK không diễn đạt được.
WHEN: trigger có điều kiện
Mệnh đề WHEN lọc xem thay đổi nào trên hàng thực sự chạy thân trigger. Nó được tính cho từng hàng, sau khi OLD và NEW đã có giá trị:
Đơn hàng đầu tiên không đạt điều kiện. Hai đơn còn lại thì đạt. Nếu không có mệnh đề WHEN, mỗi lần insert đều sẽ ghi vào big_orders và bạn phải lọc lại khi đọc.
INSTEAD OF: giúp ghi được vào view
Mặc định, view chỉ đọc được. Trigger INSTEAD OF chặn lệnh ghi vào view và chạy SQL của bạn thay vào đó, thường là chuyển nó thành lệnh ghi vào các bảng phía sau:
Ứng dụng làm việc với view như thể đó là một bảng. Trigger lo việc tách tên thành first_name và last_name ở phía sau.
Liệt kê và xóa trigger
Trigger được lưu trong sqlite_master, cùng với bảng và index:
DROP TRIGGER IF EXISTS name; là cách viết an toàn. Xóa bảng chứa trigger sẽ tự động xóa trigger đó, nên bạn không cần dọn dẹp trước.
Những cái bẫy cần biết
Vài điều khiến nhiều người vấp phải ở lần đầu:
- Trigger chạy theo từng hàng, không theo từng câu lệnh. Một lệnh
UPDATEchạm vào 1.000 hàng sẽ chạy trigger 1.000 lần. Nếu thân trigger tốn kém, chi phí sẽ cộng dồn rất nhanh. - Trigger chạy bên trong transaction bao ngoài. Nếu câu lệnh bên ngoài bị rollback, các lệnh ghi của trigger cũng bị rollback theo. Thường thì đó là điều bạn muốn, nhưng nó cũng có nghĩa trigger không phải lối thoát để "ghi lại dù có chuyện gì xảy ra".
- Recursive trigger bị tắt theo mặc định. Một trigger sửa chính bảng của nó sẽ không tự kích hoạt lại, trừ khi bạn đặt
PRAGMA recursive_triggers = ON;. Cứ để tắt, trừ khi bạn có lý do cụ thể. - Lệnh ghi từ ứng dụng chỉ có thể bỏ qua trigger nếu không đi qua cơ sở dữ liệu. Chừng nào mọi lệnh ghi đều đi qua SQLite, trigger sẽ chạy. Một ORM gửi batch bằng SQL thô vẫn kích hoạt chúng.
- Đừng rải logic nghiệp vụ ra nhiều trigger. Trigger vô hình ở chỗ gọi lệnh: ai đó muốn biết "sao hàng này lại xuất hiện?" sẽ phải lục lọi
sqlite_master. Dùng trigger cho các mối quan tâm xuyên suốt (log kiểm tra, cột dẫn xuất, view ghi được) và để phần còn lại trong code ứng dụng.
Ví dụ log kiểm tra thực tế
Kết hợp các mẫu ở trên: theo dõi mọi thay đổi trên bảng posts:
Một trigger vừa giữ updated_at luôn chính xác vừa ghi một hàng kiểm tra, tất cả ở cùng một chỗ. Code ứng dụng chạy lệnh UPDATE không cần biết hai việc đó đang diễn ra.
Tiếp theo: hỗ trợ JSON
Trigger lo phần tự động hóa quanh các sự kiện trên hàng. Phần SQLite nâng cao tiếp theo là những gì bạn có thể lưu bên trong một hàng: JSON. SQLite có đầy đủ các hàm JSON để truy vấn và cập nhật dữ liệu có cấu trúc mà không cần rời khỏi SQL, và đó là nội dung của trang tiếp theo.
Câu hỏi thường gặp
Trigger trong SQLite là gì?
Trigger là một khối SQL tự động chạy khi một sự kiện cụ thể xảy ra trên bảng: INSERT, UPDATE hoặc DELETE. Bạn định nghĩa nó một lần bằng CREATE TRIGGER, và SQLite sẽ tự kích hoạt nó mỗi khi sự kiện đó xảy ra. Nhờ trigger, bạn có thể ghi log kiểm tra, duy trì các cột dẫn xuất hoặc áp dụng quy tắc mà không phải trông cậy vào ứng dụng nhớ làm việc đó.
Trigger BEFORE, AFTER và INSTEAD OF khác nhau thế nào?
BEFORE chạy trước khi thay đổi trên hàng được áp dụng, hữu ích để kiểm tra hợp lệ hoặc chỉnh lại dữ liệu của hàng. AFTER chạy sau khi thay đổi đã xảy ra, hữu ích để ghi log hoặc đồng bộ các bảng khác. INSTEAD OF chỉ dùng được trên view và thay thế hoàn toàn thao tác lẽ ra sẽ diễn ra, giúp bạn ghi được vào view.
Làm sao tham chiếu đến hàng đang bị thay đổi bên trong trigger?
Dùng NEW.column cho hàng mới đi vào khi INSERT và UPDATE, và OLD.column cho hàng đang có khi UPDATE và DELETE. Trigger INSERT chỉ thấy NEW, trigger DELETE chỉ thấy OLD, còn trigger UPDATE thấy cả hai. Các tham chiếu này chỉ áp dụng cho hàng đang được xử lý.
Làm sao liệt kê hoặc xóa trigger trong SQLite?
Trigger được lưu trong sqlite_master: SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'; liệt kê tất cả. Để xóa, dùng DROP TRIGGER trigger_name;, hoặc DROP TRIGGER IF EXISTS trigger_name; nếu bạn không chắc trigger đó có tồn tại. Xóa một bảng cũng xóa luôn các trigger của bảng đó.