【Oracle】触发器

news2025/6/7 9:07:09

在这里插入图片描述

个人主页:Guiat
归属专栏:Oracle

在这里插入图片描述

文章目录

  • 1. 触发器基础概述
    • 1.1 触发器的概念与特点
    • 1.2 触发器的分类
    • 1.3 触发器的执行顺序
  • 2. DML触发器
    • 2.1 基础DML触发器
      • 2.1.1 INSERT触发器
      • 2.1.2 UPDATE触发器
      • 2.1.3 DELETE触发器
    • 2.2 高级DML触发器
      • 2.2.1 复合触发器
      • 2.2.2 条件触发器
  • 3. INSTEAD OF 触发器
    • 3.1 视图上的INSTEAD OF触发器
      • 3.1.1 复杂视图的DML操作
  • 4. DDL触发器
    • 4.1 系统级DDL触发器
      • 4.1.1 数据库对象变更监控
      • 4.1.2 防止意外删除的保护触发器
  • 5. 系统事件触发器
    • 5.1 登录和注销触发器
      • 5.1.1 用户会话监控

正文

1. 触发器基础概述

触发器是Oracle数据库中的一种特殊存储过程,当特定的数据库事件发生时自动执行。它是实现业务规则、数据完整性约束和审计功能的重要工具。

1.1 触发器的概念与特点

Oracle触发器
自动执行
事件驱动
透明性
不可直接调用
无需手动调用
响应特定事件
自动激活
DML操作触发
DDL操作触发
系统事件触发
对应用透明
自动数据处理
隐式执行
不能直接CALL
通过事件激活
系统自动管理

1.2 触发器的分类

Oracle触发器分类
按触发事件分类
按触发时机分类
按触发级别分类
按触发次数分类
DML触发器
DDL触发器
系统事件触发器
用户事件触发器
BEFORE触发器
AFTER触发器
INSTEAD OF触发器
行级触发器
语句级触发器
FOR EACH ROW
FOR EACH STATEMENT

1.3 触发器的执行顺序

DML语句执行
BEFORE语句级触发器
BEFORE行级触发器
实际DML操作
AFTER行级触发器
AFTER语句级触发器
提交/回滚

2. DML触发器

2.1 基础DML触发器

2.1.1 INSERT触发器

-- 创建员工表和相关表用于演示
CREATE TABLE employees_demo AS SELECT * FROM employees WHERE 1=0;
ALTER TABLE employees_demo ADD CONSTRAINT pk_emp_demo PRIMARY KEY (employee_id);

-- 创建员工审计表
CREATE TABLE employee_audit (
    audit_id NUMBER PRIMARY KEY,
    employee_id NUMBER,
    operation_type VARCHAR2(10),
    old_values VARCHAR2(4000),
    new_values VARCHAR2(4000),
    changed_by VARCHAR2(30),
    change_date DATE,
    session_id NUMBER
);

CREATE SEQUENCE employee_audit_seq START WITH 1 INCREMENT BY 1;

-- 创建INSERT触发器
CREATE OR REPLACE TRIGGER trg_employee_insert_audit
    AFTER INSERT ON employees_demo
    FOR EACH ROW
BEGIN
    -- 记录新员工插入的审计信息
    INSERT INTO employee_audit (
        audit_id,
        employee_id,
        operation_type,
        new_values,
        changed_by,
        change_date,
        session_id
    ) VALUES (
        employee_audit_seq.NEXTVAL,
        :NEW.employee_id,
        'INSERT',
        'ID: ' || :NEW.employee_id || 
        ', Name: ' || :NEW.first_name || ' ' || :NEW.last_name ||
        ', Email: ' || :NEW.email ||
        ', Salary: ' || :NEW.salary ||
        ', Hire Date: ' || TO_CHAR(:NEW.hire_date, 'YYYY-MM-DD'),
        USER,
        SYSDATE,
        SYS_CONTEXT('USERENV', 'SESSIONID')
    );
    
    -- 输出调试信息
    DBMS_OUTPUT.PUT_LINE('触发器执行: 新员工 ' || :NEW.first_name || ' ' || :NEW.last_name || ' 已插入');
END;
/

-- 测试INSERT触发器
SET SERVEROUTPUT ON

INSERT INTO employees_demo (
    employee_id, first_name, last_name, email, hire_date, job_id, salary
) VALUES (
    1001, 'John', 'Doe', 'john.doe@company.com', SYSDATE, 'IT_PROG', 6000
);

INSERT INTO employees_demo (
    employee_id, first_name, last_name, email, hire_date, job_id, salary
) VALUES (
    1002, 'Jane', 'Smith', 'jane.smith@company.com', SYSDATE, 'SA_REP', 5500
);

COMMIT;

-- 查看审计记录
SELECT * FROM employee_audit ORDER BY change_date DESC;

2.1.2 UPDATE触发器

-- 创建UPDATE触发器
CREATE OR REPLACE TRIGGER trg_employee_update_audit
    AFTER UPDATE ON employees_demo
    FOR EACH ROW
DECLARE
    v_changes VARCHAR2(4000);
