触发器与事件调度器

最后更新: 2026-04-15 作者: Oracle Team
页面目录

第七章:触发器与事件调度器

文档信息
  • 适用版本:Oracle 12c / 18c / 19c / 21c
  • 阅读时间:约 45 分钟
  • 前置知识:第六章视图与存储过程

7.1 触发器高级应用

7.1.1 条件谓词

-- 使用条件谓词区分触发事件
CREATE OR REPLACE TRIGGER trg_emp_dml_audit
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
DECLARE
    v_action VARCHAR2(10);
BEGIN
    IF INSERTING THEN
        v_action := 'INSERT';
        INSERT INTO audit_log (action, table_name, new_value, action_date)
        VALUES (v_action, 'EMPLOYEES', :NEW.employee_id || ':' || :NEW.first_name, SYSDATE);
        
    ELSIF UPDATING THEN
        v_action := 'UPDATE';
        INSERT INTO audit_log (action, table_name, old_value, new_value, action_date)
        VALUES (v_action, 'EMPLOYEES', :OLD.employee_id || ':' || :OLD.salary,
                :NEW.employee_id || ':' || :NEW.salary, SYSDATE);
        
    ELSIF DELETING THEN
        v_action := 'DELETE';
        INSERT INTO audit_log (action, table_name, old_value, action_date)
        VALUES (v_action, 'EMPLOYEES', :OLD.employee_id || ':' || :OLD.first_name, SYSDATE);
    END IF;
END trg_emp_dml_audit;
/

-- 条件谓词在触发器中的使用
-- INSERTING: 是否为 INSERT
-- UPDATING: 是否为 UPDATE
-- UPDATING('column_name'): 是否更新了特定列
-- DELETING: 是否为 DELETE

7.1.2 变异表问题

变异表限制

触发器不能直接读取或修改触发器所在的表

-- 变异表问题示例
-- 错误示例:触发器不能查询正在被修改的表
CREATE OR REPLACE TRIGGER trg_count_employees
AFTER INSERT ON employees
FOR EACH ROW
DECLARE
    v_count NUMBER;
BEGIN
    -- 这会触发 ORA-04091 错误
    SELECT COUNT(*) INTO v_count FROM employees;
END;
/

-- 解决方案1:使用语句级触发器 + 包变量
CREATE OR REPLACE PACKAGE emp_trigger_pkg AS
    v_new_count NUMBER := 0;
END emp_trigger_pkg;
/

CREATE OR REPLACE TRIGGER trg_count_employees_stmt
AFTER INSERT ON employees
DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*) INTO v_count FROM employees;
    DBMS_OUTPUT.PUT_LINE('Total employees after insert: ' || v_count);
END;
/

-- 解决方案2:使用自治事务
CREATE OR REPLACE TRIGGER trg_log_insert
AFTER INSERT ON employees
FOR EACH ROW
DECLARE
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    INSERT INTO emp_insert_log 
    VALUES (:NEW.employee_id, :NEW.first_name, SYSDATE);
    COMMIT;
END trg_log_insert;
/

-- 解决方案3:使用 INSTEAD OF 触发器(针对视图)
-- 或使用 AFTER 触发器而非 BEFORE

7.1.3 触发器与事务

-- 触发器与提交
-- 触发器内不能提交,但可以回滚
CREATE OR REPLACE TRIGGER trg_validate_salary
BEFORE INSERT OR UPDATE OF salary ON employees
FOR EACH ROW
DECLARE
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    IF :NEW.salary < 1000 THEN
        RAISE_APPLICATION_ERROR(-20001, 'Salary must be at least 1000');
    END IF;
    
    -- 自治事务中可以提交
    -- COMMIT;  -- 通常不推荐在触发器中提交
END;
/

-- 多级触发器链
-- 触发器A → 触发器B → 触发器C
-- 注意:避免循环触发

-- 设置触发器顺序
-- Oracle 11g+ 支持 FOLLOWS 子句
CREATE OR REPLACE TRIGGER trg_audit_new
AFTER INSERT ON employees
FOR EACH ROW
FOLLOWS trg_validate_salary  -- 在 trg_validate_salary 之后执行
DECLARE
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    INSERT INTO audit_trail VALUES ('INSERT', :NEW.employee_id, SYSDATE);
    COMMIT;
