1. Oracle触发器概述

Oracle数据库触发器是一种特殊的存储过程,它会在特定数据库事件(如INSERT、UPDATE、DELETE或DDL语句)发生时自动执行。触发器是数据库管理系统中的重要组成部分,它可以帮助我们实现数据完整性、业务规则自动化、审计日志记录等功能。

触发器具有以下特点:

  • 自动执行:无需显式调用,当指定事件发生时自动触发
  • 与表关联:触发器通常与特定的表或视图关联
  • 事件驱动:由特定的数据库事件激活
  • 隐式执行:对用户透明,在后台自动执行

2. 触发器的类型

Oracle数据库中的触发器可以根据不同的标准进行分类:

2.1 按触发时间分类

  • BEFORE触发器:在触发事件之前执行,常用于:

    • 修改即将被插入或更新的值
    • 检查或验证数据
    • 初始化数据
  • AFTER触发器:在触发事件之后执行,常用于:

    • 记录变更日志
    • 执行级联操作
    • 发送通知

2.2 按触发事件分类

  • DML触发器:由数据操作语言(INSERT、UPDATE、DELETE)触发
  • DDL触发器:由数据定义语言(CREATE、ALTER、DROP等)触发
  • 系统事件触发器:由系统事件(如数据库启动、关闭、登录、注销等)触发
  • INSTEAD OF触发器:用于视图,替代视图上的DML操作

2.3 按触发级别分类

  • 语句级触发器:对整个SQL语句只触发一次,无论影响多少行
  • 行级触发器:对SQL语句影响的每一行都触发一次

3. 触发器语法详解

3.1 创建触发器的基本语法

CREATE [OR REPLACE] TRIGGER trigger_name {BEFORE | AFTER | INSTEAD OF} {INSERT [OR] UPDATE [OR] DELETE} [OF column_name] ON table_name [REFERENCING OLD AS old NEW AS new] [FOR EACH ROW] [WHEN (condition)] DECLARE -- 声明变量 BEGIN -- 触发器逻辑 EXCEPTION -- 异常处理 END; / 

3.2 语法元素详解

  • CREATE [OR REPLACE] TRIGGER:创建或替换触发器
  • trigger_name:触发器名称
  • BEFORE | AFTER | INSTEAD OF:指定触发时机
  • INSERT | UPDATE | DELETE:指定触发事件,可以组合使用
  • OF column_name:仅当特定列被更新时触发(仅用于UPDATE)
  • ON table_name:指定触发器关联的表或视图
  • REFERENCING:指定新旧值的引用名称
  • FOR EACH ROW:指定为行级触发器,省略则为语句级触发器
  • WHEN (condition):指定触发条件,仅当条件满足时才执行触发器

3.3 触发器中的特殊变量

在行级触发器中,可以使用以下特殊变量引用被操作的数据:

  • :old:引用UPDATE或DELETE操作前的行数据
  • :new:引用INSERT或UPDATE操作后的行数据

对于INSERT操作,只有:new有值;对于DELETE操作,只有:old有值;对于UPDATE操作,:old和:new都有值。

3.4 触发器谓词

在触发器主体中,可以使用以下谓词判断触发事件类型:

  • INSERTING:如果触发事件是INSERT,返回TRUE
  • UPDATING:如果触发事件是UPDATE,返回TRUE
  • DELETING:如果触发事件是DELETE,返回TRUE

4. 触发器应用场景与示例

4.1 数据审计与日志记录

场景:记录表中所有数据变更的审计日志

示例:创建一个员工表和对应的审计日志表,然后创建触发器记录所有变更

