【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 触发器的概念与特点
1.2 触发器的分类
1.3 触发器的执行顺序
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;
结语
感谢您的阅读!期待您的一键三连!欢迎指正!
相关文章:

【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…...

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

git连接本地仓库以及gitee
参考:gitee创建新仓库并上传代码_gitee新建仓库导入代码-CSDN博客 git初始化以及添加git分支 在idea查看master主分支 报错 原因gitee推送更新失败问题记录:remote: error: hook declined to update refs/heads/master-CSDN博客 取消邮箱暴露...
使用Python和OpenCV实现图像识别与目标检测
在计算机视觉领域,图像识别和目标检测是两个非常重要的任务。图像识别是指识别图像中的内容,例如判断一张图片中是否包含某个特定物体;目标检测则是在图像中定位并识别多个物体的位置和类别。OpenCV是一个功能强大的开源计算机视觉库…...

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

基于5G下行信号的模糊函数分析matlab仿真,对比速度模糊函数和距离模糊函数
目录 1.引言 2.算法仿真效果演示 3.数据集格式或算法参数简介 4.MATLAB部分程序 5.算法涉及理论知识概要 6.参考文献 7.完整算法代码文件获得 1.引言 模糊函数(Ambiguity Function, AF)是信号处理领域用于分析信号时频分辨能力的核心工具…...
Selenium自动下载浏览器驱动
为什么需要自动下载浏览器驱动? 血泪场景重现 新人入职第一天: 花3小时配置Chrome/Firefox驱动版本不匹配导致SessionNotCreatedException 浏览器自动更新后: 所有测试脚本突然崩溃手动查找驱动耗时长 终极解决方案:自动下载驱…...
数据库优化实战分享:高频场景下的性能调优技巧与案例解析
在实际开发与生产运维中,数据库的性能瓶颈往往是影响系统响应速度和用户体验的关键因素。尤其是在高并发访问、海量数据处理、复杂查询逻辑等高频场景下,数据库优化不仅仅是“锦上添花”,更是“雪中送炭”。本篇博文将结合实际项目经验&#…...

Redis 过期了解
Redis 版本:5.0 : 一:过期监听: Spring Data Redis 封装了 Redis 的 Pub/Sub 功能,提供了对 key 过期事件的监听支持。 1. 核心类:KeyExpirationEventMessageListener 这个抽象类是 Spring 提供的&#x…...
微信小程序前端面经
一、技术栈与编码能力(10min) 1. Vue 3 & Composition API Q1:请解释一下 ref 和 reactive 的区别?你在项目中是如何使用的? 答:ref是包装一个原始值或对象,通过.value访问,r…...
android 之 Tombstone
Android 系统中的 Tombstone 是记录 Native 层崩溃信息的关键日志文件,当应用或系统服务因严重错误(如内存访问异常、空指针解引用等)崩溃时自动生成。以下是其核心机制与分析方法详解: 一、Tombstone 的生成机制 触发条件 当 Na…...
六级作文模板笔记
旧模板 Today there is a growing awareness that mental well-being needs to be given as much attention as physical health. In the contemporary world where change is constant and knowledge is ever-expanding, mental well-being has become increasingly importan…...

JAVA理论-JAVA基础知识
1.Java 基础 知识 1.1 面向对象的特征(了解) 面向对象的特征:封装、继承、多态、抽象 封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽量隐藏对象的内部细节,公开我希…...

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

SoloSpeech - 高质量语音处理模型,一键提取指定说话人音频并提升提取音频清晰度和质量 本地一键整合包下载
视频教程: 一个强大的语音分离和降噪软件 SoloSpeech 是由约翰霍普金斯大学、香港中文大学、南洋理工大学、清华大学及布拉格理工大学等多所高校共同主导开源的一个创新的语音处理项目,旨在解决在多人同时说话的环境中,准确提取并清晰呈现特定…...

深入解析 Java ClassLoader:揭开 JVM 动态加载的神秘面纱
大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构! Java 之所以能实现“一次编写,到处运行”,很大程度得益于其虚拟机(JVM)强大的跨平台能力。…...