END trg_audit_new;
/

7.2 DDL 触发器

7.2.1 DDL 事件类型

事件 说明
CREATE 创建对象
ALTER 修改对象
DROP 删除对象
GRANT 授予权限
REVOKE 撤销权限
ANALYZE 分析表/索引
AUDIT 审计操作
COMMENT 添加注释

7.2.2 创建 DDL 触发器

-- 记录所有 DDL 操作
CREATE OR REPLACE TRIGGER trg_ddl_audit
AFTER DDL ON SCHEMA
DECLARE
    v_sql_text ORA_NAME_LIST_T;
    v_count NUMBER;
BEGIN
    v_count := ORA_SQL_TXT(v_sql_text);
    
    INSERT INTO ddl_audit_log (
        username, 
        ddl_date, 
        ddl_type, 
        object_type, 
        object_name,
        sql_text
    ) VALUES (
        ORA_LOGIN_USER,
        SYSDATE,
        ORA_SYSEVENT,
        ORA_DICT_OBJ_TYPE,
        ORA_DICT_OBJ_NAME,
        DBMS_LOB.SUBSTR(v_sql_text(1), 4000, 1)
    );
    
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        NULL;  -- 避免 DDL 失败
END trg_ddl_audit;
/

-- 禁止删除特定表
CREATE OR REPLACE TRIGGER trg_prevent_drop
BEFORE DROP ON SCHEMA
DECLARE
    v_obj_name VARCHAR2(100) := ORA_DICT_OBJ_NAME;
    v_obj_type VARCHAR2(50) := ORA_DICT_OBJ_TYPE;
BEGIN
    IF v_obj_name IN ('EMPLOYEES', 'DEPARTMENTS') 
       AND v_obj_type = 'TABLE' THEN
        RAISE_APPLICATION_ERROR(-20001, 
            'Cannot drop protected table: ' || v_obj_name);
    END IF;
END trg_prevent_drop;
/

-- 监控表结构变更
CREATE OR REPLACE TRIGGER trg_table_change
AFTER CREATE OR ALTER OR DROP ON SCHEMA
DECLARE
    v_event VARCHAR2(50) := ORA_SYSEVENT;
    v_obj_type VARCHAR2(50) := ORA_DICT_OBJ_TYPE;
    v_obj_name VARCHAR2(100) := ORA_DICT_OBJ_NAME;
BEGIN
    IF v_obj_type IN ('TABLE', 'INDEX', 'VIEW', 'PROCEDURE') THEN
        INSERT INTO object_change_log VALUES (
            v_event, v_obj_type, v_obj_name, USER, SYSDATE
        );
        COMMIT;
    END IF;
END trg_table_change;
/

7.2.3 数据库级 DDL 触发器

-- 需要 DBA 权限
-- 在 CDB 环境,需要在 PDB 级别创建

-- 创建登录/注销触发器
CREATE OR REPLACE TRIGGER trg_logon_audit
AFTER LOGON ON DATABASE
DECLARE
    v_user VARCHAR2(100) := USER;
BEGIN
    INSERT INTO session_log (
        username, logon_time, machine, program
    ) VALUES (
        v_user, SYSDATE, 
        SYS_CONTEXT('USERENV', 'HOST'),
        SYS_CONTEXT('USERENV', 'MODULE')
    );
    COMMIT;
END trg_logon_audit;
/

CREATE OR REPLACE TRIGGER trg_logoff_audit
BEFORE LOGOFF ON DATABASE
DECLARE
    v_user VARCHAR2(100) := USER;
BEGIN
    UPDATE session_log 
    SET logoff_time = SYSDATE
    WHERE username = v_user 
    AND logoff_time IS NULL
    AND ROWNUM = 1;
    COMMIT;
END trg_logoff_audit;
/