-- 创建员工表 CREATE TABLE employees ( emp_id NUMBER PRIMARY KEY, emp_name VARCHAR2(50), salary NUMBER, department_id NUMBER, last_modified DATE ); -- 创建审计日志表 CREATE TABLE employees_audit ( audit_id NUMBER GENERATED ALWAYS AS IDENTITY, emp_id NUMBER, old_name VARCHAR2(50), new_name VARCHAR2(50), old_salary NUMBER, new_salary NUMBER, old_department_id NUMBER, new_department_id NUMBER, change_type VARCHAR2(10), change_date DATE, changed_by VARCHAR2(30) ); -- 创建审计触发器 CREATE OR REPLACE TRIGGER trg_employee_audit AFTER INSERT OR UPDATE OR DELETE ON employees FOR EACH ROW DECLARE v_user VARCHAR2(30); BEGIN -- 获取当前用户 SELECT USER INTO v_user FROM DUAL; -- 插入操作 IF INSERTING THEN INSERT INTO employees_audit ( emp_id, old_name, new_name, old_salary, new_salary, old_department_id, new_department_id, change_type, change_date, changed_by ) VALUES ( :new.emp_id, NULL, :new.emp_name, NULL, :new.salary, NULL, :new.department_id, 'INSERT', SYSDATE, v_user ); -- 更新操作 ELSIF UPDATING THEN INSERT INTO employees_audit ( emp_id, old_name, new_name, old_salary, new_salary, old_department_id, new_department_id, change_type, change_date, changed_by ) VALUES ( :new.emp_id, :old.emp_name, :new.emp_name, :old.salary, :new.salary, :old.department_id, :new.department_id, 'UPDATE', SYSDATE, v_user ); -- 删除操作 ELSIF DELETING THEN INSERT INTO employees_audit ( emp_id, old_name, new_name, old_salary, new_salary, old_department_id, new_department_id, change_type, change_date, changed_by ) VALUES ( :old.emp_id, :old.emp_name, NULL, :old.salary, NULL, :old.department_id, NULL, 'DELETE', SYSDATE, v_user ); END IF; END; / 

4.2 数据完整性约束

场景:确保员工的工资不能降低,除非有特殊授权

示例:创建触发器防止工资被非法降低

CREATE OR REPLACE TRIGGER trg_salary_check BEFORE UPDATE OF salary ON employees FOR EACH ROW DECLARE v_has_auth NUMBER; BEGIN -- 检查工资是否被降低 IF :old.salary > :new.salary THEN -- 检查是否有特殊授权(假设有一个权限表) SELECT COUNT(*) INTO v_has_auth FROM salary_reduction_auth WHERE emp_id = :old.emp_id AND auth_date = TRUNC(SYSDATE) AND approved_by = USER; -- 如果没有授权,则阻止更新 IF v_has_auth = 0 THEN RAISE_APPLICATION_ERROR(-20001, '工资不能降低,除非有特殊授权'); END IF; END IF; END; / 

4.3 自动计算衍生数据

场景:在订单表中,当订单明细变化时自动更新订单总金额

示例:创建订单和订单明细表,并设置触发器自动计算订单总额

-- 创建订单表 CREATE TABLE orders ( order_id NUMBER PRIMARY KEY, customer_id NUMBER, order_date DATE, total_amount NUMBER, status VARCHAR2(20) ); -- 创建订单明细表 CREATE TABLE order_items ( item_id NUMBER PRIMARY KEY, order_id NUMBER REFERENCES orders(order_id), product_id NUMBER, quantity NUMBER, unit_price NUMBER, CONSTRAINT fk_order_items FOREIGN KEY (order_id) REFERENCES orders(order_id) ); -- 创建触发器,在订单明细变化时更新订单总额 CREATE OR REPLACE TRIGGER trg_update_order_total AFTER INSERT OR UPDATE OR DELETE ON order_items DECLARE v_order_id NUMBER; BEGIN -- 根据操作类型确定需要更新的订单ID IF INSERTING OR UPDATING THEN v_order_id := :new.order_id; ELSIF DELETING THEN v_order_id := :old.order_id; END IF; -- 更新订单总额 UPDATE orders o SET total_amount = ( SELECT SUM(oi.quantity * oi.unit_price) FROM order_items oi WHERE oi.order_id = o.order_id ) WHERE o.order_id = v_order_id; END; / 

4.4 复杂业务规则实施

场景:实现库存管理,当库存低于安全水平时自动生成采购订单

示例:创建产品表、库存表和采购订单表,并设置触发器自动生成采购订单

