PL/SQL触发器 #
一、触发器概述 #
1.1 什么是触发器 #
触发器(Trigger)是存储在数据库中的特殊PL/SQL块,当特定事件发生时自动执行。
text
触发器特点
├── 自动执行
│ └── 事件触发
├── 不能直接调用
│ └── 由数据库自动触发
├── 事件类型
│ ├── DML事件
│ ├── DDL事件
│ └── 系统事件
└── 执行时机
├── BEFORE
├── AFTER
└── INSTEAD OF
1.2 触发器类型 #
text
触发器类型
├── DML触发器
│ ├── 语句级触发器
│ └── 行级触发器
├── INSTEAD OF触发器
│ └── 用于视图
├── DDL触发器
│ └── CREATE, ALTER, DROP
└── 系统触发器
├── 登录/登出
└── 启动/关闭
二、DML触发器 #
2.1 基本语法 #
sql
-- DML触发器基本语法
CREATE [OR REPLACE] TRIGGER trigger_name
{BEFORE|AFTER|INSTEAD OF}
{INSERT|UPDATE|DELETE} [OR {INSERT|UPDATE|DELETE}...]
ON table_name
[FOR EACH ROW]
[WHEN (condition)]
[FOLLOWS trigger_name]
[ENABLE|DISABLE]
AS|IS
-- 声明部分
BEGIN
-- 执行部分
END;
2.2 语句级触发器 #
sql
-- 语句级触发器:每条语句执行一次
CREATE OR REPLACE TRIGGER trg_audit_employees
AFTER INSERT OR UPDATE OR DELETE ON employees
DECLARE
v_action VARCHAR2(20);
BEGIN
IF INSERTING THEN
v_action := 'INSERT';
ELSIF UPDATING THEN
v_action := 'UPDATE';
ELSIF DELETING THEN
v_action := 'DELETE';
END IF;
INSERT INTO audit_log (table_name, action, action_time, user_name)
VALUES ('employees', v_action, SYSTIMESTAMP, USER);
END;
/
-- 测试
INSERT INTO employees (employee_id, first_name, last_name) VALUES (999, 'Test', 'User');
SELECT * FROM audit_log;
2.3 行级触发器 #
sql
-- 行级触发器:每行执行一次
CREATE OR REPLACE TRIGGER trg_salary_audit
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary != :OLD.salary THEN
INSERT INTO salary_audit (
employee_id,
old_salary,
new_salary,
change_date,
changed_by
) VALUES (
:NEW.employee_id,
:OLD.salary,
:NEW.salary,
SYSTIMESTAMP,
USER
);
END IF;
END;
/
-- 测试
UPDATE employees SET salary = salary * 1.1 WHERE department_id = 10;
SELECT * FROM salary_audit;
-- :NEW和:OLD说明
-- INSERT::NEW可用,:OLD不可用
-- UPDATE::NEW和:OLD都可用
-- DELETE::OLD可用,:NEW不可用
2.4 WHEN条件 #
sql
-- WHEN条件:限制触发器执行
CREATE OR REPLACE TRIGGER trg_high_salary_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW
WHEN (NEW.salary > 10000) -- 注意:不使用冒号
BEGIN
INSERT INTO high_salary_audit (employee_id, salary)
VALUES (:NEW.employee_id, :NEW.salary);
END;
/
-- 复杂WHEN条件
CREATE OR REPLACE TRIGGER trg_salary_increase_audit
AFTER UPDATE OF salary ON employees
FOR EACH ROW
WHEN (NEW.salary > OLD.salary * 1.5) -- 薪资增长超过50%
BEGIN
INSERT INTO salary_increase_audit (
employee_id, old_salary, new_salary, increase_pct
) VALUES (
:NEW.employee_id, :OLD.salary, :NEW.salary,
(:NEW.salary - :OLD.salary) / :OLD.salary * 100
);
END;
/
2.5 多事件触发器 #
sql
-- 多事件触发器
CREATE OR REPLACE TRIGGER trg_employee_audit
BEFORE INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
BEGIN
IF INSERTING THEN
INSERT INTO employee_audit (
employee_id, action, action_time, new_data
) VALUES (
:NEW.employee_id, 'INSERT', SYSTIMESTAMP,
:NEW.first_name || ' ' || :NEW.last_name
);
ELSIF UPDATING THEN
INSERT INTO employee_audit (
employee_id, action, action_time, old_data, new_data
) VALUES (
:NEW.employee_id, 'UPDATE', SYSTIMESTAMP,
:OLD.first_name || ' ' || :OLD.last_name,
:NEW.first_name || ' ' || :NEW.last_name
);
ELSIF DELETING THEN
INSERT INTO employee_audit (
employee_id, action, action_time, old_data
) VALUES (
:OLD.employee_id, 'DELETE', SYSTIMESTAMP,
:OLD.first_name || ' ' || :OLD.last_name
);
END IF;
END;
/
-- 检查特定列是否更新
CREATE OR REPLACE TRIGGER trg_check_update
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
IF UPDATING('salary') THEN
DBMS_OUTPUT.PUT_LINE('Salary updated');
END IF;
IF UPDATING('department_id') THEN
DBMS_OUTPUT.PUT_LINE('Department updated');
END IF;
END;
/
三、INSTEAD OF触发器 #
3.1 视图更新问题 #
sql
-- 创建不可更新视图
CREATE VIEW emp_dept_view AS
SELECT
e.employee_id,
e.first_name,
e.last_name,
e.salary,
d.department_id,
d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;
-- 尝试更新视图
UPDATE emp_dept_view SET department_name = 'New IT' WHERE employee_id = 100;
-- ORA-01779: cannot modify a column which maps to a non key-preserved table
3.2 创建INSTEAD OF触发器 #
sql
-- 创建INSTEAD OF触发器
CREATE OR REPLACE TRIGGER trg_emp_dept_view_update
INSTEAD OF UPDATE ON emp_dept_view
FOR EACH ROW
BEGIN
-- 更新employees表
UPDATE employees
SET
first_name = :NEW.first_name,
last_name = :NEW.last_name,
salary = :NEW.salary
WHERE employee_id = :NEW.employee_id;
-- 更新departments表
UPDATE departments
SET department_name = :NEW.department_name
WHERE department_id = :NEW.department_id;
END;
/
-- 测试
UPDATE emp_dept_view
SET first_name = 'John', salary = 15000, department_name = 'IT Department'
WHERE employee_id = 100;
3.3 INSTEAD OF INSERT触发器 #
sql
-- INSTEAD OF INSERT触发器
CREATE OR REPLACE TRIGGER trg_emp_dept_view_insert
INSTEAD OF INSERT ON emp_dept_view
FOR EACH ROW
DECLARE
v_dept_id NUMBER;
BEGIN
-- 查找或创建部门
BEGIN
SELECT department_id INTO v_dept_id
FROM departments
WHERE department_name = :NEW.department_name;
EXCEPTION
WHEN NO_DATA_FOUND THEN
INSERT INTO departments (department_id, department_name)
VALUES (dept_seq.NEXTVAL, :NEW.department_name)
RETURNING department_id INTO v_dept_id;
END;
-- 插入员工
INSERT INTO employees (employee_id, first_name, last_name, salary, department_id)
VALUES (:NEW.employee_id, :NEW.first_name, :NEW.last_name, :NEW.salary, v_dept_id);
END;
/
四、DDL触发器 #
4.1 创建DDL触发器 #
sql
-- DDL触发器:监控DDL操作
CREATE OR REPLACE TRIGGER trg_ddl_audit
AFTER DDL ON SCHEMA
DECLARE
v_sql_text VARCHAR2(4000);
BEGIN
SELECT sql_text INTO v_sql_text
FROM v$sql
WHERE sql_id = (SELECT prev_sql_id FROM v$session WHERE sid = SYS_CONTEXT('USERENV', 'SID'));
INSERT INTO ddl_audit (
object_type,
object_name,
ddl_action,
ddl_sql,
action_time,
user_name
) VALUES (
ORA_DICT_OBJ_TYPE,
ORA_DICT_OBJ_NAME,
ORA_SYSEVENT,
v_sql_text,
SYSTIMESTAMP,
USER
);
END;
/
-- 测试
CREATE TABLE test_table (id NUMBER);
DROP TABLE test_table;
SELECT * FROM ddl_audit;
4.2 DDL事件属性函数 #
sql
-- DDL事件属性函数
-- ORA_DICT_OBJ_NAME:对象名称
-- ORA_DICT_OBJ_TYPE:对象类型
-- ORA_DICT_OBJ_OWNER:对象所有者
-- ORA_SYSEVENT:系统事件
-- ORA_LOGIN_USER:登录用户
CREATE OR REPLACE TRIGGER trg_ddl_detail
AFTER DDL ON SCHEMA
BEGIN
DBMS_OUTPUT.PUT_LINE('Event: ' || ORA_SYSEVENT);
DBMS_OUTPUT.PUT_LINE('Object: ' || ORA_DICT_OBJ_NAME);
DBMS_OUTPUT.PUT_LINE('Type: ' || ORA_DICT_OBJ_TYPE);
DBMS_OUTPUT.PUT_LINE('Owner: ' || ORA_DICT_OBJ_OWNER);
DBMS_OUTPUT.PUT_LINE('User: ' || ORA_LOGIN_USER);
END;
/
五、系统触发器 #
5.1 登录/登出触发器 #
sql
-- 登录触发器
CREATE OR REPLACE TRIGGER trg_logon_audit
AFTER LOGON ON DATABASE
BEGIN
INSERT INTO logon_audit (
user_name,
session_id,
logon_time,
ip_address
) VALUES (
USER,
SYS_CONTEXT('USERENV', 'SESSIONID'),
SYSTIMESTAMP,
SYS_CONTEXT('USERENV', 'IP_ADDRESS')
);
END;
/
-- 登出触发器
CREATE OR REPLACE TRIGGER trg_logoff_audit
BEFORE LOGOFF ON DATABASE
BEGIN
UPDATE logon_audit
SET logoff_time = SYSTIMESTAMP
WHERE session_id = SYS_CONTEXT('USERENV', 'SESSIONID')
AND logoff_time IS NULL;
END;
/
-- 限制登录时间
CREATE OR REPLACE TRIGGER trg_restrict_logon
BEFORE LOGON ON DATABASE
BEGIN
IF TO_CHAR(SYSDATE, 'HH24') NOT BETWEEN 8 AND 18 THEN
IF USER NOT IN ('SYS', 'SYSTEM', 'ADMIN') THEN
RAISE_APPLICATION_ERROR(-20001, 'Login only allowed during business hours');
END IF;
END IF;
END;
/
5.2 启动/关闭触发器 #
sql
-- 数据库启动触发器
CREATE OR REPLACE TRIGGER trg_startup
AFTER STARTUP ON DATABASE
BEGIN
INSERT INTO db_event_log (event_type, event_time)
VALUES ('STARTUP', SYSTIMESTAMP);
END;
/
-- 数据库关闭触发器
CREATE OR REPLACE TRIGGER trg_shutdown
BEFORE SHUTDOWN ON DATABASE
BEGIN
INSERT INTO db_event_log (event_type, event_time)
VALUES ('SHUTDOWN', SYSTIMESTAMP);
END;
/
六、触发器管理 #
6.1 查看触发器 #
sql
-- 查看触发器信息
SELECT trigger_name, trigger_type, triggering_event, status
FROM user_triggers
WHERE table_name = 'EMPLOYEES';
-- 查看触发器源代码
SELECT text FROM user_source
WHERE name = 'TRG_SALARY_AUDIT' AND type = 'TRIGGER'
ORDER BY line;
-- 查看触发器详细信息
SELECT * FROM user_triggers WHERE trigger_name = 'TRG_SALARY_AUDIT';
6.2 启用/禁用触发器 #
sql
-- 禁用触发器
ALTER TRIGGER trg_salary_audit DISABLE;
-- 启用触发器
ALTER TRIGGER trg_salary_audit ENABLE;
-- 禁用表的所有触发器
ALTER TABLE employees DISABLE ALL TRIGGERS;
-- 启用表的所有触发器
ALTER TABLE employees ENABLE ALL TRIGGERS;
6.3 编译触发器 #
sql
-- 编译触发器
ALTER TRIGGER trg_salary_audit COMPILE;
-- 带调试信息编译
ALTER TRIGGER trg_salary_audit COMPILE DEBUG;
6.4 删除触发器 #
sql
-- 删除触发器
DROP TRIGGER trg_salary_audit;
-- 删除不存在的触发器(避免错误)
DROP TRIGGER IF EXISTS trg_salary_audit;
七、触发器最佳实践 #
7.1 避免递归触发 #
sql
-- 问题:递归触发
CREATE OR REPLACE TRIGGER trg_recursive
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
UPDATE employees SET salary = :NEW.salary * 1.1
WHERE employee_id = :NEW.employee_id; -- 再次触发UPDATE
END;
/
-- 解决:使用条件判断
CREATE OR REPLACE TRIGGER trg_no_recursive
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary != :OLD.salary * 1.1 THEN
UPDATE employees SET salary = :NEW.salary * 1.1
WHERE employee_id = :NEW.employee_id;
END IF;
END;
/
-- 或使用包变量控制
CREATE OR REPLACE PACKAGE pkg_trigger_control AS
g_in_trigger BOOLEAN := FALSE;
END;
/
CREATE OR REPLACE TRIGGER trg_controlled
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
IF NOT pkg_trigger_control.g_in_trigger THEN
pkg_trigger_control.g_in_trigger := TRUE;
UPDATE employees SET salary = :NEW.salary * 1.1
WHERE employee_id = :NEW.employee_id;
pkg_trigger_control.g_in_trigger := FALSE;
END IF;
END;
/
7.2 性能考虑 #
sql
-- 1. 避免在触发器中执行复杂操作
-- 2. 使用WHEN条件减少触发次数
-- 3. 避免在触发器中查询同一表
-- 4. 考虑使用复合触发器(11g+)
-- 复合触发器(11g+)
CREATE OR REPLACE TRIGGER trg_compound_employee
FOR INSERT OR UPDATE ON employees
COMPOUND TRIGGER
-- 声明部分(共享)
TYPE t_emp_list IS TABLE OF NUMBER;
v_emp_list t_emp_list := t_emp_list();
-- 语句级BEFORE
BEFORE STATEMENT IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Before statement');
END BEFORE STATEMENT;
-- 行级BEFORE
BEFORE EACH ROW IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Before row: ' || :NEW.employee_id);
END BEFORE EACH ROW;
-- 行级AFTER
AFTER EACH ROW IS
BEGIN
v_emp_list.EXTEND;
v_emp_list(v_emp_list.LAST) := :NEW.employee_id;
END AFTER EACH ROW;
-- 语句级AFTER
AFTER STATEMENT IS
BEGIN
FOR i IN 1..v_emp_list.COUNT LOOP
DBMS_OUTPUT.PUT_LINE('After statement: ' || v_emp_list(i));
END LOOP;
END AFTER STATEMENT;
END;
/
7.3 错误处理 #
sql
-- 触发器中的错误处理
CREATE OR REPLACE TRIGGER trg_check_salary
BEFORE INSERT OR UPDATE OF salary ON employees
FOR EACH ROW
DECLARE
v_max_salary NUMBER;
BEGIN
SELECT max_salary INTO v_max_salary
FROM jobs
WHERE job_id = :NEW.job_id;
IF :NEW.salary > v_max_salary THEN
RAISE_APPLICATION_ERROR(-20001,
'Salary exceeds maximum for job: ' || v_max_salary);
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL; -- 允许没有job_id的员工
WHEN OTHERS THEN
-- 记录错误但允许操作继续
INSERT INTO error_log (error_message, error_time)
VALUES (SQLERRM, SYSTIMESTAMP);
END;
/
八、实际应用示例 #
8.1 审计日志 #
sql
-- 通用审计触发器
CREATE OR REPLACE TRIGGER trg_audit_generic
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
DECLARE
v_operation VARCHAR2(10);
BEGIN
IF INSERTING THEN
v_operation := 'INSERT';
ELSIF UPDATING THEN
v_operation := 'UPDATE';
ELSIF DELETING THEN
v_operation := 'DELETE';
END IF;
INSERT INTO audit_trail (
table_name,
operation,
primary_key,
old_values,
new_values,
operation_time,
user_name
) VALUES (
'EMPLOYEES',
v_operation,
COALESCE(:NEW.employee_id, :OLD.employee_id),
CASE WHEN v_operation IN ('UPDATE', 'DELETE')
THEN :OLD.first_name || '|' || :OLD.last_name || '|' || :OLD.salary
END,
CASE WHEN v_operation IN ('INSERT', 'UPDATE')
THEN :NEW.first_name || '|' || :NEW.last_name || '|' || :NEW.salary
END,
SYSTIMESTAMP,
USER
);
END;
/
8.2 数据验证 #
sql
-- 数据验证触发器
CREATE OR REPLACE TRIGGER trg_validate_employee
BEFORE INSERT OR UPDATE ON employees
FOR EACH ROW
BEGIN
-- 验证邮箱格式
IF :NEW.email IS NOT NULL THEN
IF NOT REGEXP_LIKE(:NEW.email, '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') THEN
RAISE_APPLICATION_ERROR(-20001, 'Invalid email format');
END IF;
END IF;
-- 验证薪资范围
IF :NEW.salary < 0 THEN
RAISE_APPLICATION_ERROR(-20002, 'Salary cannot be negative');
END IF;
-- 验证入职日期
IF :NEW.hire_date > SYSDATE THEN
RAISE_APPLICATION_ERROR(-20003, 'Hire date cannot be in the future');
END IF;
END;
/
8.3 自动计算 #
sql
-- 自动计算字段
CREATE OR REPLACE TRIGGER trg_calculate_total
BEFORE INSERT OR UPDATE ON order_items
FOR EACH ROW
BEGIN
:NEW.total_price := :NEW.quantity * :NEW.unit_price;
:NEW.discount_amount := :NEW.total_price * NVL(:NEW.discount_pct, 0) / 100;
:NEW.final_price := :NEW.total_price - :NEW.discount_amount;
END;
/
-- 自动更新时间戳
CREATE OR REPLACE TRIGGER trg_update_timestamp
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
:NEW.last_update_time := SYSTIMESTAMP;
:NEW.last_update_user := USER;
END;
/
九、总结 #
触发器要点:
| 类型 | 触发时机 | 用途 |
|---|---|---|
| DML触发器 | INSERT/UPDATE/DELETE | 数据审计、验证 |
| INSTEAD OF | 视图操作 | 更新不可更新视图 |
| DDL触发器 | CREATE/ALTER/DROP | DDL审计 |
| 系统触发器 | LOGON/LOGOFF/STARTUP | 安全审计 |
最佳实践:
- 触发器逻辑要简单高效
- 避免递归触发
- 使用WHEN条件减少触发
- 完善的错误处理
- 注意触发器顺序(FOLLOWS)
下一步,让我们学习游标与异常!
最后更新:2026-03-27