BEGIN
    -- 构建变更信息
    v_changes := '';
    
    IF :OLD.first_name != :NEW.first_name THEN
        v_changes := v_changes || 'First Name: ' || :OLD.first_name || ' -> ' || :NEW.first_name || '; ';
    END IF;
    
    IF :OLD.last_name != :NEW.last_name THEN
        v_changes := v_changes || 'Last Name: ' || :OLD.last_name || ' -> ' || :NEW.last_name || '; ';
    END IF;
    
    IF :OLD.email != :NEW.email THEN
        v_changes := v_changes || 'Email: ' || :OLD.email || ' -> ' || :NEW.email || '; ';
    END IF;
    
    IF :OLD.salary != :NEW.salary THEN
        v_changes := v_changes || 'Salary: ' || :OLD.salary || ' -> ' || :NEW.salary || '; ';
    END IF;
    
    IF :OLD.job_id != :NEW.job_id OR (:OLD.job_id IS NULL AND :NEW.job_id IS NOT NULL) 
       OR (:OLD.job_id IS NOT NULL AND :NEW.job_id IS NULL) THEN
        v_changes := v_changes || 'Job: ' || NVL(:OLD.job_id, 'NULL') || ' -> ' || NVL(:NEW.job_id, 'NULL') || '; ';
    END IF;
    
    -- 只有当有实际变更时才记录
    IF LENGTH(v_changes) > 0 THEN
        INSERT INTO employee_audit (
            audit_id,
            employee_id,
            operation_type,
            old_values,
            new_values,
            changed_by,
            change_date,
            session_id
        ) VALUES (
            employee_audit_seq.NEXTVAL,
            :NEW.employee_id,
            'UPDATE',
            'Original: ID=' || :OLD.employee_id || ', Name=' || :OLD.first_name || ' ' || :OLD.last_name || 
            ', Email=' || :OLD.email || ', Salary=' || :OLD.salary,
            'Updated: ' || RTRIM(v_changes, '; '),
            USER,
            SYSDATE,
            SYS_CONTEXT('USERENV', 'SESSIONID')
        );
        
        DBMS_OUTPUT.PUT_LINE('触发器执行: 员工 ' || :NEW.employee_id || ' 信息已更新');
        DBMS_OUTPUT.PUT_LINE('变更内容: ' || RTRIM(v_changes, '; '));
    END IF;
END;
/

-- 测试UPDATE触发器
UPDATE employees_demo 
SET salary = 6500, email = 'john.doe.new@company.com'
WHERE employee_id = 1001;

UPDATE employees_demo 
SET first_name = 'Janet'
WHERE employee_id = 1002;

COMMIT;

-- 查看更新审计记录
SELECT * FROM employee_audit WHERE operation_type = 'UPDATE' ORDER BY change_date DESC;

2.1.3 DELETE触发器

-- 创建DELETE触发器
CREATE OR REPLACE TRIGGER trg_employee_delete_audit
    BEFORE DELETE ON employees_demo
    FOR EACH ROW
BEGIN
    -- 记录删除前的员工信息
    INSERT INTO employee_audit (
        audit_id,
        employee_id,
        operation_type,
        old_values,
        changed_by,
        change_date,
        session_id
    ) VALUES (
        employee_audit_seq.NEXTVAL,
        :OLD.employee_id,
        'DELETE',
        'Deleted: ID=' || :OLD.employee_id || 
        ', Name=' || :OLD.first_name || ' ' || :OLD.last_name ||
        ', Email=' || :OLD.email ||
        ', Salary=' || :OLD.salary ||
        ', Job=' || :OLD.job_id,
        USER,
        SYSDATE,
        SYS_CONTEXT('USERENV', 'SESSIONID')
    );
    
    DBMS_OUTPUT.PUT_LINE('触发器执行: 员工 ' || :OLD.first_name || ' ' || :OLD.last_name || ' 即将被删除');
END;
/

-- 测试DELETE触发器
DELETE FROM employees_demo WHERE employee_id = 1002;
COMMIT;

-- 查看删除审计记录
SELECT * FROM employee_audit WHERE operation_type = 'DELETE' ORDER BY change_date DESC;

2.2 高级DML触发器

2.2.1 复合触发器

-- 创建复合触发器,处理工资历史记录
CREATE TABLE salary_history (
    history_id NUMBER PRIMARY KEY,
    employee_id NUMBER,
    old_salary NUMBER,
    new_salary NUMBER,
    change_date DATE,
    change_reason VARCHAR2(200),
    effective_date DATE
);

CREATE SEQUENCE salary_history_seq START WITH 1 INCREMENT BY 1;

