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 中的通用触发器的主要内容,如果未能解决你的问题,请参考以下文章

如何让通用 Webhook 触发器插件与 Jenkins 中的多分支管道一起使用?

oracle中的trigger有几种啊

oracle中的触发器问题

Oracle 中的删除分区触发器

Oracle 中的触发器无效

使用触发器删除 Oracle 表中的行