-- 创建产品表 CREATE TABLE products ( product_id NUMBER PRIMARY KEY, product_name VARCHAR2(100), unit_price NUMBER, safety_stock NUMBER ); -- 创建库存表 CREATE TABLE inventory ( inventory_id NUMBER PRIMARY KEY, product_id NUMBER REFERENCES products(product_id), quantity NUMBER, last_updated DATE ); -- 创建采购订单表 CREATE TABLE purchase_orders ( po_id NUMBER PRIMARY KEY, product_id NUMBER REFERENCES products(product_id), quantity NUMBER, order_date DATE, status VARCHAR2(20) DEFAULT 'PENDING' ); -- 创建触发器,检查库存并生成采购订单 CREATE OR REPLACE TRIGGER trg_check_inventory AFTER UPDATE OF quantity ON inventory FOR EACH ROW DECLARE v_safety_stock NUMBER; v_product_name VARCHAR2(100); BEGIN -- 获取产品的安全库存 SELECT safety_stock, product_name INTO v_safety_stock, v_product_name FROM products WHERE product_id = :new.product_id; -- 检查库存是否低于安全水平 IF :new.quantity < v_safety_stock THEN -- 生成采购订单,订购数量为安全库存的两倍 INSERT INTO purchase_orders ( po_id, product_id, quantity, order_date ) VALUES ( seq_po_id.NEXTVAL, :new.product_id, v_safety_stock * 2, SYSDATE ); -- 记录日志(假设有一个日志表) INSERT INTO system_logs ( log_id, log_date, log_message ) VALUES ( seq_log_id.NEXTVAL, SYSDATE, '自动生成采购订单: 产品 ' || v_product_name || ' 库存低于安全水平,已生成采购订单 #' || seq_po_id.CURRVAL ); END IF; END; / 

4.5 数据同步与复制

场景:将主数据库中的关键数据实时同步到报表数据库

示例:创建触发器实现数据同步

-- 假设我们有一个远程数据库链接名为 REPORT_DB_LINK -- 创建触发器同步员工数据到报表数据库 CREATE OR REPLACE TRIGGER trg_sync_employee_to_report AFTER INSERT OR UPDATE OR DELETE ON employees FOR EACH ROW DECLARE v_exists NUMBER; BEGIN -- 插入操作 IF INSERTING THEN -- 在报表数据库中插入新记录 EXECUTE IMMEDIATE 'INSERT INTO employees@REPORT_DB_LINK ' || '(emp_id, emp_name, salary, department_id, last_modified) ' || 'VALUES (:1, :2, :3, :4, :5)' USING :new.emp_id, :new.emp_name, :new.salary, :new.department_id, SYSDATE; -- 更新操作 ELSIF UPDATING THEN -- 检查报表数据库中是否存在该记录 EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM employees@REPORT_DB_LINK WHERE emp_id = :1' INTO v_exists USING :old.emp_id; IF v_exists > 0 THEN -- 更新报表数据库中的记录 EXECUTE IMMEDIATE 'UPDATE employees@REPORT_DB_LINK SET ' || 'emp_name = :1, salary = :2, department_id = :3, ' || 'last_modified = :4 WHERE emp_id = :5' USING :new.emp_name, :new.salary, :new.department_id, SYSDATE, :old.emp_id; ELSE -- 如果不存在,则插入新记录 EXECUTE IMMEDIATE 'INSERT INTO employees@REPORT_DB_LINK ' || '(emp_id, emp_name, salary, department_id, last_modified) ' || 'VALUES (:1, :2, :3, :4, :5)' USING :new.emp_id, :new.emp_name, :new.salary, :new.department_id, SYSDATE; END IF; -- 删除操作 ELSIF DELETING THEN -- 从报表数据库中删除记录 EXECUTE IMMEDIATE 'DELETE FROM employees@REPORT_DB_LINK WHERE emp_id = :1' USING :old.emp_id; END IF; EXCEPTION WHEN OTHERS THEN -- 记录同步错误 INSERT INTO sync_errors ( error_id, error_date, table_name, operation, key_value, error_message ) VALUES ( seq_error_id.NEXTVAL, SYSDATE, 'EMPLOYEES', CASE WHEN INSERTING THEN 'INSERT' WHEN UPDATING THEN 'UPDATE' WHEN DELETING THEN 'DELETE' END, CASE WHEN INSERTING THEN :new.emp_id WHEN UPDATING THEN :old.emp_id WHEN DELETING THEN :old.emp_id END, SQLERRM ); END; / 

4.6 DDL触发器应用

场景:记录数据库中的所有DDL操作,用于安全和审计

示例:创建DDL触发器记录所有DDL操作

