

文章目录
- [1. 触发器基础概述](#1. 触发器基础概述)
-
- [1.1 触发器的概念与特点](#1.1 触发器的概念与特点)
- [1.2 触发器的分类](#1.2 触发器的分类)
- [1.3 触发器的执行顺序](#1.3 触发器的执行顺序)
- [2. DML触发器](#2. DML触发器)
-
- [2.1 基础DML触发器](#2.1 基础DML触发器)
-
- [2.1.1 INSERT触发器](#2.1.1 INSERT触发器)
- [2.1.2 UPDATE触发器](#2.1.2 UPDATE触发器)
- [2.1.3 DELETE触发器](#2.1.3 DELETE触发器)
- [2.2 高级DML触发器](#2.2 高级DML触发器)
-
- [2.2.1 复合触发器](#2.2.1 复合触发器)
- [2.2.2 条件触发器](#2.2.2 条件触发器)
- [3. INSTEAD OF 触发器](#3. INSTEAD OF 触发器)
-
- [3.1 视图上的INSTEAD OF触发器](#3.1 视图上的INSTEAD OF触发器)
-
- [3.1.1 复杂视图的DML操作](#3.1.1 复杂视图的DML操作)
- [4. DDL触发器](#4. DDL触发器)
-
- [4.1 系统级DDL触发器](#4.1 系统级DDL触发器)
-
- [4.1.1 数据库对象变更监控](#4.1.1 数据库对象变更监控)
- [4.1.2 防止意外删除的保护触发器](#4.1.2 防止意外删除的保护触发器)
- [5. 系统事件触发器](#5. 系统事件触发器)
-
- [5.1 登录和注销触发器](#5.1 登录和注销触发器)
-
- [5.1.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触发器
sql
-- 创建员工表和相关表用于演示
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', '[email protected]', SYSDATE, 'IT_PROG', 6000
);
INSERT INTO employees_demo (
employee_id, first_name, last_name, email, hire_date, job_id, salary
) VALUES (
1002, 'Jane', 'Smith', '[email protected]', SYSDATE, 'SA_REP', 5500
);
COMMIT;
-- 查看审计记录
SELECT * FROM employee_audit ORDER BY change_date DESC;
2.1.2 UPDATE触发器
sql
-- 创建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 = '[email protected]'
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触发器
sql
-- 创建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 复合触发器
sql
-- 创建复合触发器,处理工资历史记录
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', '[email protected]', SYSDATE, 'IT_PROG', 5000);
INSERT INTO employees_demo (employee_id, first_name, last_name, email, hire_date, job_id, salary)
VALUES (1004, 'Alice', 'Brown', '[email protected]', 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 条件触发器
sql
-- 创建条件触发器:只在特定条件下触发
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操作
sql
-- 创建复杂视图
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', '[email protected]', 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 数据库对象变更监控
sql
-- 创建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 防止意外删除的保护触发器
sql
-- 创建保护重要表的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 用户会话监控
sql
-- 创建会话监控表
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;
结语
感谢您的阅读!期待您的一键三连!欢迎指正!
