KoreanFoodie's Study
SQL 기초 - 18. 트리거(무결성, BEFORE/AFTER, 다중, 중첩, DLL) 본문
이 글은 '김상형의 SQL 정복'의 예제 코드를 설명하는 방식으로 SQL 쿼리 예제를 소개하고 있습니다.
SQL 기초 - 18. 트리거(무결성, BEFORE/AFTER, 다중, 중첩, DLL)
트리거는 특정 사건이 발생했을 때 자동으로 호출되는 코드로, 직접 호출할 수 없으며 일정한 조건이 되면 자동으로 호출된다. 시스템이 호출하므로 인수를 전달할 수 없고 리턴값도 반환할 수 없다.
주로 테이블이나 데이터베이스에 변화가 발생할 때 실행할 코드를 트리거로 정의한다. 정의는 다음과 같다.
CREATE [OR REPLACE] TRIGGER 트리거이름
BEFORE | AFTER | INSTEAD OF
INSERT OR UPDATE OR DELETE [OF 컬럼]
ON 테이블명
[FOR EACH ROW]
BEGIN
명령;
END;
이제 구체적인 예제 코드를 살펴보자.
-- 간단한 예제를 통해 알아보자
CREATE OR REPLACE TRIGGER TR_Message
AFTER UPDATE ON tCity
BEGIN
DBMS_OUTPUT.PUT_LINE('도시 정보를 갱신하였습니다.');
END;
UPDATE tCity SET popu = popu + 1 WHERE name = '서울';
-- 각 INSERT, UPDATE, DELETE 시 각각 다른 메시지를 출력한다.
-- 이때 OR을 써야함에 주의하자
CREATE OR REPLACE TRIGGER TR_Message
AFTER INSERT OR UPDATE OR DELETE ON tCity
BEGIN
IF INSERTING THEN
DBMS_OUTPUT.PUT_LINE('새로운 도시를 삽입하였습니다.');
END IF;
IF UPDATING THEN
DBMS_OUTPUT.PUT_LINE('도시 정보를 갱신하였습니다.');
END IF;
IF DELETING THEN
DBMS_OUTPUT.PUT_LINE('도시를 삭제하였습니다.');
END IF;
END;
INSERT INTO tCity VALUES ('여주',608,11,'n','경기');
UPDATE tCity SET popu = 12 WHERE name = '여주';
DELETE FROM tCity WHERE NAME = '여주';
-- 삭제를 하지 않고 트리거를 잠시 정지시켜 놓을 때는 다음 두 구문을 사용한다.
ALTER TRIGGER TR_Message ENABLE | DISABLE;
ALTER TABLE tCity ENABLE | DISABLE ALL TRIGGERS;
-- SQL 명령 하나에 대해 한 번 호출되는 트리거를 문장(Statement) 트리거라고 한다.
UPDATE tCity SET popu = popu + 1 WHERE region = '경기';
-- NEW는 INSERT, UPDATE시 변경된 후의 레코드를, OLD는 변경되기 전의 레코드를 가진다.
-- ':'을 앞에 붙여 이전과 이후를 비교해 볼 수 있다.
CREATE OR REPLACE TRIGGER TR_Message
AFTER UPDATE ON tCity
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE(:OLD.name || ':' || :OLD.popu || '->' || :NEW.popu);
END;
-- OLD, NEW가 맘에 들지 않으면 FOR EACH ROW 앞에 다음 구문으로 별명을 지정한다.
REFERENCING OLD AS pre_rec NEW AS post_rec
UPDATE tCity SET area = area + 1 WHERE name = '서울';
-- 트리거 발생제한 : 행에 대한 모든 변화를 잡지 말고, 관심있는 열이 바뀌었을 때만 트리거가 발생하도록 하자.
-- AFTER UPDATE OF 열 ON 테이블 구문을 활용한다.
CREATE OR REPLACE TRIGGER TR_Message
AFTER UPDATE OF popu ON tCity
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE(:OLD.name || ':' || :OLD.popu || '->' || :NEW.popu);
END;
CREATE OR REPLACE TRIGGER TR_Message
AFTER UPDATE ON tCity
FOR EACH ROW
WHEN (NEW.popu > 10)
BEGIN
DBMS_OUTPUT.PUT_LINE(:OLD.name || ':' || :OLD.popu || '->' || :NEW.popu);
END;
UPDATE tCity SET popu = popu + 1 WHERE region = '강원';
-- 트리거의 예외 발생 : 트리거는 무결성을 지키는 궁극의 장치다.
-- 하지만 무겁고 느리기 때문에 규칙 검사 체크, 기본값, NULL허용 등의 간편한 제약을 써야 한다.
-- 트리거가 중첩되면 무한 루프에 빠지거나 성능이 떨어질 수 있다.
CREATE OR REPLACE TRIGGER TR_Message
AFTER UPDATE ON tCity
FOR EACH ROW
BEGIN
IF :NEW.popu > 10000 THEN
RAISE_APPLICATION_ERROR(-20000, '인구가 1억을 넘을 수는 없습니다.');
END IF;
DBMS_OUTPUT.PUT_LINE(:OLD.name || ':' || :OLD.popu || '->' || :NEW.popu);
END;
UPDATE tCity SET popu = 12345 WHERE name = '서울';
-- AFTER 문을 쓰게 되면, 먼저 삽입을 한 다음 롤백을 하는 식으로 트리거가 동작한다.
CREATE OR REPLACE TRIGGER TR_PreventChoi
AFTER INSERT ON tStaff
FOR EACH ROW
BEGIN
IF SUBSTR(:NEW.name,1,1) = '최' THEN
RAISE_APPLICATION_ERROR(-20000, '최가는 안되!');
END IF;
END;
INSERT INTO tStaff VALUES ('최무선','총무부','남','2021-6-25','사원',280,0);
-- 만약 성이 최씨인 사람을 김씨로 바꾸어 삽입하려고 하면, BEFORE를 써야 한다.
CREATE OR REPLACE TRIGGER TR_PreventChoi
BEFORE INSERT ON tStaff
FOR EACH ROW
BEGIN
IF SUBSTR(:NEW.name,1,1) = '최' THEN
DBMS_OUTPUT.PUT_LINE('최가는 안되');
:NEW.name := '김' || SUBSTR(:NEW.name, 2);
END IF;
END;
CREATE OR REPLACE TRIGGER TR_DoubleMoney
BEFORE INSERT ON tMember
FOR EACH ROW
BEGIN
IF :NEW.age = 18 THEN
:NEW.money := :NEW.money * 2;
END IF;
END;
INSERT INTO tMember (member, age, email, addr, money) VALUES
('신입생', 18, 'fresher@kyunghee.ac.kr', '서울 회기동', 8000);
-- 다음 뷰는 자동차와 제조사의 정보를 조인해서 보여준다.
CREATE VIEW vCarMaker AS
SELECT tCar.*, factory, domestic FROM tCar INNER JOIN tMaker ON
tCar.maker = tMaker.maker;
INSERT INTO vCarMaker VALUES('티코', 800, 900, '대우', '울릉', 'y');
-- 다음 트리거를 정의하면 위의 INSERT문을 실행했을때 자동으로 두 개의 뷰로 나누어 삽입을 실행한다.
CREATE OR REPLACE TRIGGER TR_AddNewCar
INSTEAD OF INSERT ON vCarMaker
FOR EACH ROW
BEGIN
INSERT INTO tCar (car, capacity, price, maker) VALUES
(:NEW.car, :NEW.capacity, :NEW.price, :NEW.maker);
INSERT INTO tMaker (maker, factory, domestic) VALUES
(:NEW.maker, :NEW.factory, :NEW.domestic);
END;
CREATE OR REPLACE TRIGGER TR_Message2
AFTER UPDATE ON tCity
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE(:OLD.name || '면적:' || :OLD.area || '->' || :NEW.area);
END;
CREATE OR REPLACE TRIGGER TR_Message3
AFTER UPDATE ON tCity
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE(:OLD.name || '지역:' || :OLD.region || '->' || :NEW.region);
END;
-- 비슷한 여러 개의 트리거를 사용할 때, FOLLOWS를 이용하여 순서를 지정할 수 있다.
CREATE OR REPLACE TRIGGER TR_Message3
AFTER UPDATE ON tCity
FOR EACH ROW
FOLLOWS TR_Message
BEGIN
DBMS_OUTPUT.PUT_LINE(:OLD.name || '지역:' || :OLD.region || '->' || :NEW.region);
END;
UPDATE tCity SET area = 1000, popu = 12345 WHERE name = '서울';
-- 중첩 트리거는 트리거가 또 다른 트리거를 호출하는 구조이다.
-- 이전의 TR_AddNewCar 트리거에서 TR_OnNewCar를 호출한다.
-- 중첩은 일반적으로 바람직하지 않고, 신중하게 정의해야 한다.
CREATE OR REPLACE TRIGGER TR_OnNewCar
AFTER INSERT ON tCar
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE('신차가 출시되었습니다.');
END;
INSERT INTO vCarMaker VALUES('티코', 800, 900, '대우', '울릉', 'y');
-- DDL 트리거 : CREATE, DROP, ALTER 같은 이벤트를 설치하면 서버의 상태를 쉽게 모니터링할 수 있다.
CREATE OR REPLACE TRIGGER TR_Change
AFTER DDL ON DATABASE
BEGIN
DBMS_OUTPUT.PUT_LINE('명령 : ' || ora_sysevent);
DBMS_OUTPUT.PUT_LINE('타입 : ' || ora_dict_obj_type);
DBMS_OUTPUT.PUT_LINE('이름 : ' || ora_dict_obj_name);
END;
CREATE TABLE tTemp ( id INT );
DROP TABLE tTemp;
CREATE TABLE tCityHistory
(
dt DATE,
suser VARCHAR(20),
ip VARCHAR(20),
action VARCHAR(10),
name CHAR(10),
area VARCHAR(30) NULL,
popu VARCHAR(30) NULL
);
-- 변경 이력 관리 : 트리거를 이용해 tCityHistory에 변경 이력을 저장하는 예제이다.
CREATE OR REPLACE TRIGGER TR_History
AFTER INSERT OR UPDATE OR DELETE ON tCity
FOR EACH ROW
DECLARE
areaChange VARCHAR(30);
popuChange VARCHAR(30);
suser VARCHAR(20);
ip VARCHAR(20);
BEGIN
SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') INTO suser FROM DUAL;
SELECT SYS_CONTEXT('USERENV', 'IP_ADDRESS') INTO ip FROM DUAL;
IF INSERTING THEN
INSERT INTO tCityHistory VALUES
(SYSDATE, suser, ip, 'INSERT', :NEW.name, :NEW.area, :NEW.popu);
END IF;
IF UPDATING THEN
IF :OLD.area = :NEW.area THEN
areaChange := :OLD.area;
ELSE
areaChange := :OLD.area || '->' || :NEW.area;
END IF;
IF :OLD.popu = :NEW.popu THEN
popuChange := :OLD.popu;
ELSE
popuChange := :OLD.popu || '->' || :NEW.popu;
END IF;
INSERT INTO tCityHistory VALUES
(SYSDATE, suser, ip, 'UPDATE', :NEW.name, areaChange, popuChange);
END IF;
IF DELETING THEN
INSERT INTO tCityHistory VALUES
(SYSDATE, suser, ip, 'DELETE', :OLD.name, :OLD.area, :OLD.popu);
END IF;
END;
'Database' 카테고리의 다른 글
SQL 기초 - 19. 트랜잭션 모드와 락 (0) | 2021.10.25 |
---|---|
SQL 기초 - 17. 커서와 동적쿼리 (0) | 2021.10.25 |
SQL 기초 - 16. 저장 프로시저, 인수, 함수(+테이블 반환 함수) (0) | 2021.10.25 |
SQL 기초 - 15. PL/SQL 제어문 (IF, LOOP...), 예외처리(EXCEPTION, RAISE) (0) | 2021.10.25 |
SQL 기초 - 14. 테이블 관리 (테이블 수정, 필드 수정, SQL ALTER) (0) | 2021.10.21 |
Comments