-- 创建DDL审计表 CREATE TABLE ddl_audit ( audit_id NUMBER GENERATED ALWAYS AS IDENTITY, operation_date DATE, user_name VARCHAR2(30), object_type VARCHAR2(20), object_name VARCHAR2(30), ddl_operation VARCHAR2(30), sql_text VARCHAR2(4000) ); -- 创建DDL触发器 CREATE OR REPLACE TRIGGER trg_ddl_audit AFTER DDL ON DATABASE BEGIN INSERT INTO ddl_audit ( operation_date, user_name, object_type, object_name, ddl_operation, sql_text ) VALUES ( SYSDATE, ORA_LOGIN_USER, ORA_DICT_OBJ_TYPE, ORA_DICT_OBJ_NAME, ORA_SYSEVENT, ORA_SQL_TXT ); EXCEPTION WHEN OTHERS THEN -- 记录错误但不阻止DDL操作 INSERT INTO ddl_audit ( operation_date, user_name, object_type, object_name, ddl_operation, sql_text ) VALUES ( SYSDATE, ORA_LOGIN_USER, 'ERROR', ORA_DICT_OBJ_NAME, ORA_SYSEVENT, 'Error capturing DDL: ' || SQLERRM ); END; / 

4.7 系统事件触发器应用

场景:记录用户登录和登出信息,用于安全审计

示例:创建系统事件触发器记录用户登录和登出

-- 创建用户会话审计表 CREATE TABLE session_audit ( audit_id NUMBER GENERATED ALWAYS AS IDENTITY, user_name VARCHAR2(30), session_id NUMBER, login_time DATE, logout_time DATE, ip_address VARCHAR2(20), terminal VARCHAR2(30), os_user VARCHAR2(30) ); -- 创建登录触发器 CREATE OR REPLACE TRIGGER trg_logon_audit AFTER LOGON ON DATABASE BEGIN INSERT INTO session_audit ( user_name, session_id, login_time, ip_address, terminal, os_user ) VALUES ( USER, SYS_CONTEXT('USERENV', 'SESSIONID'), SYSDATE, SYS_CONTEXT('USERENV', 'IP_ADDRESS'), SYS_CONTEXT('USERENV', 'TERMINAL'), SYS_CONTEXT('USERENV', 'OS_USER') ); EXCEPTION WHEN OTHERS THEN NULL; -- 不阻止用户登录 END; / -- 创建登出触发器 CREATE OR REPLACE TRIGGER trg_logoff_audit BEFORE LOGOFF ON DATABASE BEGIN UPDATE session_audit SET logout_time = SYSDATE WHERE session_id = SYS_CONTEXT('USERENV', 'SESSIONID') AND user_name = USER AND logout_time IS NULL; EXCEPTION WHEN OTHERS THEN NULL; -- 不阻止用户登出 END; / 

5. 触发器最佳实践

5.1 性能考虑

触发器可能会对数据库性能产生显著影响,特别是在批量操作时。以下是一些性能优化的建议:

  1. 谨慎使用行级触发器:行级触发器会对受影响的每一行执行一次,如果可能,使用语句级触发器替代。

  2. 避免在触发器中执行耗时操作:触发器应尽量简短高效,避免在触发器中执行复杂的查询或远程操作。

  3. 合理使用条件谓词:使用INSERTING、UPDATING、DELETING谓词来避免不必要的操作。

  4. 使用批量操作:当需要在触发器中处理多行数据时,考虑使用批量操作而不是逐行处理。

5.2 维护性考虑

  1. 文档化触发器逻辑:为每个触发器添加详细的注释,说明其用途和业务逻辑。

  2. 避免过度使用触发器:触发器会使业务逻辑分散,难以维护。考虑将复杂的业务逻辑放在存储过程或应用程序中。

  3. 错误处理:在触发器中实现适当的错误处理,避免因触发器失败而导致业务操作失败。

  4. 测试:对触发器进行充分的测试,特别是在复杂的应用场景中。

5.3 安全性考虑

  1. 权限控制:限制对触发器的修改权限,防止未授权的更改。

  2. 审计:对敏感表的触发器操作进行审计,记录所有变更。

  3. 避免SQL注入:在动态SQL中使用绑定变量,防止SQL注入攻击。

6. 触发器管理

6.1 查看触发器信息

-- 查看所有触发器 SELECT trigger_name, trigger_type, triggering_event, table_name, status FROM user_triggers; -- 查看特定表的触发器 SELECT trigger_name, trigger_type, triggering_event, status FROM user_triggers WHERE table_name = 'EMPLOYEES'; -- 查看触发器代码 SELECT trigger_body FROM user_triggers WHERE trigger_name = 'TRG_EMPLOYEE_AUDIT'; 

