当前位置: 首页 > article >正文

【Oracle】触发器

在这里插入图片描述

个人主页: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_auditAFTER INSERT ON employees_demoFOR 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 ONINSERT 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_auditAFTER UPDATE ON employees_demoFOR EACH ROW
DECLAREv_changes VARCHAR2(4000);
BEGIN-- 构建变更信息v_changes := '';IF :OLD.first_name != :NEW.first_name THENv_changes := v_changes || 'First Name: ' || :OLD.first_name || ' -> ' || :NEW.first_name || '; ';END IF;IF :OLD.last_name != :NEW.last_name THENv_changes := v_changes || 'Last Name: ' || :OLD.last_name || ' -> ' || :NEW.last_name || '; ';END IF;IF :OLD.email != :NEW.email THENv_changes := v_changes || 'Email: ' || :OLD.email || ' -> ' || :NEW.email || '; ';END IF;IF :OLD.salary != :NEW.salary THENv_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) THENv_changes := v_changes || 'Job: ' || NVL(:OLD.job_id, 'NULL') || ' -> ' || NVL(:NEW.job_id, 'NULL') || '; ';END IF;-- 只有当有实际变更时才记录IF LENGTH(v_changes) > 0 THENINSERT 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_auditBEFORE DELETE ON employees_demoFOR 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_managementFOR UPDATE OF salary ON employees_demoCOMPOUND 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 ISBEGINDBMS_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 ISBEGIN-- 验证工资变更规则IF :NEW.salary <= 0 THENRAISE_APPLICATION_ERROR(-20001, '工资必须大于0');END IF;IF :NEW.salary > :OLD.salary * 2 THENRAISE_APPLICATION_ERROR(-20002, '工资增长不能超过100%');END IF;IF :NEW.salary < :OLD.salary * 0.5 THENRAISE_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 ISBEGIN-- 收集变更信息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 ISv_total_old_salary NUMBER := 0;v_total_new_salary NUMBER := 0;v_avg_increase_pct NUMBER;BEGIN-- 批量插入工资历史记录FOR i IN 1..g_change_count LOOPINSERT 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 THENv_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_alertAFTER UPDATE OF salary ON employees_demoFOR EACH ROWWHEN (NEW.salary > 10000) -- 条件:新工资超过10000
DECLAREv_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');EXCEPTIONWHEN 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_insertINSTEAD OF INSERT ON employee_dept_viewFOR EACH ROW
DECLAREv_dept_exists NUMBER;v_location_exists NUMBER;
BEGINDBMS_OUTPUT.PUT_LINE('=== INSTEAD OF INSERT 触发器执行 ===');-- 检查部门是否存在SELECT COUNT(*) INTO v_dept_existsFROM departmentsWHERE department_id = :NEW.department_id;IF v_dept_exists = 0 THEN-- 如果部门不存在,先创建部门DBMS_OUTPUT.PUT_LINE('部门不存在,创建新部门: ' || :NEW.department_name);-- 检查位置是否存在SELECT COUNT(*) INTO v_location_existsFROM locationsWHERE 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);EXCEPTIONWHEN OTHERS THENDBMS_OUTPUT.PUT_LINE('插入失败: ' || SQLERRM);RAISE;
END;
/-- 创建INSTEAD OF UPDATE触发器
CREATE OR REPLACE TRIGGER trg_employee_dept_updateINSTEAD OF UPDATE ON employee_dept_viewFOR EACH ROW
BEGINDBMS_OUTPUT.PUT_LINE('=== INSTEAD OF UPDATE 触发器执行 ===');-- 更新员工信息UPDATE employees_demoSET first_name = :NEW.first_name,last_name = :NEW.last_name,email = :NEW.email,salary = :NEW.salary,department_id = :NEW.department_idWHERE 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 THENUPDATE departmentsSET department_name = :NEW.department_name,location_id = :NEW.location_idWHERE 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 THENUPDATE locationsSET city = :NEW.city,country_id = :NEW.country_idWHERE location_id = :NEW.location_id;DBMS_OUTPUT.PUT_LINE('位置更新: ' || SQL%ROWCOUNT || ' 行');END IF;EXCEPTIONWHEN OTHERS THENDBMS_OUTPUT.PUT_LINE('更新失败: ' || SQLERRM);RAISE;
END;
/-- 创建INSTEAD OF DELETE触发器
CREATE OR REPLACE TRIGGER trg_employee_dept_deleteINSTEAD OF DELETE ON employee_dept_viewFOR EACH ROW
DECLAREv_emp_count NUMBER;
BEGINDBMS_OUTPUT.PUT_LINE('=== INSTEAD OF DELETE 触发器执行 ===');-- 删除员工DELETE FROM employees_demoWHERE employee_id = :OLD.employee_id;DBMS_OUTPUT.PUT_LINE('员工删除: ' || :OLD.first_name || ' ' || :OLD.last_name);-- 检查部门是否还有员工SELECT COUNT(*) INTO v_emp_countFROM employees_demoWHERE department_id = :OLD.department_id;IF v_emp_count = 0 THENDBMS_OUTPUT.PUT_LINE('部门 ' || :OLD.department_name || ' 已无员工,考虑删除部门');-- 可以选择删除空部门或保留END IF;EXCEPTIONWHEN OTHERS THENDBMS_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_auditAFTER DDL ON SCHEMA -- 在当前SCHEMA上的DDL操作后触发
DECLAREv_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'));EXCEPTIONWHEN 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_tablesBEFORE DROP ON SCHEMA
DECLAREv_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');BEGINv_object_name := ora_dict_obj_name;v_current_time := TO_CHAR(SYSDATE, 'HH24:MI:SS');-- 检查是否为受保护的表FOR i IN 1..protected_tables.COUNT LOOPIF UPPER(v_object_name) = protected_tables(i) THENv_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 THENRAISE_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;EXCEPTIONWHEN 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_loginAFTER LOGON ON SCHEMA
DECLAREv_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;-- 检查是否为可疑登录DECLAREv_recent_logins NUMBER;v_different_hosts NUMBER;BEGIN-- 检查最近1小时内的登录次数SELECT COUNT(*) INTO v_recent_loginsFROM user_session_logWHERE username = USERAND login_time >= SYSDATE - 1/24;-- 检查是否从不同主机登录SELECT COUNT(DISTINCT host_name) INTO v_different_hostsFROM user_session_logWHERE username = USERAND login_time >= SYSDATE - 1/24;-- 可疑活动警报IF v_recent_logins > 10 THENDBMS_OUTPUT.PUT_LINE('警告: 用户 ' || USER || ' 1小时内登录超过10次');END IF;IF v_different_hosts > 3 THENDBMS_OUTPUT.PUT_LINE('警告: 用户 ' || USER || ' 从多个不同主机登录');END IF;END;EXCEPTIONWHEN OTHERS THEN-- 登录触发器不应阻止用户登录NULL;
END;
/-- 创建注销触发器
CREATE OR REPLACE TRIGGER trg_user_logoutBEFORE LOGOFF ON SCHEMA
DECLAREv_session_id NUMBER;v_login_time DATE;v_duration NUMBER;
BEGINv_session_id := SYS_CONTEXT('USERENV', 'SESSIONID');-- 获取登录时间SELECT login_time INTO v_login_timeFROM user_session_logWHERE session_id = v_session_idAND username = USERAND status = 'ACTIVE'AND ROWNUM = 1ORDER BY login_time DESC;-- 计算会话持续时间(分钟)v_duration := ROUND((SYSDATE - v_login_time) * 24 * 60, 2);-- 更新注销信息UPDATE user_session_logSET logout_time = SYSDATE,session_duration = v_duration,status = 'COMPLETED'WHERE session_id = v_session_idAND username = USERAND status = 'ACTIVE';COMMIT;EXCEPTIONWHEN 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
BEGINDBMS_OUTPUT.PUT_LINE('=== 用户会话统计 ===');DBMS_OUTPUT.PUT_LINE('统计期间: 最近 ' || p_days_back || ' 天');IF p_username IS NOT NULL THENDBMS_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_loginFROM user_session_logWHERE (p_username IS NULL OR username = p_username)AND login_time >= SYSDATE - p_days_backGROUP BY usernameORDER BY total_sessions DESC) LOOPDBMS_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.lryc.cn/news/2402705.html

