【问题标题】:Convert the new row to XML inside a Oracle Trigger在 Oracle 触发器中将新行转换为 XML
【发布时间】:2020-11-01 14:58:27
【问题描述】:

您好,我正在尝试使用 oracle 触发器在表中创建更改日志

日志表由 ,time of action ,tablename,actiontype 和 xmldata (clob) 组成

我正在尝试将新行转换为 xml 并将其保存为 xmldata

 create or replace TRIGGER EVAL_CHANGE_TriggerActual_DYNAMIC
  AFTER INSERT OR UPDATE OR DELETE
  ON PROJ_TEST
  REFERENCING NEW AS new OLD AS old
 FOR EACH ROW
DECLARE
  log_action  varchar(10);
 p_xmldata     XMLtype;
  P_NEWROWDATA    clob;
  p_newrowxml       clob;  
BEGIN

  select rtrim(xmlelementcol,',') into p_newrowxml from (  Select LISTAGG(str, '') as xmlelementcol from (select  'XMLElement("'||cols.column_Name||'", :NEW.'||cols.column_name||'),' as str 
              from SYS.ALL_TAB_COLS cols where upper(cols.owner)=upper('DEV_CUSTOM')  and upper(cols.table_name)=upper('PROJ_TEST')   order by column_id )); 
             p_newrowxml:=CONCAT('select XMLElement("ResearchTable",',p_newrowxml); 
             p_newrowxml:=CONCAT(p_newrowxml,')from dual');  
  DBMS_OUTPUT.PUT_LINE(p_newrowxml); 
         EXECUTE IMMEDIATE  p_newrowxml into p_xmldata;
 p_newrowdata:=p_xmldata.getClobVal();  

 
  IF INSERTING THEN
    log_action := 'Insert';
  ELSIF UPDATING THEN
    log_action := 'Update';
  ELSIF DELETING THEN
    log_action := 'Delete';
  ELSE
    DBMS_OUTPUT.PUT_LINE('This code is not reachable.');
  END IF;
 INSERT INTO audits(table_name, transaction_name, by_user, transaction_date,xmldata,TRIGGERNAMEdesc)
   VALUES('PROJ_TEST', log_action, USER, SYSDATE,p_newrowdata,'EVAL_CHANGE_TriggerDynamic');
END;

现在如果我删除下面的代码

'XMLElement("'||cols.column_Name||'", :NEW.'||cols.column_name||'),' 

'XMLElement("'||cols.column_Name||'", 1),' 

否则它的工作不会在立即执行时出现错误。谁能帮忙

