个人主页: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_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;
结语
感谢您的阅读!期待您的一键三连!欢迎指正!