-- 限制特定用户登录时间
CREATE OR REPLACE TRIGGER trg_restrict_hours
AFTER LOGON ON DATABASE
DECLARE
    v_user VARCHAR2(50) := USER;
    v_hour NUMBER := TO_CHAR(SYSDATE, 'HH24');
BEGIN
    IF v_user = 'APP_USER' AND v_hour NOT BETWEEN 8 AND 18 THEN
        RAISE_APPLICATION_ERROR(-20001, 
            'Access allowed only during business hours');
    END IF;
END trg_restrict_hours;
/

7.3 事件触发器

7.3.1 事件触发器类型

事件 说明
AFTER STARTUP 数据库启动后
BEFORE SHUTDOWN 数据库关闭前
AFTER DB_ROLE_CHANGE 角色变更后
BEFORE SET ROLE 设置角色前
AFTER LOGON 用户登录后
BEFORE LOGOFF 用户登出前
AFTER SERVERERROR 服务器错误后

7.3.2 创建事件触发器

-- 数据库启动事件
CREATE OR REPLACE TRIGGER trg_after_startup
AFTER STARTUP ON DATABASE
BEGIN
    INSERT INTO startup_log VALUES (
        'Database started', SYSDATE, INSTANCE
    );
    COMMIT;
END trg_after_startup;
/

-- 数据库关闭事件
CREATE OR REPLACE TRIGGER trg_before_shutdown
BEFORE SHUTDOWN ON DATABASE
BEGIN
    INSERT INTO shutdown_log VALUES (
        'Shutdown initiated', SYSDATE
    );
    COMMIT;
END trg_before_shutdown;
/

-- 错误触发器
CREATE OR REPLACE TRIGGER trg_error_alert
AFTER SERVERERROR ON DATABASE
DECLARE
    v_msg VARCHAR2(2000);
BEGIN
    v_msg := DBMS_UTILITY.FORMAT_ERROR_STACK;
    
    IF SQLCODE = -1 THEN  -- ORA-00001: 唯一约束违反
        INSERT INTO error_alert_log VALUES (
            'DUP_VAL_ON_INDEX', v_msg, SYSDATE
        );
    ELSIF SQLCODE = -54 THEN  -- ORA-00054: 资源忙
        INSERT INTO error_alert_log VALUES (
            'RESOURCE_BUSY', v_msg, SYSDATE
        );
    END IF;
    
    COMMIT;
END trg_error_alert;
/

7.4 DBMS_SCHEDULER 调度器

7.4.1 调度器组件

┌─────────────────────────────────────────────────────────────────┐
│                  DBMS_SCHEDULER 架构                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  调度器对象                                                       │
│  ───────────                                                     │
│  • Programs (程序)     - 定义要执行的操作                        │
│  • Jobs (作业)         - 安排程序执行                            │
│  • Schedules (计划)    - 定义执行时间                           │
│  • Job Classes (作业类) - 分组和管理资源                         │
│  • Windows (窗口)      - 时间窗口内启用资源                      │
│                                                                 │
│  作业类型                                                         │
│  ───────                                                         │
│  • PL/SQL 块                                                      │
│  • 存储过程                                                      │
│  • 外部程序 (C, Java, OS command)                                │
│                                                                 │
│  调度类型                                                         │
│  ───────                                                         │
│  • 基于时间 - 定期执行                                           │
│  • 基于事件 - 响应事件执行                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.4.2 创建程序对象

-- 创建 PL/SQL 程序
BEGIN
    DBMS_SCHEDULER.CREATE_PROGRAM(
        program_name   => 'PROG_ARCHIVE_OLD_DATA',
        program_type   => 'PLSQL_BLOCK',
        program_action => '
            BEGIN
                DELETE FROM sales 
                WHERE sale_date < ADD_MONTHS(SYSDATE, -24);
                COMMIT;
            END;',
        number_of_arguments => 0,
        enabled             => TRUE,
        comments            => 'Archive sales data older than 24 months'
    );
END;
/

