oracle 中的通用触发器
Posted
技术标签:
【中文标题】oracle 中的通用触发器【英文标题】:generic triggers in oracle 【发布时间】:2019-10-30 10:23:29 【问题描述】:我正在编写一个通用的 oracle 触发器。假设有许多主表,如 PERSON_INFO、EMPLOYEE_INFO 等,以及它们对应的审计表,如 PERSON_INFO_AUDIT、EMPLOYEE_INFO_AUDIT。结构如下。
PERSON_INFO 有列:-
------------------------------------------------
| PERSON_INFO |
------------------------------------------------
| PERSON_ID | FIRST_NAME | LAST_NAME |
| (NUMBER) | (VARCHAR2) | (VARCHAR2)|
------------------------------------------------
| 1 | Andrew | Jack |
------------------------------------------------
PERSON_INFO_AUDIT 包含 PERSON_INFO 的所有列以及另外两个列 OPERATIONS 和 AUDIT_DATE。
要求是,如果任何主表被更新或主表中的任何行被删除,则主表的旧条目应插入其相应的审计表中。
然后我写了如下更新:-
更新 PERSON_INFO SET FIRST_NAME= 'John';
那么 PERSON_INFO 的旧值应插入到 PERSON_INFO_AUDIT 表中,如下所示:-
PERSON_INFO_AUDIT 现在应该包含:-
-------------------------------------------------------------------------
| PERSON_INFO_AUDIT |
-------------------------------------------------------------------------
| PERSON_ID | FIRST_NAME | LAST_NAME | AUDIT_DATE | OPERATIONS|
| (NUMBER) | (VARCHAR2) | (VARCHAR2)| (TIMESTAMP) | (CHAR) |
-------------------------------------------------------------------------
| 1 | Andrew | Jack | 30-08-2019 | U |
-------------------------------------------------------------------------
这里的 audit_date 是今天的日期,操作指定主表中的行是被删除(D)还是更新(U)。为了方便上述场景,我编写了以下触发函数。
CREATE OR replace TRIGGER trig_PERSON_INFO_deleteupdate
after UPDATE OR DELETE
ON PERSON_INFO
FOR EACH ROW
DECLARE
base_table_name clob;
audit_table_name clob;
base_table_cols_in_string clob;
audit_table_cols_in_string clob;
operation char;
final_query clob;
BEGIN
base_table_name:= 'PERSON_INFO';
audit_table_name := base_table_name || '_AUDIT';
IF UPDATING THEN
operation:= 'U';
ELSE
operation:= 'D';
END IF;
SELECT LISTAGG(COLUMN_NAME, ',') WITHIN GROUP (ORDER BY column_id)
INTO base_table_cols_in_string
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME= 'PERSON_INFO';
audit_table_cols_in_string:= base_table_cols_in_string || ',AUDIT_DATE,OPERATIONS';
final_query:= 'INSERT INTO ' || audit_table_name || '(' || audit_table_cols_in_string || ') VALUES(' || ':OLD.PERSON_ID,:OLD.FIRST_NAME,:OLD.LAST_NAME,' || SYSDATE || ',''' || operation || ''');';
dbms_output.put_line(final_query);
EXECUTE IMMEDIATE final_query;
END;
形成的查询是:
INSERT INTO PERSON_INFO_AUDIT(PERSON_ID,FIRST_NAME,LAST_NAME,AUDIT_DATE,OPERATIONS) VALUES(:OLD.PERSON_ID,:OLD.FIRST_NAME,:OLD.LAST_NAME,30-10-19,'U');
但是,当我尝试使用 EXECUTE IMMEDIATE final_query 执行查询时,我遇到了错误
【问题讨论】:
只去掉最后一个单引号前的分号。 @BarbarosÖzhan 要求只保留审计表中的旧值。新值无论如何都会出现在主表中 您尝试做的事情是不可能的。没有动态方法可以访问:old
和:new
中的字段。通常做的是读取DBA_TAB_COLUMNS
并动态创建整个触发器。
@Abhishek 我想这取决于您所说的“通用”是什么意思。我在您的 OP 中注意到构建您的 final_query
的 PL/SQL 代码按名称引用字段。您需要为每个表重写触发器的那一部分。这是您的理解/期望吗?
让甲骨文承担压力。使用 Flashback Data Archive - 适用于 Oracle 11.2.0.4 及更高版本。
【参考方案1】:
这个错误的原因是:
-
动态查询末尾有分号。删除那个分号。
在准备动态查询时,表的旧值应写为变量/列名,而不是常量(不应在 '' 中写旧值)。
Sysdate 应正确写入。
请在下面找到更正的代码:
create or replace TRIGGER trig_PERSON_INFO_deleteupdate
after UPDATE OR DELETE
ON PERSON_INFO
FOR EACH ROW
DECLARE
base_table_name clob;
audit_table_name clob;
base_table_cols_in_string clob;
audit_table_cols_in_string clob;
operation char;
final_query clob;
BEGIN
base_table_name:= 'PERSON_INFO';
audit_table_name := base_table_name || '_AUDIT';
IF UPDATING THEN
operation:= 'U';
ELSE
operation:= 'D';
END IF;
SELECT LISTAGG(COLUMN_NAME, ',') WITHIN GROUP (ORDER BY column_id)
INTO base_table_cols_in_string
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME= 'PERSON_INFO';
audit_table_cols_in_string:= base_table_cols_in_string || ',AUDIT_DATE,OPERATIONS';
final_query:= 'INSERT INTO ' || audit_table_name || '(' || audit_table_cols_in_string
|| ') VALUES(''' || :OLD.PERSON_ID || ''',''' || :OLD.FIRST_NAME || ''',''' || :OLD.LAST_NAME
|| ''',date ''' || to_char(SYSDATE,'yyyy-mm-dd)' || ''',''' || operation || ''')';
dbms_output.put_line(final_query);
EXECUTE IMMEDIATE final_query;
END;
希望对你有帮助:)
【讨论】:
【参考方案2】:编辑我对编译问题 as the other posted solution 的解决方案更适合 OP 的问题,但我希望保留话语位。
但是,我们必须真正质疑生成动态插入语句是否是最佳解决方案。首先,您还需要生成 VALUES 子句的投影,否则就没有动力了。如果表结构发生变化,您需要更改两组列。此外,审计表名称是固定的(因为拥有触发器的表的名称是固定的)。那么每次生成 INSERT 语句的回报是多少呢?将其与动态 DML 的风险(和开销)进行比较。
Oracle 中“通用触发器”的整个想法是有缺陷的。 SQL 是一种强类型语言,PL/SQL 也是。它们针对预定义的数据结构工作。触发器属于表并使用表的当前结构。因此,任何审计解决方案都应该承认这一事实:使用 RDBMS 的颗粒而不是反对它。
更好的方法是从数据字典中为触发器生成 DDL,并让它执行静态插入语句。是的,每次更改表的结构时都需要重新生成触发代码,但坦率地说,如果您经常更改表的结构,这会成为一种负担,那么您在建模过程中就会遇到更大的问题需要解决.
最后一点。自 Oracle 11.2.0.4 以来,根本不需要编写此类审计触发器。 Oracle 有一项称为闪回数据存档(以前标记为 Total Recall)的功能,它可以自动记录我们想要的任何表。使用内置功能总是比滚动我们自己的代码更可取。因此,如果您使用具有此功能的 Oracle 版本,您绝对应该使用它。 Find out more.
【讨论】:
【参考方案3】:我为我的答案找到了一个通用的解决方案。我的要求是他们有很多基表和相应的审计表。如果任何基表被更新/删除,我希望将它们的相应条目插入到他们的审计表中。为此,我想使用一个动态完成这项工作的通用函数。我找到了解决方案并发布了一个:-
CREATE OR REPLACE procedure IR_DEV.audit_trigger(main_table_name varchar2)
AS
audit_table_name clob;
main_table_col_list clob;
trig_struct clob;
BEGIN
audit_table_name := main_table_name || '_AUDIT';
select LISTAGG(COLUMN_NAME,',') WITHIN GROUP(ORDER BY column_id)
into main_table_col_list
from COLS where table_name=upper(main_table_name);
trig_struct:='CREATE or REPLACE TRIGGER trig_'||main_table_name ||'_deleteupdate'||chr(10)
||'AFTER UPDATE OR DELETE ON '|| main_table_name||chr(10)
||'FOR EACH ROW'||chr(10)
||'DECLARE'||chr(10)
||' opt varchar2(1);'||chr(10)
||'BEGIN'||chr(10)
||' IF UPDATING THEN'||chr(10)
||' opt:=''U'';'||chr(10)
||' ELSE'||chr(10)
||' opt:=''D'';'||chr(10)
||' END IF;'||chr(10)||chr(10)
||' INSERT INTO ' || audit_table_name || ' ('||main_table_col_list||',audit_date,operations )'||chr(10)
||' VALUES ('||':old.'||REPLACE(main_table_col_list,',',',:old.')||',sysdate,opt);'||chr(10)||chr(10)
||'END;';
dbms_output.put_line(trig_struct);
execute immediate trig_struct;
END;
高度赞赏任何 cmets/建议。
【讨论】:
以上是关于oracle 中的通用触发器的主要内容,如果未能解决你的问题,请参考以下文章