-- 复合触发器
CREATE OR REPLACE TRIGGER trg_salary_management
    FOR UPDATE OF salary ON employees_demo
    COMPOUND TRIGGER
    
    -- 声明部分:定义包级变量
    TYPE emp_salary_rec IS RECORD (
        employee_id NUMBER,
        old_salary NUMBER,
        new_salary NUMBER
    );
    
    TYPE emp_salary_tab IS TABLE OF emp_salary_rec INDEX BY PLS_INTEGER;
    g_salary_changes emp_salary_tab;
    g_change_count PLS_INTEGER := 0;
    
    -- BEFORE STATEMENT: 语句执行前
    BEFORE STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('=== 工资更新开始 ===');
        DBMS_OUTPUT.PUT_LINE('时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
        g_change_count := 0;
        g_salary_changes.DELETE; -- 清空集合
    END BEFORE STATEMENT;
    
    -- BEFORE EACH ROW: 每行处理前
    BEFORE EACH ROW IS
    BEGIN
        -- 验证工资变更规则
        IF :NEW.salary <= 0 THEN
            RAISE_APPLICATION_ERROR(-20001, '工资必须大于0');
        END IF;
        
        IF :NEW.salary > :OLD.salary * 2 THEN
            RAISE_APPLICATION_ERROR(-20002, '工资增长不能超过100%');
        END IF;
        
        IF :NEW.salary < :OLD.salary * 0.5 THEN
            RAISE_APPLICATION_ERROR(-20003, '工资减少不能超过50%');
        END IF;
        
        DBMS_OUTPUT.PUT_LINE('验证通过: 员工' || :NEW.employee_id || 
                           ' 工资从 $' || :OLD.salary || ' 变更为 $' || :NEW.salary);
    END BEFORE EACH ROW;
    
    -- AFTER EACH ROW: 每行处理后
    AFTER EACH ROW IS
    BEGIN
        -- 收集变更信息
        g_change_count := g_change_count + 1;
        g_salary_changes(g_change_count).employee_id := :NEW.employee_id;
        g_salary_changes(g_change_count).old_salary := :OLD.salary;
        g_salary_changes(g_change_count).new_salary := :NEW.salary;
        
        DBMS_OUTPUT.PUT_LINE('记录变更: 员工' || :NEW.employee_id || ' 工资变更已收集');
    END AFTER EACH ROW;
    
    -- AFTER STATEMENT: 语句执行后
    AFTER STATEMENT IS
        v_total_old_salary NUMBER := 0;
        v_total_new_salary NUMBER := 0;
        v_avg_increase_pct NUMBER;
    BEGIN
        -- 批量插入工资历史记录
        FOR i IN 1..g_change_count LOOP
            INSERT INTO salary_history (
                history_id,
                employee_id,
                old_salary,
                new_salary,
                change_date,
                change_reason,
                effective_date
            ) VALUES (
                salary_history_seq.NEXTVAL,
                g_salary_changes(i).employee_id,
                g_salary_changes(i).old_salary,
                g_salary_changes(i).new_salary,
                SYSDATE,
                '系统更新',
                SYSDATE
            );
            
            v_total_old_salary := v_total_old_salary + g_salary_changes(i).old_salary;
            v_total_new_salary := v_total_new_salary + g_salary_changes(i).new_salary;
        END LOOP;
        
        -- 计算统计信息
        IF v_total_old_salary > 0 THEN
            v_avg_increase_pct := ROUND((v_total_new_salary - v_total_old_salary) / v_total_old_salary * 100, 2);
        END IF;
        
        DBMS_OUTPUT.PUT_LINE('=== 工资更新完成 ===');
        DBMS_OUTPUT.PUT_LINE('更新员工数: ' || g_change_count);
        DBMS_OUTPUT.PUT_LINE('总原工资: $' || v_total_old_salary);
        DBMS_OUTPUT.PUT_LINE('总新工资: $' || v_total_new_salary);
        DBMS_OUTPUT.PUT_LINE('平均增长: ' || NVL(v_avg_increase_pct, 0) || '%');
        
    END AFTER STATEMENT;
    
END trg_salary_management;
/

-- 测试复合触发器
-- 插入测试数据
INSERT INTO employees_demo (employee_id, first_name, last_name, email, hire_date, job_id, salary)
VALUES (1003, 'Bob', 'Johnson', 'bob.johnson@company.com', SYSDATE, 'IT_PROG', 5000);

INSERT INTO employees_demo (employee_id, first_name, last_name, email, hire_date, job_id, salary)
VALUES (1004, 'Alice', 'Brown', 'alice.brown@company.com', SYSDATE, 'SA_REP', 4500);

COMMIT;

-- 测试批量工资更新
UPDATE employees_demo 
SET salary = salary * 1.1 
WHERE employee_id IN (1001, 1003, 1004);

COMMIT;

-- 查看工资历史记录
SELECT * FROM salary_history ORDER BY change_date DESC;

2.2.2 条件触发器

-- 创建条件触发器:只在特定条件下触发
CREATE OR REPLACE TRIGGER trg_high_salary_alert
    AFTER UPDATE OF salary ON employees_demo
    FOR EACH ROW
    WHEN (NEW.salary > 10000) -- 条件:新工资超过10000
DECLARE
    v_manager_email VARCHAR2(100);
    v_dept_name VARCHAR2(50);
BEGIN
    -- 获取部门经理信息(模拟)
    DBMS_OUTPUT.PUT_LINE('=== 高工资预警 ===');
    DBMS_OUTPUT.PUT_LINE('员工: ' || :NEW.first_name || ' ' || :NEW.last_name);
    DBMS_OUTPUT.PUT_LINE('工资: $' || :OLD.salary || ' -> $' || :NEW.salary);
    DBMS_OUTPUT.PUT_LINE('增长: $' || (:NEW.salary - :OLD.salary));
    DBMS_OUTPUT.PUT_LINE('触发高工资预警,需要管理层审批');
    
    -- 记录到预警表
    INSERT INTO salary_alerts (
        alert_id,
        employee_id,
        old_salary,
        new_salary,
        alert_date,
        alert_type,
        status
    ) VALUES (
        salary_alert_seq.NEXTVAL,
        :NEW.employee_id,
        :OLD.salary,
        :NEW.salary,
        SYSDATE,
        'HIGH_SALARY',
        'PENDING'
    );
    
EXCEPTION
    WHEN OTHERS THEN
        -- 创建预警表(如果不存在)
        EXECUTE IMMEDIATE '
            CREATE TABLE salary_alerts (
                alert_id NUMBER PRIMARY KEY,
                employee_id NUMBER,
                old_salary NUMBER,
                new_salary NUMBER,
                alert_date DATE,
                alert_type VARCHAR2(20),
                status VARCHAR2(20)
            )';
        
        EXECUTE IMMEDIATE 'CREATE SEQUENCE salary_alert_seq START WITH 1 INCREMENT BY 1';
        
        -- 重新插入记录
        INSERT INTO salary_alerts VALUES (
            1, :NEW.employee_id, :OLD.salary, :NEW.salary, SYSDATE, 'HIGH_SALARY', 'PENDING'
        );
END;
/

-- 测试条件触发器
UPDATE employees_demo SET salary = 12000 WHERE employee_id = 1001; -- 触发
UPDATE employees_demo SET salary = 8000 WHERE employee_id = 1003;  -- 不触发

COMMIT;

3. INSTEAD OF 触发器

3.1 视图上的INSTEAD OF触发器

3.1.1 复杂视图的DML操作

-- 创建复杂视图
CREATE OR REPLACE VIEW employee_dept_view AS
SELECT e.employee_id,
       e.first_name,
       e.last_name,
       e.email,
       e.salary,
       e.hire_date,
       d.department_id,
       d.department_name,
       d.location_id,
       l.city,
       l.country_id
FROM employees_demo e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id;

-- 创建INSTEAD OF INSERT触发器
CREATE OR REPLACE TRIGGER trg_employee_dept_insert
    INSTEAD OF INSERT ON employee_dept_view
    FOR EACH ROW
DECLARE
    v_dept_exists NUMBER;
    v_location_exists NUMBER;
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== INSTEAD OF INSERT 触发器执行 ===');
    
    -- 检查部门是否存在
    SELECT COUNT(*) INTO v_dept_exists
    FROM departments
    WHERE department_id = :NEW.department_id;
    
    IF v_dept_exists = 0 THEN
        -- 如果部门不存在,先创建部门
        DBMS_OUTPUT.PUT_LINE('部门不存在,创建新部门: ' || :NEW.department_name);
        
        -- 检查位置是否存在
        SELECT COUNT(*) INTO v_location_exists
        FROM locations
        WHERE location_id = :NEW.location_id;
        
        IF v_location_exists = 0 THEN
            -- 创建位置(简化处理)
            INSERT INTO locations (location_id, city, country_id)
            VALUES (:NEW.location_id, :NEW.city, :NEW.country_id);
            DBMS_OUTPUT.PUT_LINE('创建新位置: ' || :NEW.city);
        END IF;
        
        -- 创建部门
        INSERT INTO departments (department_id, department_name, location_id)
        VALUES (:NEW.department_id, :NEW.department_name, :NEW.location_id);
    END IF;
    
    -- 插入员工记录
    INSERT INTO employees_demo (
        employee_id, first_name, last_name, email, 
        salary, hire_date, department_id
    ) VALUES (
        :NEW.employee_id, :NEW.first_name, :NEW.last_name, :NEW.email,
        :NEW.salary, :NEW.hire_date, :NEW.department_id
    );
    
    DBMS_OUTPUT.PUT_LINE('员工插入成功: ' || :NEW.first_name || ' ' || :NEW.last_name);
    
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('插入失败: ' || SQLERRM);
        RAISE;
END;
/

-- 创建INSTEAD OF UPDATE触发器
CREATE OR REPLACE TRIGGER trg_employee_dept_update
    INSTEAD OF UPDATE ON employee_dept_view
    FOR EACH ROW
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== INSTEAD OF UPDATE 触发器执行 ===');
    
    -- 更新员工信息
    UPDATE employees_demo
    SET first_name = :NEW.first_name,
        last_name = :NEW.last_name,
        email = :NEW.email,
        salary = :NEW.salary,
        department_id = :NEW.department_id
    WHERE employee_id = :OLD.employee_id;
    
    DBMS_OUTPUT.PUT_LINE('员工更新: ' || SQL%ROWCOUNT || ' 行');
    
    -- 如果部门信息有变化,更新部门表
    IF :OLD.department_name != :NEW.department_name OR
       :OLD.location_id != :NEW.location_id THEN
        
        UPDATE departments
        SET department_name = :NEW.department_name,
            location_id = :NEW.location_id
        WHERE department_id = :NEW.department_id;
        
        DBMS_OUTPUT.PUT_LINE('部门更新: ' || SQL%ROWCOUNT || ' 行');
    END IF;
    
    -- 如果位置信息有变化,更新位置表
    IF :OLD.city != :NEW.city OR :OLD.country_id != :NEW.country_id THEN
        UPDATE locations
        SET city = :NEW.city,
            country_id = :NEW.country_id
        WHERE location_id = :NEW.location_id;
        
        DBMS_OUTPUT.PUT_LINE('位置更新: ' || SQL%ROWCOUNT || ' 行');
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('更新失败: ' || SQLERRM);
        RAISE;
END;
/

-- 创建INSTEAD OF DELETE触发器
CREATE OR REPLACE TRIGGER trg_employee_dept_delete
    INSTEAD OF DELETE ON employee_dept_view
    FOR EACH ROW
DECLARE
    v_emp_count NUMBER;
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== INSTEAD OF DELETE 触发器执行 ===');
    
    -- 删除员工
    DELETE FROM employees_demo
    WHERE employee_id = :OLD.employee_id;
    
    DBMS_OUTPUT.PUT_LINE('员工删除: ' || :OLD.first_name || ' ' || :OLD.last_name);
    
    -- 检查部门是否还有员工
    SELECT COUNT(*) INTO v_emp_count
    FROM employees_demo
    WHERE department_id = :OLD.department_id;
    
    IF v_emp_count = 0 THEN
        DBMS_OUTPUT.PUT_LINE('部门 ' || :OLD.department_name || ' 已无员工,考虑删除部门');
        -- 可以选择删除空部门或保留
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('删除失败: ' || SQLERRM);
        RAISE;
END;
/

-- 测试INSTEAD OF触发器
-- 通过视图插入数据
INSERT INTO employee_dept_view (
    employee_id, first_name, last_name, email, salary, hire_date,
    department_id, department_name, location_id, city, country_id
) VALUES (
    1005, 'Mike', 'Wilson', 'mike.wilson@company.com', 7000, SYSDATE,
    999, 'Research Lab', 9999, 'Innovation City', 'US'
);

-- 通过视图更新数据
UPDATE employee_dept_view 
SET salary = 7500, department_name = 'Advanced Research Lab'
WHERE employee_id = 1005;

-- 查看结果
SELECT * FROM employee_dept_view WHERE employee_id = 1005;

COMMIT;

4. DDL触发器

4.1 系统级DDL触发器

4.1.1 数据库对象变更监控

-- 创建DDL审计表
CREATE TABLE ddl_audit_log (
    audit_id NUMBER PRIMARY KEY,
    username VARCHAR2(30),
    object_type VARCHAR2(30),
    object_name VARCHAR2(128),
    object_owner VARCHAR2(30),
    ddl_operation VARCHAR2(30),
    ddl_text CLOB,
    client_info VARCHAR2(64),
    host_name VARCHAR2(64),
    ip_address VARCHAR2(15),
    audit_timestamp DATE
);

CREATE SEQUENCE ddl_audit_seq START WITH 1 INCREMENT BY 1;

-- 创建DDL触发器
CREATE OR REPLACE TRIGGER trg_ddl_audit
    AFTER DDL ON SCHEMA -- 在当前SCHEMA上的DDL操作后触发
DECLARE
    v_ddl_text CLOB;
    v_client_info VARCHAR2(64);
    v_host_name VARCHAR2(64);
    v_ip_address VARCHAR2(15);
BEGIN
    -- 获取DDL语句文本
    SELECT xmlserialize(content xmlquery('//text()' passing xmltype(ora_sql_txt(1)) returning content)) 
    INTO v_ddl_text FROM dual;
    
    -- 获取客户端信息
    v_client_info := SYS_CONTEXT('USERENV', 'CLIENT_INFO');
    v_host_name := SYS_CONTEXT('USERENV', 'HOST');
    v_ip_address := SYS_CONTEXT('USERENV', 'IP_ADDRESS');
    
    -- 记录DDL操作
    INSERT INTO ddl_audit_log (
        audit_id,
        username,
        object_type,
        object_name,
        object_owner,
        ddl_operation,
        ddl_text,
        client_info,
        host_name,
        ip_address,
        audit_timestamp
    ) VALUES (
        ddl_audit_seq.NEXTVAL,
        ora_login_user,
        ora_dict_obj_type,
        ora_dict_obj_name,
        ora_dict_obj_owner,
        ora_sysevent,
        v_ddl_text,
        v_client_info,
        v_host_name,
        v_ip_address,
        SYSDATE
    );
    
    -- 输出监控信息
    DBMS_OUTPUT.PUT_LINE('=== DDL操作监控 ===');
    DBMS_OUTPUT.PUT_LINE('用户: ' || ora_login_user);
    DBMS_OUTPUT.PUT_LINE('操作: ' || ora_sysevent);
    DBMS_OUTPUT.PUT_LINE('对象类型: ' || ora_dict_obj_type);
    DBMS_OUTPUT.PUT_LINE('对象名称: ' || ora_dict_obj_name);
    DBMS_OUTPUT.PUT_LINE('对象所有者: ' || ora_dict_obj_owner);
    DBMS_OUTPUT.PUT_LINE('时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
    
EXCEPTION
    WHEN OTHERS THEN
        -- DDL触发器中的异常处理要小心,避免阻止正常DDL操作
        INSERT INTO ddl_audit_log (
            audit_id, username, ddl_operation, ddl_text, audit_timestamp
        ) VALUES (
            ddl_audit_seq.NEXTVAL, USER, 'ERROR', 'DDL audit error: ' || SQLERRM, SYSDATE
        );
        COMMIT;
END;
/

-- 测试DDL触发器
-- 创建表
CREATE TABLE test_ddl_table (
    id NUMBER,
    description VARCHAR2(100)
);

-- 修改表
ALTER TABLE test_ddl_table ADD created_date DATE DEFAULT SYSDATE;

-- 创建索引
CREATE INDEX idx_test_ddl_id ON test_ddl_table(id);

-- 删除对象
DROP INDEX idx_test_ddl_id;
DROP TABLE test_ddl_table;

-- 查看DDL审计记录
SELECT username, ddl_operation, object_type, object_name, audit_timestamp
FROM ddl_audit_log
ORDER BY audit_timestamp DESC;

4.1.2 防止意外删除的保护触发器

-- 创建保护重要表的DDL触发器
CREATE OR REPLACE TRIGGER trg_protect_critical_tables
    BEFORE DROP ON SCHEMA
DECLARE
    v_object_name VARCHAR2(128);
    v_current_time VARCHAR2(10);
    v_is_protected BOOLEAN := FALSE;
    
    -- 定义受保护的表列表
    TYPE protected_tables_type IS TABLE OF VARCHAR2(128);
    protected_tables protected_tables_type := protected_tables_type(
        'EMPLOYEES_DEMO',
        'EMPLOYEE_AUDIT', 
        'SALARY_HISTORY',
        'DDL_AUDIT_LOG'
    );
    
BEGIN
    v_object_name := ora_dict_obj_name;
    v_current_time := TO_CHAR(SYSDATE, 'HH24:MI:SS');
    
    -- 检查是否为受保护的表
    FOR i IN 1..protected_tables.COUNT LOOP
        IF UPPER(v_object_name) = protected_tables(i) THEN
            v_is_protected := TRUE;
            EXIT;
        END IF;
    END LOOP;
    
    IF v_is_protected THEN
        -- 在工作时间(9:00-18:00)禁止删除重要表
        IF TO_NUMBER(TO_CHAR(SYSDATE, 'HH24')) BETWEEN 9 AND 18 THEN
            RAISE_APPLICATION_ERROR(-20100, 
                '禁止在工作时间(9:00-18:00)删除重要表: ' || v_object_name);
        END IF;
        
        -- 记录删除尝试
        INSERT INTO ddl_audit_log (
            audit_id, username, object_type, object_name, ddl_operation,
            ddl_text, audit_timestamp
        ) VALUES (
            ddl_audit_seq.NEXTVAL, USER, ora_dict_obj_type, v_object_name, 'DROP_ATTEMPT',
            '尝试删除受保护的表: ' || v_object_name, SYSDATE
        );
        COMMIT;
        
        -- 发出警告但允许删除(非工作时间)
        DBMS_OUTPUT.PUT_LINE('警告: 正在删除重要表 ' || v_object_name);
        DBMS_OUTPUT.PUT_LINE('删除时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
        DBMS_OUTPUT.PUT_LINE('操作用户: ' || USER);
    END IF;
    
EXCEPTION
    WHEN OTHERS THEN
        -- 记录错误但不阻止操作
        NULL;
END;
/

-- 测试保护触发器
-- 创建测试表
CREATE TABLE test_protected_table AS SELECT * FROM employees_demo WHERE 1=0;

-- 尝试删除(应该成功,因为不在保护列表中)
DROP TABLE test_protected_table;

-- 尝试删除受保护的表(在工作时间会被阻止)
-- DROP TABLE employees_demo; -- 取消注释来测试

5. 系统事件触发器

5.1 登录和注销触发器

5.1.1 用户会话监控

-- 创建会话监控表
CREATE TABLE user_session_log (
    log_id NUMBER PRIMARY KEY,
    username VARCHAR2(30),
    session_id NUMBER,
    host_name VARCHAR2(64),
    ip_address VARCHAR2(15),
    program VARCHAR2(64),
    module VARCHAR2(64),
    login_time DATE,
    logout_time DATE,
    session_duration NUMBER, -- 分钟
    status VARCHAR2(20)
);

CREATE SEQUENCE session_log_seq START WITH 1 INCREMENT BY 1;

-- 创建登录触发器
CREATE OR REPLACE TRIGGER trg_user_login
    AFTER LOGON ON SCHEMA
DECLARE
    v_session_id NUMBER;
    v_host_name VARCHAR2(64);
    v_ip_address VARCHAR2(15);
    v_program VARCHAR2(64);
    v_module VARCHAR2(64);
BEGIN
    -- 获取会话信息
    v_session_id := SYS_CONTEXT('USERENV', 'SESSIONID');
    v_host_name := SYS_CONTEXT('USERENV', 'HOST');
    v_ip_address := SYS_CONTEXT('USERENV', 'IP_ADDRESS');
    v_program := SYS_CONTEXT('USERENV', 'PROGRAM');
    v_module := SYS_CONTEXT('USERENV', 'MODULE');
    
    -- 记录登录信息
    INSERT INTO user_session_log (
        log_id,
        username,
        session_id,
        host_name,
        ip_address,
        program,
        module,
        login_time,
        status
    ) VALUES (
        session_log_seq.NEXTVAL,
        USER,
        v_session_id,
        v_host_name,
        v_ip_address,
        v_program,
        v_module,
        SYSDATE,
        'ACTIVE'
    );
    
    COMMIT;
    
    -- 检查是否为可疑登录
    DECLARE
        v_recent_logins NUMBER;
        v_different_hosts NUMBER;
    BEGIN
        -- 检查最近1小时内的登录次数
        SELECT COUNT(*) INTO v_recent_logins
        FROM user_session_log
        WHERE username = USER
          AND login_time >= SYSDATE - 1/24;
        
        -- 检查是否从不同主机登录
        SELECT COUNT(DISTINCT host_name) INTO v_different_hosts
        FROM user_session_log
        WHERE username = USER
          AND login_time >= SYSDATE - 1/24;
        
        -- 可疑活动警报
        IF v_recent_logins > 10 THEN
            DBMS_OUTPUT.PUT_LINE('警告: 用户 ' || USER || ' 1小时内登录超过10次');
        END IF;
        
        IF v_different_hosts > 3 THEN
            DBMS_OUTPUT.PUT_LINE('警告: 用户 ' || USER || ' 从多个不同主机登录');
        END IF;
    END;
    
EXCEPTION
    WHEN OTHERS THEN
        -- 登录触发器不应阻止用户登录
        NULL;
END;
/

-- 创建注销触发器
CREATE OR REPLACE TRIGGER trg_user_logout
    BEFORE LOGOFF ON SCHEMA
DECLARE
    v_session_id NUMBER;
    v_login_time DATE;
    v_duration NUMBER;
BEGIN
    v_session_id := SYS_CONTEXT('USERENV', 'SESSIONID');
    
    -- 获取登录时间
    SELECT login_time INTO v_login_time
    FROM user_session_log
    WHERE session_id = v_session_id
      AND username = USER
      AND status = 'ACTIVE'
      AND ROWNUM = 1
    ORDER BY login_time DESC;
    
    -- 计算会话持续时间(分钟)
    v_duration := ROUND((SYSDATE - v_login_time) * 24 * 60, 2);
    
    -- 更新注销信息
    UPDATE user_session_log
    SET logout_time = SYSDATE,
        session_duration = v_duration,
        status = 'COMPLETED'
    WHERE session_id = v_session_id
      AND username = USER
      AND status = 'ACTIVE';
    
    COMMIT;
    
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        -- 可能是登录触发器未记录的会话
        INSERT INTO user_session_log (
            log_id, username, session_id, logout_time, status
        ) VALUES (
            session_log_seq.NEXTVAL, USER, v_session_id, SYSDATE, 'LOGOUT_ONLY'
        );
        COMMIT;
    WHEN OTHERS THEN
        -- 注销触发器也不应阻止用户注销
        NULL;
END;
/

-- 创建会话统计查询
CREATE OR REPLACE PROCEDURE show_session_statistics(
    p_username IN VARCHAR2 DEFAULT NULL,
    p_days_back IN NUMBER DEFAULT 7
)
AS
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 用户会话统计 ===');
    DBMS_OUTPUT.PUT_LINE('统计期间: 最近 ' || p_days_back || ' 天');
    IF p_username IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE('用户: ' || p_username);
    END IF;
    DBMS_OUTPUT.PUT_LINE('');
    
    -- 显示会话统计
    FOR rec IN (
        SELECT username,
               COUNT(*) AS total_sessions,
               ROUND(AVG(session_duration), 2) AS avg_duration_min,
               MAX(session_duration) AS max_duration_min,
               COUNT(DISTINCT host_name) AS unique_hosts,
               MAX(login_time) AS last_login
        FROM user_session_log
        WHERE (p_username IS NULL OR username = p_username)
          AND login_time >= SYSDATE - p_days_back
        GROUP BY username
        ORDER BY total_sessions DESC
    ) LOOP
        DBMS_OUTPUT.PUT_LINE('用户: ' || rec.username);
        DBMS_OUTPUT.PUT_LINE('  总会话数: ' || rec.total_sessions);
        DBMS_OUTPUT.PUT_LINE('  平均时长: ' || NVL(rec.avg_duration_min, 0) || ' 分钟');
        DBMS_OUTPUT.PUT_LINE('  最长时长: ' || NVL(rec.max_duration_min, 0) || ' 分钟');
        DBMS_OUTPUT.PUT_LINE('  不同主机: ' || rec.unique_hosts);
        DBMS_OUTPUT.PUT_LINE('  最后登录: ' || TO_CHAR(rec.last_login, 'YYYY-MM-DD HH24:MI:SS'));
        DBMS_OUTPUT.PUT_LINE('---');
    END LOOP;
END;
/

-- 查看会话监控结果
SELECT * FROM user_session_log ORDER BY login_time DESC;

-- 执行统计查询
EXEC show_session_statistics;

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2402705.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于深度学习的无人机轨迹预测

完整代码见文末 随着无人机技术的不断发展&#xff0c;无人机在农业、物流、监控等领域的应用日益广泛。精准的轨迹预测不仅能够提高无人机飞行的效率和安全性&#xff0c;还能在应对复杂环境下的突发状况时做出迅速反应。因此&#xff0c;基于深度学习的无人机轨迹预测已成为…

git连接本地仓库以及gitee

参考:gitee创建新仓库并上传代码_gitee新建仓库导入代码-CSDN博客 git初始化以及添加git分支 在idea查看master主分支 报错 原因gitee推送更新失败问题记录&#xff1a;remote: error: hook declined to update refs/heads/master-CSDN博客 取消邮箱暴露

麒麟v10系统的docker重大问题解决-不支持容器名称解析

今天给客户在麒麟v10Kylin-Server-V10-SP1下安装nextcloudonlyoffice的时候出现无法连接onlyoffice的问题,经过分析找到了是docker版本过低的原因,现在把解决思路和步骤分享给大家。 一、问题 用一键安装工具,给客户装好了系统,Nextcloud可以正常访问 但是访问nextcloud中的o…

基于5G下行信号的模糊函数分析matlab仿真,对比速度模糊函数和距离模糊函数

目录 1.引言 2.算法仿真效果演示 3.数据集格式或算法参数简介 4.MATLAB部分程序 5.算法涉及理论知识概要 6.参考文献 7.完整算法代码文件获得 1.引言 模糊函数&#xff08;Ambiguity Function, AF&#xff09;是信号处理领域用于分析信号时频分辨能力的核心工具&#xf…

Redis 过期了解

Redis 版本&#xff1a;5.0 &#xff1a; 一&#xff1a;过期监听&#xff1a; Spring Data Redis 封装了 Redis 的 Pub/Sub 功能&#xff0c;提供了对 key 过期事件的监听支持。 1. 核心类&#xff1a;KeyExpirationEventMessageListener 这个抽象类是 Spring 提供的&#x…

JAVA理论-JAVA基础知识

1.Java 基础 知识 1.1 面向对象的特征&#xff08;了解&#xff09; 面向对象的特征&#xff1a;封装、继承、多态、抽象 封装&#xff1a;就是把对象的属性和行为&#xff08;数据&#xff09;结合为一个独立的整体&#xff0c;并尽量隐藏对象的内部细节&#xff0c;公开我希…

免费无限使用GPT Plus、Claude Pro、Grok Super、Deepseek满血版

渗透智能-ShirtAI&#xff0c;可以免费无限使用GPT Plus、Claude Pro、Grok Super、Deepseek满血版、除此之外还能免费使用AI搜索、Gemini AI、AI照片修复、AI橡皮擦、AI去背景、AI智能抠图、AI证件照、OCR识别、在线思维导图、在线绘图工具、PDF工具箱、PDF翻译。 传送入口&a…

SoloSpeech - 高质量语音处理模型,一键提取指定说话人音频并提升提取音频清晰度和质量 本地一键整合包下载

视频教程&#xff1a; 一个强大的语音分离和降噪软件 SoloSpeech 是由约翰霍普金斯大学、香港中文大学、南洋理工大学、清华大学及布拉格理工大学等多所高校共同主导开源的一个创新的语音处理项目&#xff0c;旨在解决在多人同时说话的环境中&#xff0c;准确提取并清晰呈现特定…

深入解析 Java ClassLoader:揭开 JVM 动态加载的神秘面纱

大家好&#xff0c;这里是架构资源栈&#xff01;点击上方关注&#xff0c;添加“星标”&#xff0c;一起学习大厂前沿架构&#xff01; Java 之所以能实现“一次编写&#xff0c;到处运行”&#xff0c;很大程度得益于其虚拟机&#xff08;JVM&#xff09;强大的跨平台能力。…

CICD实战(一) -----Jenkins的下载与安装

服务器IPJenkins192.168.242.153gitlab192.168.242.154 1、安装工具&#xff08;可选&#xff0c;如果有就不需要安装&#xff09; sudo yum install wget net-tools 2、关闭防火墙 #关闭防火墙(如果是云服务器部署,去安全组放通对应的端口即可) systemctl stop firewalld …

Devops系列---python基础篇二

1、列表 1.1 概念 格式&#xff1a; 名称 [ “元素1”,“元素2”,…] #定义一个列表 computer ["主机","键盘","显示器","鼠标"]类型方法用途查index(“元素”)查看元素索引位置count(“元素”)统计元素出现的次数reverse()倒序排…

​​TLV4062-Q1​​、TLV4082-Q1​​迟滞电压比较器应用笔记

文章目录 主要作用应用场景关键优势典型应用示意图TLV4062-Q1 和 TLV4082-Q1 的主要作用及应用场景如下: 主要作用 精密电压监测:是一款双通道、低功耗比较器,用于监测输入电压是否超过预设阈值。 集成高精度基准电压源(阈值精度1%),内置60mV迟滞功能,可避免因噪声导致的…

DHCP介绍

DHCP介绍 1 DHCP简述2 DHCP协议分析2.1 主要流程2.2 DHCP全部报文介绍2.3 IP租用更新报文2.4 DHCP协议抓包分析 3 DHCP应用3.1 DNSmasq参数配置3.2 DNSmasq框架代码3.2.1 创建socket监听67端口3.2.2 监听67端口3.2.3 处理DHCP请求 3.3 DNSmasq模块排障方法 4 常见问题排查4.1 问…

[蓝桥杯]耐摔指数

耐摔指数 题目描述 X 星球的居民脾气不太好&#xff0c;但好在他们生气的时候唯一的异常举动是&#xff1a;摔手机。 各大厂商也就纷纷推出各种耐摔型手机。X 星球的质监局规定了手机必须经过耐摔测试&#xff0c;并且评定出一个耐摔指数来&#xff0c;之后才允许上市流通。…

2024年第十五届蓝桥杯青少Scratch初级组-国赛—画矩形

2024年第十五届蓝桥杯青少Scratch初级组-国赛—画矩形 题目点下方&#xff0c;支持在线编程&#xff0c;在线获取源码和素材&#xff5e; 画矩形_scratch_少儿编程题库学习中心-嗨信奥 程序演示可点下方&#xff0c;支持源码获取&#xff5e; 画矩形-scratch作品-少儿编程题库…

JMM初学

文章目录 1,线程间的同步和通信1.1, 共享内存并发模型 (Shared Memory Model)线程通信机制线程同步机制特点 1.2, 消息传递并发模型 (Message Passing Model)线程通信机制线程同步机制特点 适用场景对比 2,Java内存模型JMM2.0,Java内存模型的基础&#xff08;1&#xff09;内存…

构建云原生安全治理体系:挑战、策略与实践路径

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 一、引言&#xff1a;从传统安全走向“云原生安全” 随着企业 IT 架构从传统单体系统向容器化、微服务和云原生平台转型&#xf…

vcs仿真产生fsdb波形的两种方式

目录 方法一&#xff1a; 使用verilog自带的系统函数 方法二&#xff1a; 使用UCLI command 2.1 需要了解什么是vcs的ucli&#xff0c;怎么使用ucli&#xff1f; 2.2 使用ucli dump波形的方法 使用vcs仿真产生fsdb波形有两种方式&#xff0c;本文参考《vcs user guide 20…

Go语言底层(三): sync 锁 与 对象池

1. 背景 在并发编程中&#xff0c;正确地管理共享资源是构建高性能程序的关键。Go 语言标准库中的 sync 包提供了一组基础而强大的并发原语&#xff0c;用于实现安全的协程间同步与资源控制。本文将简要介绍 sync 包中常用的类型和方法: sync 锁 与 对象池&#xff0c;帮助开发…

2025年06月06日Github流行趋势

项目名称&#xff1a;agent-zero 项目地址url&#xff1a;https://github.com/frdel/agent-zero项目语言&#xff1a;Python历史star数&#xff1a;8958今日star数&#xff1a;324项目维护者&#xff1a;frdel, 3clyp50, linuztx, evrardt, Jbollenbacher项目简介&#xff1a;A…