-- 创建存储过程程序
BEGIN
    DBMS_SCHEDULER.CREATE_PROGRAM(
        program_name   => 'PROG_BACKUP_TABLE',
        program_type   => 'STORED_PROCEDURE',
        program_action => 'PKG_MAINTENANCE.BACKUP_TABLE',
        number_of_arguments => 1,
        enabled             => FALSE,  -- 需要先定义参数
        comments            => 'Backup specified table'
    );
END;
/

-- 定义程序参数
BEGIN
    DBMS_SCHEDULER.DEFINE_PROGRAM_ARGUMENT(
        program_name      => 'PROG_BACKUP_TABLE',
        argument_position => 1,
        argument_name     => 'P_TABLE_NAME',
        argument_type     => 'VARCHAR2',
        default_value     => NULL
    );
END;
/

-- 启用程序
BEGIN
    DBMS_SCHEDULER.ENABLE('PROG_ARCHIVE_OLD_DATA');
END;
/

7.4.3 创建计划对象

-- 一次性计划
BEGIN
    DBMS_SCHEDULER.CREATE_SCHEDULE(
        schedule_name => 'SCH_ONE_TIME',
        start_date    => TO_TIMESTAMP_TZ('2026-05-01 02:00:00 America/New_York', 
                                          'YYYY-MM-DD HH24:MI:SS TZR'),
        end_date      => NULL,
        repeat_interval => NULL,
        comments      => 'One-time maintenance job'
    );
END;
/

-- 重复执行计划 (日历表达式)
BEGIN
    -- 每周一凌晨 2:00 执行
    DBMS_SCHEDULER.CREATE_SCHEDULE(
        schedule_name   => 'SCH_WEEKLY_MAINT',
        start_date      => SYSTIMESTAMP,
        repeat_interval => 'FREQ=WEEKLY;BYDAY=MON;BYHOUR=2;BYMINUTE=0;BYSECOND=0',
        comments        => 'Weekly maintenance on Monday at 2 AM'
    );
END;
/

-- 每日执行
BEGIN
    DBMS_SCHEDULER.CREATE_SCHEDULE(
        schedule_name   => 'SCH_DAILY_NIGHT',
        start_date      => SYSTIMESTAMP,
        repeat_interval => 'FREQ=DAILY;BYHOUR=2;BYMINUTE=0',
        comments        => 'Daily at 2 AM'
    );
END;
/

-- 每月最后一天执行
BEGIN
    DBMS_SCHEDULER.CREATE_SCHEDULE(
        schedule_name   => 'SCH_MONTHLY_END',
        start_date      => SYSTIMESTAMP,
        repeat_interval => 'FREQ=MONTHLY;BYMONTHDAY=-1;BYHOUR=3;BYMINUTE=0',
        comments        => 'Last day of each month at 3 AM'
    );
END;
/

-- 使用 PL/SQL 表达式定义间隔
BEGIN
    DBMS_SCHEDULER.CREATE_SCHEDULE(
        schedule_name   => 'SCH_EVERY_6_HOURS',
        start_date      => SYSTIMESTAMP,
        repeat_interval => '
            DECLARE
                next_time TIMESTAMP;
            BEGIN
                next_time := SYSTIMESTAMP + INTERVAL ''6'' HOUR;
                RETURN TO_CHAR(next_time, ''YYYYMMDD HH24MI'');
            END;',
        comments        => 'Every 6 hours'
    );
END;
/

7.4.4 创建作业

-- 基础作业
BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name        => 'JOB_ARCHIVE_DATA',
        program_name    => 'PROG_ARCHIVE_OLD_DATA',
        schedule_name   => 'SCH_WEEKLY_MAINT',
        job_class       => 'DEFAULT_JOB_CLASS',
        enabled         => TRUE,
        auto_drop       => FALSE,      -- 不要自动删除
        comments        => 'Weekly archive job'
    );
END;
/

-- 作业与内联计划组合
BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name        => 'JOB_REFRESH_REPORT',
        job_type        => 'PLSQL_BLOCK',
        job_action      => 'BEGIN P_REFRESH_MONTHLY_REPORT; END;',
        start_date      => SYSTIMESTAMP,
        repeat_interval => 'FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=1',
        enabled         => TRUE,
        comments        => 'Refresh monthly report on 1st of each month'
    );