6.2 启用和禁用触发器

-- 禁用触发器 ALTER TRIGGER trg_employee_audit DISABLE; -- 启用触发器 ALTER TRIGGER trg_employee_audit ENABLE; -- 禁用表的所有触发器 ALTER TABLE employees DISABLE ALL TRIGGERS; -- 启用表的所有触发器 ALTER TABLE employees ENABLE ALL TRIGGERS; 

6.3 删除触发器

-- 删除触发器 DROP TRIGGER trg_employee_audit; 

7. 常见问题与解决方案

7.1 触发器中的变异表问题

问题:在行级触发器中查询或修改正在被触发的表,会导致”变异表”错误。

解决方案

  1. 使用语句级触发器替代行级触发器
  2. 使用复合触发器(Oracle 11g及以上版本)
  3. 使用自治事务
  4. 使用临时表存储中间结果

示例:使用复合触发器解决变异表问题

CREATE OR REPLACE TRIGGER trg_compound_employee FOR INSERT OR UPDATE OR DELETE ON employees COMPOUND TRIGGER -- 声明部分 TYPE emp_list IS TABLE OF employees.emp_id%TYPE; v_emp_ids emp_list := emp_list(); -- BEFORE STATEMENT部分 BEFORE STATEMENT IS BEGIN -- 初始化集合 v_emp_ids := emp_list(); END BEFORE STATEMENT; -- AFTER EACH ROW部分 AFTER EACH ROW IS BEGIN -- 收集受影响的员工ID IF INSERTING THEN v_emp_ids.EXTEND; v_emp_ids(v_emp_ids.LAST) := :new.emp_id; ELSIF UPDATING THEN v_emp_ids.EXTEND; v_emp_ids(v_emp_ids.LAST) := :new.emp_id; ELSIF DELETING THEN v_emp_ids.EXTEND; v_emp_ids(v_emp_ids.LAST) := :old.emp_id; END IF; END AFTER EACH ROW; -- AFTER STATEMENT部分 AFTER STATEMENT IS BEGIN -- 在语句级别处理收集的数据 FOR i IN 1..v_emp_ids.COUNT LOOP -- 这里可以安全地查询employees表 -- 例如,更新相关表或执行其他操作 NULL; END LOOP; END AFTER STATEMENT; END trg_compound_employee; / 

7.2 递归触发器问题

问题:触发器执行的操作又触发了同一个触发器,导致无限递归。

解决方案

  1. 使用包变量控制触发器执行
  2. 使用条件逻辑避免递归
  3. 重新设计业务逻辑

示例:使用包变量控制递归触发器

-- 创建控制包 CREATE OR REPLACE PACKAGE trigger_control IS g_employee_trigger_active BOOLEAN := TRUE; END trigger_control; / -- 修改触发器以避免递归 CREATE OR REPLACE TRIGGER trg_employee_recursive AFTER UPDATE ON employees FOR EACH ROW BEGIN -- 检查触发器是否应该执行 IF trigger_control.g_employee_trigger_active THEN -- 禁用触发器以避免递归 trigger_control.g_employee_trigger_active := FALSE; -- 执行触发器逻辑 -- 这里可能会更新employees表,但不会再次触发触发器 UPDATE other_table SET ... WHERE employee_id = :new.emp_id; -- 重新启用触发器 trigger_control.g_employee_trigger_active := TRUE; END IF; END; / 

7.3 触发器执行权限问题

问题:触发器执行时可能因为权限不足而失败。

解决方案

  1. 使用定义者权限(默认)而不是调用者权限
  2. 显式授予触发器所需的对象权限
  3. 使用角色授权(但注意角色在触发器中默认不生效)

示例:使用AUTHID子句指定触发器权限模式

-- 使用定义者权限(默认) CREATE OR REPLACE TRIGGER trg_employee_definer AFTER INSERT ON employees FOR EACH ROW AUTHID DEFINER -- 显式指定为定义者权限 BEGIN -- 使用定义者权限执行 INSERT INTO audit_table ... END; / -- 使用调用者权限 CREATE OR REPLACE TRIGGER trg_employee_current_user AFTER INSERT ON employees FOR EACH ROW AUTHID CURRENT_USER -- 指定为调用者权限 BEGIN -- 使用调用者权限执行 INSERT INTO audit_table ... END; / 

8. 高级触发器技术

