트리거
AFTER 와 INSTEAD OF, 그리고 사용하기 전에 따져 볼 것들입니다. 트리거는 행마다 돌지 않습니다.
표가 바뀔 때 저절로 도는 코드
프로시저는 불러야 돕니다. 트리거는 표가 바뀌면 저절로 돕니다. 누가 어떤 경로로 고치든 실행되므로, 3.4 의 제약처럼 빠져나갈 길이 없습니다.
트리거 안에서는 무엇이 바뀌었는지를 두 개의 가상 표로 봅니다.
| 문장 | inserted | deleted |
|---|---|---|
| INSERT | 넣은 행 | 비어 있음 |
| UPDATE | 고친 뒤의 행 | 고치기 전의 행 |
| DELETE | 비어 있음 | 지운 행 |
글의 제목이 바뀔 때마다 이력을 남기는 표를 두고, 트리거로 채워 봅니다.
CREATE TABLE Board.POST_HISTORY ( num int IDENTITY(1,1) NOT NULL, post_num int NOT NULL, old_title nvarchar(200) NULL, new_title nvarchar(200) NULL, chg_date datetime2(0) NOT NULL CONSTRAINT DF_POST_HISTORY_chg DEFAULT SYSDATETIME(), CONSTRAINT PK_POST_HISTORY PRIMARY KEY CLUSTERED (num));
트리거는 행마다 돌지 않습니다
가장 흔한 실수부터 봅니다. 행이 하나뿐이라고 여기고 적은 트리거입니다.
CREATE OR ALTER TRIGGER Board.TR_POSTS_TITLE_BAD ON Board.POSTS AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF NOT UPDATE(title) RETURN; DECLARE @post_num int, @old nvarchar(200), @new nvarchar(200); SELECT @post_num = num, @old = title FROM deleted; -- 한 행이라고 가정합니다 SELECT @new = title FROM inserted; INSERT INTO Board.POST_HISTORY (post_num, old_title, new_title) VALUES (@post_num, @old, @new); END GO -- (가) 한 행을 고칩니다. UPDATE Board.POSTS SET title = title + N' (고침)' WHERE num = 250; -- (나) 공지사항 35건을 한 번에 고칩니다. UPDATE Board.POSTS SET title = title + N' (고침)' WHERE board_num = 1;
| 고친 문장 | 고친 행 | 남은 기록 |
|---|---|---|
| (가) 한 행 | 1 | 1 |
| (나) 35 행 | 35 | 1 |
트리거는 바뀐 행마다 도는 것이 아니라 문장마다 한 번 돕니다.
inserted 와
deleted 에는 바뀐 행이 모두
들어 있습니다. 그것을 변수 하나에 담으면 4.1 에서 본 대로 마지막 값 하나만
남습니다.
오류가 나지 않는 것이 가장 나쁩니다. 화면에서 한 건씩 고치는 동안에는 잘 돌다가, 관리자가 일괄 수정을 한 날부터 이력이 새기 시작합니다.
집합으로 적습니다
CREATE OR ALTER TRIGGER Board.TR_POSTS_TITLE ON Board.POSTS AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF NOT UPDATE(title) RETURN; -- 두 가상 표를 키로 이어 한 문장으로 넣습니다. INSERT INTO Board.POST_HISTORY (post_num, old_title, new_title) SELECT i.num, d.title, i.title FROM inserted i JOIN deleted d ON d.num = i.num WHERE ISNULL(i.title, N'') <> ISNULL(d.title, N''); END
| post_num | 이전 | 이후 |
|---|---|---|
| 10 | 제약 조건 질문드립니다 | 제약 조건 질문드립니다 (고침) |
| 20 | 인덱스 정리해 봤습니다 | 인덱스 정리해 봤습니다 (고침) |
| 30 | 제약 조건 정리해 봤습니다 | 제약 조건 정리해 봤습니다 (고침) |
거르는 조건 둘을 더 두었습니다. UPDATE(title) 은
그 열이 SET 목록에 있었는지를
봅니다 — 조회수만 고친 문장에서는 트리거가 곧바로 나갑니다. 그리고
WHERE 로 값이 실제로 달라진 행만
남깁니다.
| 고친 문장 | 남은 기록 |
|---|---|
| 제목을 바꿈 (35 행) | 35 |
| 조회수만 바꿈 | 0 |
| 제목을 같은 값으로 덮어씀 | 0 |
UPDATE(title) 은 값이 바뀌었는지가 아니라
목록에 있었는지를 봅니다. 그래서 같은 값으로 덮어써도 참입니다. 값
비교는 따로 해야 합니다.
트리거 안은 이미 트랜잭션 안입니다
BEGIN TRAN 을 적지 않고
UPDATE 만 해도, 그 트리거 안에서
@@TRANCOUNT 는 1 입니다. 4.6 에서 본 자동 커밋
트랜잭션 안에 있기 때문입니다.
CREATE OR ALTER TRIGGER Board.TR_POSTS_GUARD ON Board.POSTS AFTER UPDATE AS BEGIN SET NOCOUNT ON; SELECT @@TRANCOUNT AS 트랜잭션수, @@NESTLEVEL AS 중첩수준; IF EXISTS (SELECT 1 FROM inserted WHERE hit_count < 0) THROW 50003, N'조회수는 음수가 될 수 없습니다', 1; END GO UPDATE Board.POSTS SET hit_count = hit_count WHERE num = 250;
| 트랜잭션수 | 중첩수준 |
|---|---|
| 1 | 1 |
그래서 트리거에서 THROW 하면 그 문장
전체가 되돌아갑니다. 3.4 의 제약과 같은 효과를, 제약으로는 적을 수
없는 규칙에 대해 낼 수 있습니다.
BEGIN TRAN; BEGIN TRY UPDATE Board.POSTS SET hit_count = -1 WHERE num = 250; END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS 번호, XACT_STATE() AS 상태; END CATCH IF XACT_STATE() <> 0 ROLLBACK; SELECT num, hit_count FROM Board.POSTS WHERE num = 250;
| 번호 | XACT_STATE() | 250 번 값 |
|---|---|---|
| 50003 | -1 | 250 (원래대로) |
ROLLBACK 은 배치를 통째로 죽입니다
THROW 대신
ROLLBACK 을 적으면 결과가 다릅니다.
-- 트리거 안에서 ROLLBACK 을 적었을 때 BEGIN TRAN; UPDATE Board.POSTS SET hit_count = -1 WHERE num = 250; PRINT N'>> 이 줄이 실행됩니까?'; SELECT @@TRANCOUNT AS 남은트랜잭션;
4.5 에서 본 것과 정반대입니다. 보통은 오류가 나도 다음 문장이
실행되는데, 트리거의 ROLLBACK 은 배치를 중단시킵니다.
부르는 쪽의 TRY...CATCH 도 소용없습니다.
트리거에서는 ROLLBACK 대신
THROW 를 사용하십시오. 같은 효과를 내면서
부르는 쪽이 잡아 처리할 수 있습니다. 오류 번호로 무엇이 막혔는지도 전할 수
있습니다.
대신 실행합니다
AFTER 트리거는 문장이 끝난 뒤에 돕니다.
INSTEAD OF 는 그 문장 대신
돕니다. 원래 문장은 실행되지 않고 트리거가 할 일을 정합니다.
3.7 에서 조인 뷰로 두 표를 함께 고치려다 막혔습니다.
UPDATE Board.V_POST_LIST SET title = N'x', nickname = N'y' WHERE num = 250;
INSTEAD OF 트리거를 달면
무엇을 어떻게 고칠지 직접 적을 수 있습니다.
CREATE OR ALTER TRIGGER Board.TR_V_POST_LIST_UPD ON Board.V_POST_LIST INSTEAD OF UPDATE AS BEGIN SET NOCOUNT ON; UPDATE p SET title = i.title, hit_count = i.hit_count FROM Board.POSTS p JOIN inserted i ON i.num = p.num; UPDATE u SET nickname = i.nickname FROM Member.USERS u JOIN Board.POSTS p ON p.user_num = u.num JOIN inserted i ON i.num = p.num; END GO UPDATE Board.V_POST_LIST SET title = N'제목을 바꿉니다', nickname = N'닉네임도' WHERE num = 250;
| num | title | nickname |
|---|---|---|
| 250 | 제목을 바꿉니다 | 닉네임도 |
이 예제는 되기 때문에 위험합니다. 글 하나의 제목을 고치려다
그 사람의 닉네임을 바꿔 버렸습니다. 다른 글에도 그 이름으로
나옵니다. 뷰가 여러 표를 잇고 있으면 무엇을 고치는 것인지 문장만 보고 알 수
없습니다 — INSTEAD OF 로 되게 만들기 전에
정말 그 뷰로 고쳐야 하는지 먼저 따지십시오. 3.7 의 결론은
여전히 유효합니다.
사용하기 전에
건너뛰는 길이 있습니다
-- DELETE 트리거를 달아 둔 표입니다. DELETE FROM Board.T_TRG; -- (가) TRUNCATE TABLE Board.T_TRG; -- (나)
| 지운 방법 | 남은 행 | 트리거가 남긴 기록 |
|---|---|---|
| (가) DELETE | 0 | 1건 (5행을 지웠다고) |
| (나) TRUNCATE | 0 | 0건 |
TRUNCATE 는 트리거를
발동하지 않습니다. 대량 삽입(BULK INSERT)도
기본으로는 그렇습니다. 그리고 누구든 트리거를 끌 수 있습니다.
DISABLE TRIGGER Board.TR_T_TRG_DEL ON Board.T_TRG; DELETE FROM Board.T_TRG; -- 기록이 남지 않습니다 ENABLE TRIGGER Board.TR_T_TRG_DEL ON Board.T_TRG; -- 지금 무엇이 걸려 있고 꺼져 있는지 봅니다. SELECT t.name AS 트리거, OBJECT_NAME(t.parent_id) AS 대상, t.is_instead_of_trigger AS INSTEAD_OF, t.is_disabled AS 꺼짐 FROM sys.triggers t WHERE t.is_ms_shipped = 0;
| 트리거 | 대상 | INSTEAD_OF | 꺼짐 |
|---|---|---|---|
| TR_POSTS_TITLE | POSTS | 0 | 0 |
| TR_V_POST_LIST_UPD | V_POST_LIST | 1 | 0 |
보이지 않습니다
UPDATE Board.POSTS SET title = … 이라는 문장만
읽어서는 이력 표에 무엇이 쌓이는지 알 수 없습니다. 자료가
이상한데 원인을 못 찾을 때, 트리거를 떠올리기까지 시간이 걸립니다.
트리거가 트리거를 부르면 더 어려워집니다. 이 데이터베이스의 설정을 확인해 둡니다.
| 설정 | 값 | 뜻 |
|---|---|---|
| nested triggers | 1 | 트리거가 다른 트리거를 발동합니다(최대 32단계) |
| RECURSIVE_TRIGGERS | 0 | 자기 표를 고쳐도 자기를 다시 부르지 않습니다 |
언제 사용합니까
- 감사 기록 — 누가 무엇을 언제 바꿨는지 남기는 일입니다.
어느 경로로 들어와도 남아야 하므로 트리거가 맞습니다. 이 단원의
POST_HISTORY가 그 예입니다. - 제약으로 적을 수 없는 규칙 — 3.4 의
CHECK는 같은 행 안만 봅니다. 다른 표를 봐야 하는 규칙은 트리거로 지킵니다. - 뷰를 통한 수정 —
INSTEAD OF가 유일한 방법입니다.
먼저 다른 것으로 되는지 보십시오. 값의 범위는
CHECK(3.4), 다른 열에서 이끌어 내는 값은 계산
열(3.6), 여러 문장을 묶는 절차는 프로시저(4.2)가 낫습니다. 셋 다 문장을 읽는
사람에게 보입니다. 트리거는 그것들로 안 될 때 씁니다.
직접 해보기
3.3 에서 답글이 달린 글은 외래 키 때문에 지울 수 없다는 것을 보았습니다. 그런데 오류 메시지가 547 FK_POSTS_PARENT 라서 화면에 그대로 보이기 어렵습니다. 트리거로 알아보기 쉬운 메시지를 내도록 해 보세요.
아래 트리거는 댓글이 지워질 때 이력에 남깁니다. 한 건씩 지울 때는 잘 돌았는데 관리자가 일괄 삭제를 한 뒤로 기록이 맞지 않습니다. 무엇이 잘못되었고 어떻게 고칩니까. 이력 표는 아래와 같이 두었다고 합시다.
CREATE TABLE Board.CMT_HISTORY ( num int IDENTITY(1,1) PRIMARY KEY, post_num int NOT NULL, user_num int NOT NULL, del_date datetime2(0) NOT NULL);
CREATE OR ALTER TRIGGER Board.TR_CMT_DEL ON Board.COMMENTS AFTER DELETE AS BEGIN SET NOCOUNT ON; DECLARE @post int, @user int; SELECT @post = post_num, @user = user_num FROM deleted; INSERT INTO Board.CMT_HISTORY (post_num, user_num, del_date) VALUES (@post, @user, SYSDATETIME()); END
이 단원에서 만든 것을 지웁니다.
DROP TRIGGER Board.TR_POSTS_TITLE, Board.TR_POSTS_GUARD;
DROP VIEW Board.V_POST_LIST;
DROP TABLE Board.POST_HISTORY, Board.CMT_HISTORY, Board.T_TRG, Board.T_TRG_LOG, Board.T_NOTRG;
- 트리거는 표가 바뀌면 저절로 돕니다. 바뀐 내용은
inserted·deleted가상 표로 봅니다. - 행마다가 아니라 문장마다 한 번 돕니다. 35 행을 고쳐도 한 번이고,
deleted에는 35 행이 모두 들어 있습니다. - 변수 하나에 담지 마십시오. 오류 없이 기록이 한 건만 남습니다.
INSERT … SELECT … FROM inserted로 집합을 그대로 다룹니다. UPDATE(열)은 값이 바뀌었는지가 아니라SET목록에 있었는지를 봅니다. 값 비교는 따로 합니다.- 트리거 안은 이미 트랜잭션 안입니다.
THROW하면 문장 전체가 되돌아갑니다. - 트리거에서
ROLLBACK하면 배치가 통째로 중단됩니다(3609).THROW를 사용하십시오. INSTEAD OF는 원래 문장 대신 돕니다. 조인 뷰 수정(4405)을 푸는 유일한 방법이지만, 되게 만들기 전에 그래야 하는지 따지십시오.TRUNCATE는 트리거를 건너뜁니다. 누구든DISABLE TRIGGER로 끌 수도 있습니다.- 제약 · 계산 열 · 프로시저로 되는 일은 그것으로 하십시오. 트리거는 문장을 읽는 사람에게 보이지 않습니다.