END;
/

-- 带参数的作业
BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name        => 'JOB_BACKUP_EMP',
        program_name    => 'PROG_BACKUP_TABLE',
        schedule_name   => 'SCH_DAILY_NIGHT',
        job_class       => 'DEFAULT_JOB_CLASS',
        enabled         => TRUE,
        comments        => 'Daily backup of employees table'
    );
END;
/

-- 设置作业参数
BEGIN
    DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE(
        job_name            => 'JOB_BACKUP_EMP',
        argument_position   => 1,
        argument_value      => 'EMPLOYEES'
    );
END;
/

-- 立即执行作业
BEGIN
    DBMS_SCHEDULER.RUN_JOB(
        job_name  => 'JOB_ARCHIVE_DATA',
        use_current_session => FALSE  -- 在新会话中运行
    );
END;
/

-- 停止作业
BEGIN
    DBMS_SCHEDULER.STOP_JOB(
        job_name          => 'JOB_ARCHIVE_DATA',
        force             => TRUE
    );
END;
/

7.4.5 作业管理

-- 查看作业状态
SELECT 
    job_name,
    job_type,
    status,
    enabled,
    run_count,
    failure_count,
    last_start_date,
    last_run_duration,
    next_run_date
FROM user_scheduler_jobs;

-- 查看作业运行历史
SELECT 
    job_name,
    log_id,
    operation,
    status,
    TO_CHAR(log_date, 'YYYY-MM-DD HH24:MI:SS') AS log_date,
    additional_info
FROM user_scheduler_job_run_details
ORDER BY log_date DESC;

-- 启用/禁用作业
BEGIN
    DBMS_SCHEDULER.ENABLE('JOB_ARCHIVE_DATA');
    -- 或
    DBMS_SCHEDULER.DISABLE('JOB_ARCHIVE_DATA');
END;
/

-- 删除作业
BEGIN
    DBMS_SCHEDULER.DROP_JOB(
        job_name       => 'JOB_ARCHIVE_DATA',
        force          => TRUE
    );
END;
/

-- 修改作业
BEGIN
    DBMS_SCHEDULER.SET_ATTRIBUTE(
        name      => 'JOB_ARCHIVE_DATA',
        attribute => 'repeat_interval',
        value     => 'FREQ=DAILY;BYHOUR=3'
    );
END;
/

7.4.6 作业类与资源管理

-- 创建作业类
BEGIN
    DBMS_SCHEDULER.CREATE_JOB_CLASS(
        job_class_name  => 'MAINTENANCE_JOBS',
        resource_provider_plan => 'MAINTENANCE_PLAN',
        service         => NULL,
        logging_level   => DBMS_SCHEDULER.LOGGING_RUNS,
        log_history     => 30,  -- 保留 30 天日志
        comments        => 'Maintenance job class'
    );
END;
/

-- 将作业分配到作业类
BEGIN
    DBMS_SCHEDULER.SET_ATTRIBUTE(
        name      => 'JOB_ARCHIVE_DATA',
        attribute => 'job_class',
        value     => 'MAINTENANCE_JOBS'
    );
END;
/

-- 创建时间窗口(配合资源计划)
BEGIN
    DBMS_SCHEDULER.CREATE_WINDOW(
        window_name     => 'WEEKEND_WINDOW',
        resource_plan   => 'MAINTENANCE_PLAN',
        start_date      => TO_TIMESTAMP_TZ('2026-01-01 00:00:00 America/New_York',
                                           'YYYY-MM-DD HH24:MI:SS TZR'),
        repeat_interval => 'FREQ=WEEKLY;BYDAY=SAT,SUN;BYHOUR=0;BYMINUTE=0;BYSECOND=0',
        duration       => INTERVAL '12' HOUR,
        window_priority => 'LOW',
        comments        => 'Weekend maintenance window'
    );
END;
/

-- 创建窗口组
BEGIN
    DBMS_SCHEDULER.CREATE_WINDOW_GROUP(
        group_name => 'OFF_HOURS_WINDOW',
        window_list => ARRAY('WEEKDAY_WINDOW', 'WEEKEND_WINDOW')
    );