8.1 INSTEAD OF触发器

INSTEAD OF触发器用于视图,替代视图上的DML操作。这使得可以通过视图更新不可更新的基表。

示例:创建一个连接视图并使用INSTEAD OF触发器使其可更新

-- 创建部门表 CREATE TABLE departments ( dept_id NUMBER PRIMARY KEY, dept_name VARCHAR2(50), location VARCHAR2(50) ); -- 创建员工表 CREATE TABLE employees ( emp_id NUMBER PRIMARY KEY, emp_name VARCHAR2(50), salary NUMBER, dept_id NUMBER REFERENCES departments(dept_id) ); -- 创建连接视图 CREATE OR REPLACE VIEW emp_dept_view AS SELECT e.emp_id, e.emp_name, e.salary, d.dept_id, d.dept_name, d.location FROM employees e JOIN departments d ON e.dept_id = d.dept_id; -- 创建INSTEAD OF INSERT触发器 CREATE OR REPLACE TRIGGER trg_iov_emp_dept_insert INSTEAD OF INSERT ON emp_dept_view DECLARE v_dept_count NUMBER; BEGIN -- 检查部门是否存在 SELECT COUNT(*) INTO v_dept_count FROM departments WHERE dept_id = :new.dept_id; -- 如果部门不存在,则插入新部门 IF v_dept_count = 0 THEN INSERT INTO departments (dept_id, dept_name, location) VALUES (:new.dept_id, :new.dept_name, :new.location); END IF; -- 插入员工记录 INSERT INTO employees (emp_id, emp_name, salary, dept_id) VALUES (:new.emp_id, :new.emp_name, :new.salary, :new.dept_id); END; / -- 创建INSTEAD OF UPDATE触发器 CREATE OR REPLACE TRIGGER trg_iov_emp_dept_update INSTEAD OF UPDATE ON emp_dept_view BEGIN -- 更新员工信息 IF UPDATING('emp_name') OR UPDATING('salary') OR UPDATING('dept_id') THEN UPDATE employees SET emp_name = :new.emp_name, salary = :new.salary, dept_id = :new.dept_id WHERE emp_id = :old.emp_id; END IF; -- 更新部门信息 IF UPDATING('dept_name') OR UPDATING('location') THEN UPDATE departments SET dept_name = :new.dept_name, location = :new.location WHERE dept_id = :old.dept_id; END IF; END; / -- 创建INSTEAD OF DELETE触发器 CREATE OR REPLACE TRIGGER trg_iov_emp_dept_delete INSTEAD OF DELETE ON emp_dept_view BEGIN -- 删除员工记录 DELETE FROM employees WHERE emp_id = :old.emp_id; -- 检查是否还有员工属于该部门 DECLARE v_emp_count NUMBER; BEGIN SELECT COUNT(*) INTO v_emp_count FROM employees WHERE dept_id = :old.dept_id; -- 如果没有员工属于该部门,则删除部门 IF v_emp_count = 0 THEN DELETE FROM departments WHERE dept_id = :old.dept_id; END IF; END; END; / 

8.2 复合触发器

复合触发器(Oracle 11g及以上版本)允许在一个触发器中定义多个时序点,便于共享变量和状态。

示例:使用复合触发器实现复杂的审计功能

CREATE OR REPLACE TRIGGER trg_compound_employee_audit FOR INSERT OR UPDATE OR DELETE ON employees COMPOUND TRIGGER -- 声明部分 TYPE emp_record IS RECORD ( emp_id employees.emp_id%TYPE, old_name employees.emp_name%TYPE, new_name employees.emp_name%TYPE, old_salary employees.salary%TYPE, new_salary employees.salary%TYPE, change_type VARCHAR2(10) ); TYPE emp_list IS TABLE OF emp_record; v_emp_changes emp_list := emp_list(); -- BEFORE STATEMENT部分 BEFORE STATEMENT IS BEGIN -- 初始化集合 v_emp_changes := emp_list(); END BEFORE STATEMENT; -- AFTER EACH ROW部分 AFTER EACH ROW IS BEGIN -- 收集变更信息 v_emp_changes.EXTEND; IF INSERTING THEN v_emp_changes(v_emp_changes.LAST) := emp_record( :new.emp_id, NULL, :new.emp_name, NULL, :new.salary, 'INSERT' ); ELSIF UPDATING THEN v_emp_changes(v_emp_changes.LAST) := emp_record( :new.emp_id, :old.emp_name, :new.emp_name, :old.salary, :new.salary, 'UPDATE' ); ELSIF DELETING THEN v_emp_changes(v_emp_changes.LAST) := emp_record( :old.emp_id, :old.emp_name, NULL, :old.salary, NULL, 'DELETE' ); END IF; END AFTER EACH ROW; -- AFTER STATEMENT部分 AFTER STATEMENT IS v_user VARCHAR2(30); BEGIN -- 获取当前用户 SELECT USER INTO v_user FROM DUAL; -- 批量插入审计记录 FOR i IN 1..v_emp_changes.COUNT LOOP INSERT INTO employees_audit ( emp_id, old_name, new_name, old_salary, new_salary, change_type, change_date, changed_by ) VALUES ( v_emp_changes(i).emp_id, v_emp_changes(i).old_name, v_emp_changes(i).new_name, v_emp_changes(i).old_salary, v_emp_changes(i).new_salary, v_emp_changes(i).change_type, SYSDATE, v_user ); END LOOP; END AFTER STATEMENT; END trg_compound_employee_audit; / 