【问题讨论】:

    标签: oracle xmlelement


    【解决方案1】:

    触发器中的一个陷阱是: :new / :old 是非常特定于触发器的并且处理起来很乏味。一种解决方法是显式构造 xmltype:

    create or replace TRIGGER EVAL_CHANGE_TRIGGER
      AFTER INSERT OR UPDATE OR DELETE
      REFERENCING NEW AS NEW OLD AS OLD
      ON ResearchTable
    DECLARE
      log_action  varchar(10);
      p_xmldata     XMLtype;
      p_newrowdata    clob;
    BEGIN
        select XMLElement("ResearchTable", 
                       XMLElement("myColumn1", :NEW.myColumn1),
                       XMLElement("myColumn2", :NEW.myColumn2),
                       ....)
                       into p_xmldata from dual;
                       
       p_newrowdata:=p_xmldata.getClobVal();  
      
      IF INSERTING THEN
        log_action := 'Insert';
      ELSIF UPDATING THEN
        log_action := 'Update';
      ELSIF DELETING THEN
        log_action := 'Delete';
      ELSE
        DBMS_OUTPUT.PUT_LINE('This code is not reachable.');
      END IF;
    
     INSERT INTO auditsResearch (table_name, transaction_name, by_user, transaction_date,XMLDATA)
       VALUES('PROJ_TEST', log_action, USER, SYSDATE,p_newrowdata);
    END;
    

    xml-generation 是非常特定于表的(因为 :new - 限制),我会通过查询目标表的元数据来使用一些代码生成:

    select 'XMLElement("'||cols.column_Name||'", :NEW.'||cols.column_name||'),'
     from SYS.ALL_TAB_COLS cols
    where upper(cols.owner)=upper('MY_TARGET_SCHEMA')
    and upper(cols.table_name)=upper('MY_TABLE')
    order by column_id
    ;
    

    【讨论】:

    • 嗨,Michael 先生 ..您的想法似乎可行 ..但我在创建代码生成时遇到错误,我更新了问题中的代码。请您帮帮我
    • 最后一个 sql 仅用于源代码生成,不用于触发器。执行触发器外部的sql,用生成的数据修改触发器。
    • 代码生成仅在您必须“审核”许多表时才有用......如果您的触发器仅用于一个具有几列的表,则此技术是过度的
    • 您可以创建一个包,通过遍历所有表来生成触发器代码,为每个表创建触发器代码并“立即执行”以创建真正的触发器。
    • “删除”操作可能需要特别注意,所有 :new.XX 都应该为空,只有 :old 可用
    【解决方案2】:

    虽然我的第一个答案有效,但它在很大程度上取决于表格列,如果必须审核许多表格,则不会真正使用。基于@Sayan Malakshinov 的评论和链接的文章StevenFeuerstein&CompoundTrigger。如果在一次更新中处理了许多行,最终的审计循环可能会导致性能问题....批量更新可以移动到更优化的更新复合触发器,将所有更新行写入一个“插入到 tblaudit( ) select auditColumns from tblone where id in (recorded id´s)"

    -- prepare test-scenario 
    
    CREATE TABLE TBLONE
    (
      ID                   NUMBER(19),
      POSTALCODE              VARCHAR2(20 CHAR),
      STREET                  VARCHAR2(255 CHAR),
      HOUSENUMBER             VARCHAR2(25 CHAR),
      CITY                    VARCHAR2(255 CHAR)
    )
    ;
    
    
    ALTER TABLE TBLONE ADD (
      PRIMARY KEY
      (ID)
      USING INDEX);
    
    insert into tblone values (1,'123123','street1','1a','DevVille1');
    insert into tblone values (2,'345','street2','2b','DevVille2');
    insert into tblone values (3,'678','street3','3c','DevVille3');
    
    
    CREATE TABLE TBLAUDIT
    (
      ID                   NUMBER(19),
      log_action          VARCHAR2(10 CHAR),
      log_user          varchar(100 char),
      log_data            clob
    )
    ;
    -- ... timestamp of modification might be missing ...
    
    CREATE OR REPLACE TRIGGER trg_audit_tblone    
    FOR UPDATE OR INSERT OR DELETE ON tblone    
    COMPOUND TRIGGER     
       TYPE id_rt IS RECORD (    
          id_column   NUMBER(19) -- assuming all primary-keys are of same type ....
          ,log_action  varchar(10)
       );    
        
       TYPE row_level_info_t IS TABLE OF id_rt  INDEX BY PLS_INTEGER;    
        
       g_row_level_info   row_level_info_t;    
        
       AFTER EACH ROW IS    
       BEGIN  
          g_row_level_info (g_row_level_info.COUNT + 1).id_column := :NEW.id;   -- store primary key only
          -- remember the type of trigger-action
          IF INSERTING THEN
            g_row_level_info (g_row_level_info.COUNT).log_action := 'Insert';
      ELSIF UPDATING THEN
            g_row_level_info (g_row_level_info.COUNT).log_action := 'Update';
      ELSIF DELETING THEN
            g_row_level_info (g_row_level_info.COUNT).log_action := 'Delete';
       end if;
           
       END AFTER EACH ROW;    
        
       AFTER STATEMENT IS    
          l_rowdata    clob; 
       BEGIN      
          -- for all row-actions recorded 
          FOR indx IN 1 .. g_row_level_info.COUNT    
          LOOP          
            if g_row_level_info (indx).log_action= 'Delete' then
                -- no row selectable, direct insert to audit
                insert into TBLAUDIT values(g_row_level_info(indx).id_column,sys_context('USERENV','SESSION_USER') , g_row_level_info (indx).log_action,null);
            else
                select xmltype(cursor(select * from tblone where id=g_row_level_info (indx).id_column)).getclobval() into l_rowdata from dual;
                insert into TBLAUDIT values(g_row_level_info(indx).id_column,sys_context('USERENV','SESSION_USER') , g_row_level_info (indx).log_action,l_rowdata);
            end if;
          END LOOP;    
       END AFTER STATEMENT;    
    END trg_audit_tblone;     
    
    
    update tblone
    set street='newStreet'
    where id=1;
    
    select id,log_action,log_user,dbms_lob.substr(log_data,200) from tblaudit;
    
    
    update tblone
    set street='newStreet';
    
    select id,log_action,log_user,dbms_lob.substr(log_data,200) from tblaudit;
    
    delete from tblone where id=2;
    
    select id,log_action,log_user,dbms_lob.substr(log_data,200) from tblaudit;
    

    如果真的需要考虑多行更新:

    create TYPE type_audit_entry as object (    
          id_column   NUMBER(19) -- assuming all primary-keys are of same type ....
          ,log_action  varchar(10)
       );
    
    create type typeTable_audit as table of type_audit_entry;
    
    
    CREATE OR REPLACE TRIGGER trg_audit_update_tblone    
    FOR UPDATE ON tblone    
    COMPOUND TRIGGER     
    g_row_level_info typeTable_audit := typeTable_audit();
    
        BEFORE STATEMENT IS
         BEGIN
           g_row_level_info:=typeTable_audit();
         END BEFORE STATEMENT;
        
        
       AFTER EACH ROW IS    
       BEGIN  
            g_row_level_info.extend;
            g_row_level_info (g_row_level_info.COUNT):=type_audit_entry(:NEW.id,null);
       END AFTER EACH ROW;    
        
       AFTER STATEMENT IS    
          l_rowdata    clob; 
       BEGIN      
          insert into TBLAUDIT(ID,log_action,log_user,log_time,log_data)
          select stored_audit.id_column,'Update',sys_context('USERENV','SESSION_USER'),sysdate,xmltype(cursor(select * from tblone where id=stored_audit.id_column)).getclobval()
          from TABLE(g_row_level_info) stored_audit;
       END AFTER STATEMENT;    
    END trg_audit_update_tblone;  
    
    delete from tblaudit;
    
    update tblone
    set street='newStreet';
    
    select id,log_action,log_time,log_user,dbms_lob.substr(log_data,200) from tblaudit;
    

    【讨论】:

    • 很好,但组合主键会失败
    • ?它必须用于组合键,仅此而已。该方法本身并不关心关键结构
    • 或直接从 oracle 使用商业审计选项,不涉及编码
    • 您好,我使用了您的建议并缩小了错误范围,请您检查更新后的问题,它似乎是一些语法问题
    • 正如我在 cmets 中对我的回答所说,您可以更好地改进您的解决方案:只需将所有更改的行聚合到一个 xmltype 中:select xmltype(cursor(select * from tblone t,table(your_collection) nt where t.id=nt.column_value)) xml_agg_rows from dual
    【解决方案3】:

    有一种简单的方法可以将整行甚至记录集转换为 xml:您可以将 cursor 包装起来并将其作为输入参数传递给 XMLType。例如:

    select xmltype(cursor(select * from test where N=1)) from dual;
    

    PL/SQL 示例:

    declare x xmltype;
    begin
       select xmltype(cursor(select * from test where N=1))
         into x
       from dual;
       dbms_output.put_line(x.getclobval());
    end;
    /
    

    【讨论】:

    • 虽然这在 pl/sql 中可用,但在触发器中不起作用。在触发器中,您将得到一个“ora-04091”,选择触发器所在的同一张表
    • @MichaelHauptmann 这不是问题:而不是单行级(在行级触发器中运行大量代码非常慢),我会将其更改为复合触发器,其中在行-级别触发器我只是在语句后触发器中聚合 ID 并将更改的行的整个行集聚合到一个 xmltype 中。
    • 所以基本上是oraclemagazine 的方法。即使使用复合触发器,您也无法选择触发表而不会导致 ora-04091 .... 或者您有工作示例吗?
    • @MichaelHauptmann ora-04091 出现在行级触发器中,而不是语句级:stevenfeuersteinonplsql.blogspot.com/2016/12/…
    • @SayanMalakshinov 具有空值的列不会出现在生成的 xml 中
    猜你喜欢
    • 1970-01-01
    • 2020-07-15
    • 2013-07-16
    • 1970-01-01
    • 2011-09-11
    • 2021-12-01
    • 2019-04-08
    • 2016-11-16
    • 2015-08-03
    相关资源
    最近更新 更多