END;
/

7.4.7 事件驱动作业

-- 创建事件生产者(表)
CREATE TABLE job_event_queue (
    event_id NUMBER PRIMARY KEY,
    event_type VARCHAR2(50),
    target_table VARCHAR2(100),
    event_time TIMESTAMP DEFAULT SYSTIMESTAMP
);

-- 创建队列
BEGIN
    DBMS_AQADM.CREATE_QUEUE(
        queue_name  => 'JOB_EVENT_QUEUE',
        queue_table => 'JOB_EVENT_QTABLE',
        queue_type  => DBMS_AQADM.NORMAL_QUEUE
    );
END;
/

-- 创建事件作业
BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name        => 'JOB_EVENT_HANDLER',
        job_type        => 'PLSQL_BLOCK',
        job_action      => 'BEGIN P_HANDLE_JOB_EVENT; END;',
        event_condition => 'TABLENAME = ''JOB_EVENT_QUEUE'' AND EVENT_TYPE = ''INSERT''',
        queue_spec      => 'JOB_EVENT_QUEUE',
        enabled         => TRUE,
        comments        => 'Event-driven job'
    );
END;
/

-- 基于错误事件的作业
BEGIN
    DBMS_SCHEDULER.CREATE_JOB(
        job_name          => 'JOB_ERROR_HANDLER',
        job_type          => 'PLSQL_BLOCK',
        job_action        => 'BEGIN P_HANDLE_ERROR; END;',
        start_date        => SYSTIMESTAMP,
        event_condition   => 'EVENT_TYPE = ''SERVERERROR'' AND ERROR_NUMBER = 604',
        queue_spec        => 'SCHEDULER$_EVENT_QUEUE',
        enabled           => TRUE
    );
END;
/

7.5 DBMS_JOB (旧版对比)

-- DBMS_JOB 是旧版调度器,仍可用但推荐使用 DBMS_SCHEDULER

-- 提交作业
DECLARE
    v_job_id NUMBER;
BEGIN
    DBMS_JOB.SUBMIT(
        job       => v_job_id,
        what      => 'P_GENERATE_REPORT;',
        next_date => TRUNC(SYSDATE + 1) + 1/24,  -- 明天凌晨 1 点
        interval  => 'TRUNC(SYSDATE + 1) + 1/24'  -- 每天执行
    );
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('Job ID: ' || v_job_id);
END;
/

-- 管理作业
BEGIN
    DBMS_JOB.BROKEN(v_job_id, TRUE);   -- 标记为损坏
    DBMS_JOB.RUN(v_job_id);             -- 立即运行
    DBMS_JOB.REMOVE(v_job_id);          -- 删除作业
END;
/

-- DBMS_JOB vs DBMS_SCHEDULER 对比
-- ┌──────────────────────────────────────────────────────────────┐
-- │ 特性             │ DBMS_JOB        │ DBMS_SCHEDULER           │
-- ├──────────────────────────────────────────────────────────────┤
-- │ 资源管理         │ 无              │ 支持作业类、资源计划      │
-- │ 事件驱动         │ 无              │ 支持事件触发              │
-- │ 外部程序         │ 无              │ 支持 OS 命令              │
-- │ 依赖关系         │ 无              │ 支持作业链                │
-- │ 日志保留         │ 有限            │ 可配置                     │
-- │ 推荐程度         │ 不推荐          │ 推荐                       │
-- └──────────────────────────────────────────────────────────────┘

7.6 本章小结

✅ DDL 触发器:审计和防止危险 DDL 操作
✅ 事件触发器:LOGON/LOGOFF/SHUTDOWN 等事件
✅ 触发器高级特性:条件谓词、FOLLOWS、变异表处理
✅ DBMS_SCHEDULER:现代调度框架
✅ 调度组件:Programs、Schedules、Jobs、Job Classes、Windows
✅ 事件驱动作业:响应队列事件执行

📖 下章预告:事务与并发控制

下一步:学习 第八章:事务与并发控制