8.3 自治事务触发器

自治事务触发器允许触发器中的操作独立于主事务提交或回滚。

示例:创建一个自治事务触发器记录错误日志

-- 创建错误日志表 CREATE TABLE error_logs ( log_id NUMBER GENERATED ALWAYS AS IDENTITY, error_date DATE, user_name VARCHAR2(30), table_name VARCHAR2(30), operation VARCHAR2(10), error_message VARCHAR2(4000) ); -- 创建自治事务触发器 CREATE OR REPLACE TRIGGER trg_employee_error_log AFTER INSERT OR UPDATE OR DELETE ON employees DECLARE PRAGMA AUTONOMOUS_TRANSACTION; -- 声明为自治事务 BEGIN -- 这里可以执行独立于主事务的操作 -- 例如,记录错误日志,即使主事务回滚,这些日志也会保留 -- 模拟错误记录 INSERT INTO error_logs ( error_date, user_name, table_name, operation, error_message ) VALUES ( SYSDATE, USER, 'EMPLOYEES', CASE WHEN INSERTING THEN 'INSERT' WHEN UPDATING THEN 'UPDATE' WHEN DELETING THEN 'DELETE' END, '操作已执行' ); -- 提交自治事务 COMMIT; EXCEPTION WHEN OTHERS THEN -- 记录触发器本身的错误 INSERT INTO error_logs ( error_date, user_name, table_name, operation, error_message ) VALUES ( SYSDATE, USER, 'EMPLOYEES', 'TRIGGER', '触发器错误: ' || SQLERRM ); COMMIT; END; / 

9. 触发器与数据库版本兼容性

不同版本的Oracle数据库对触发器的支持有所不同:

9.1 Oracle 8i及之前版本

  • 基本DML触发器支持
  • 基本DDL和系统事件触发器支持
  • INSTEAD OF触发器支持

9.2 Oracle 9i

  • 增强的触发器功能
  • 更好的错误处理机制
  • 触发器执行效率优化

9.3 Oracle 10g

  • 触发器编译优化
  • 增强的触发器管理功能
  • 更好的触发器调试支持

9.4 Oracle 11g

  • 引入复合触发器
  • 触发器结果缓存
  • 更好的触发器性能监控

9.5 Oracle 12c及更高版本

  • 增强的触发器安全功能
  • 更好的触发器与PL/SQL集成
  • 触发器执行计划优化

10. 总结

Oracle数据库触发器是强大的数据库对象,可以用于实现数据完整性、业务规则自动化、审计日志记录等功能。通过本文的详细介绍,我们了解了触发器的类型、语法、应用场景以及最佳实践。

触发器的主要优势在于它们能够自动执行,无需应用程序干预,确保数据一致性和业务规则的强制实施。然而,触发器也可能带来性能和维护方面的挑战,因此在使用触发器时应谨慎考虑。

在实际应用中,应根据具体需求选择合适的触发器类型,并遵循最佳实践,以确保触发器的高效、可靠和可维护性。同时,随着Oracle数据库版本的不断更新,触发器功能也在不断增强,开发者应关注新版本中引入的触发器特性,以便更好地利用这些功能解决实际问题。

通过合理使用触发器,我们可以构建更加健壮、安全和高效的数据库应用系统。