相关文章:

  • 基于深度学习的无人机轨迹预测
  • git连接本地仓库以及gitee
  • 使用Python和OpenCV实现图像识别与目标检测
  • 麒麟v10系统的docker重大问题解决-不支持容器名称解析
  • 基于5G下行信号的模糊函数分析matlab仿真,对比速度模糊函数和距离模糊函数
  • Selenium自动下载浏览器驱动
  • 数据库优化实战分享:高频场景下的性能调优技巧与案例解析
  • Redis 过期了解
  • 微信小程序前端面经
  • android 之 Tombstone
  • 六级作文模板笔记
  • JAVA理论-JAVA基础知识
  • 免费无限使用GPT Plus、Claude Pro、Grok Super、Deepseek满血版
  • SoloSpeech - 高质量语音处理模型,一键提取指定说话人音频并提升提取音频清晰度和质量 本地一键整合包下载
  • 深入解析 Java ClassLoader:揭开 JVM 动态加载的神秘面纱
  • CICD实战(一) -----Jenkins的下载与安装
  • 【.net core】.KMZ文件解压为.KML文件并解析为GEOJSON坐标数据集。附KML处理多线(LineString)闭环问题
  • Python打卡训练营day46——2025.06.06
  • 网络资源缓存
  • Linux中 SONAME 的作用
  • Devops系列---python基础篇二
  • 自定义事件wpf
  • ​​TLV4062-Q1​​、TLV4082-Q1​​迟滞电压比较器应用笔记
  • C++.OpenGL (3/64)着色器(Shader)深入
  • DHCP介绍
  • 李沐《动手学深度学习》d2l安装教程
  • [蓝桥杯]耐摔指数
  • 深入理解数字音频:采样率、位深与量化
  • 2024年第十五届蓝桥杯青少Scratch初级组-国赛—画矩形
  • java面试场景题: 设计⼀个微博系统