CICD实战(一) -----Jenkins的下载与安装
服务器IPJenkins192.168.242.153gitlab192.168.242.154 1、安装工具(可选,如果有就不需要安装) sudo yum install wget net-tools 2、关闭防火墙 #关闭防火墙(如果是云服务器部署,去安全组放通对应的端口即可) systemctl stop firewalld …...
【.net core】.KMZ文件解压为.KML文件并解析为GEOJSON坐标数据集。附KML处理多线(LineString)闭环问题
通过使用ZipFile解压KMZ文件,获取其中的KML文件,并解析KML文件,输出解析后的坐标数据集。 KML文件:地理信息的标准格式 解析后的坐标数据集输出格式(GEOJSON坐标数据集):[[[经度,纬度],[经度,纬度]]] 解…...
Python打卡训练营day46——2025.06.06
知识点回顾: 不同CNN层的特征图:不同通道的特征图什么是注意力:注意力家族,类似于动物园,都是不同的模块,好不好试了才知道。通道注意力:模型的定义和插入的位置通道注意力后的特征图和热力图 …...
网络资源缓存
前端性能优化是提升用户体验和页面响应速度的关键,可以从 网络优化、资源优化、缓存优化 三个方面系统地进行。以下是详细说明: 一、网络优化(减少请求数、降低延迟、提升加载速度) 减少 HTTP 请求数量 合并请求(CSS…...
Linux中 SONAME 的作用
🧠 一、从 -lexample 到 SONAME ✅ 假设你有以下文件结构: /libexample.so → libexample.so.1 /libexample.so.1 → libexample.so.1.0.0 /libexample.so.1.0.0 # SONAME: libexample.so.1/libexample.so.2 → libexample.so.2.0.0 /libexample.so.2.0…...

Devops系列---python基础篇二
1、列表 1.1 概念 格式: 名称 [ “元素1”,“元素2”,…] #定义一个列表 computer ["主机","键盘","显示器","鼠标"]类型方法用途查index(“元素”)查看元素索引位置count(“元素”)统计元素出现的次数reverse()倒序排…...
自定义事件wpf
// 自定义控件 public class MyCustomControl : Control { public static readonly RoutedEvent MyCustomEvent EventManager.RegisterRoutedEvent( "MyCustom", RoutingStrategy.Bubbling, typeof(RoutedEventHandler), typeof(MyCustomControl) ); public event R…...

TLV4062-Q1、TLV4082-Q1迟滞电压比较器应用笔记
文章目录 主要作用应用场景关键优势典型应用示意图TLV4062-Q1 和 TLV4082-Q1 的主要作用及应用场景如下: 主要作用 精密电压监测:是一款双通道、低功耗比较器,用于监测输入电压是否超过预设阈值。 集成高精度基准电压源(阈值精度1%),内置60mV迟滞功能,可避免因噪声导致的…...
C++.OpenGL (3/64)着色器(Shader)深入
着色器(Shader)深入 着色器核心概念 #mermaid-svg-xC0jTt9mJWGVa7yE {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-xC0jTt9mJWGVa7yE .error-icon{fill:#552222;}#mermaid-svg-xC0jTt9mJWGVa7yE .error-text{fi…...

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 问…...
李沐《动手学深度学习》d2l安装教程
文章目录 最新回答报错提醒安装对应版本安装C工具和Windows SDK 最新回答 安装旧版本即可 pip install d2l0.17.0 WARNING: Ignoring invalid distribution -pencv-python (e:\python3.10\lib\site-packages) Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple C…...

[蓝桥杯]耐摔指数
耐摔指数 题目描述 X 星球的居民脾气不太好,但好在他们生气的时候唯一的异常举动是:摔手机。 各大厂商也就纷纷推出各种耐摔型手机。X 星球的质监局规定了手机必须经过耐摔测试,并且评定出一个耐摔指数来,之后才允许上市流通。…...
深入理解数字音频:采样率、位深与量化
在当今数字时代,音频技术已经渗透到我们生活的方方面面——从流媒体音乐到视频会议,从播客到智能家居。但你是否曾好奇过,这些美妙的声音是如何被捕捉、存储并在数字世界中重现的?本文将带你深入了解数字音频的核心概念࿰…...

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