使用 oracle 触发器审计 50 列

Posted

技术标签:

【中文标题】使用 oracle 触发器审计 50 列【英文标题】:auditing 50 columns using oracle trigger 【发布时间】:2012-06-30 23:50:46 【问题描述】:

我需要在oracle 11g 中创建一个trigger 用于审核表。

我有一张带有50 columns 的表,需要audited

对于every new insert 到表中,我需要在audit table (1 row) 中输入一个条目。 对于every update,假设我更新1st 2nd column,那么它将在审计中创建两条记录,其old value and new value

审计表的结构将是

 id        NOT NULL
 attribute NOT NULL
 OLD VALUE NOT NULL
 NEW VALUE NOT NULL
 cre_date  NOT NULL
 upd_date  NULL
 cre_time  NOT NULL
 upd_time  NULL

insert的情况下,只有主键(主表)即idcre_date and cre_time需要被填充并且attribute等于*,如果更新,假设colA和colB正在更新然后都需要填充。在这种情况下,将使用第一条记录的属性colA 和相应的old and new 值创建两条记录,colB 的属性相同

现在我的审计解决方案是not very optimized,我创建了一个row level trigger,它将根据它的new and old value(如果-else) ,它将填充审计表。 我对我的解决方案不满意,这就是我在这里发帖的原因。 我在下面的链接中看到的另一个解决方案:

http://***.com/questions/1421645/oracle-excluding-updates-of-one-column-for-firing-a-trigger

这在我的情况下不起作用,我已经为此做了一个 POC,如下所示:

create table temp12(id number);

create or replace trigger my_trigger
after update or insert on temp12
for each row
declare
  TYPE tab_col_nt IS table of varchar2(30);

  v_tab_col_nt tab_col_nt;

begin
 v_tab_col_nt := tab_col_nt('id','name');

   for r in v_tab_col_nt.first..v_tab_col_nt.last
   loop
      if updating(r) then
         insert into data_table values(1,'i am updating'||r);
      else
      insert into data_table values(2,'i am inserting'||r);
      end if;
   end loop;

 end;

如果更新它正在调用其他部分,我不知道为什么。 这可以通过compound trigger实现吗

【问题讨论】:

值得一提的是,自从 11.2.0.4 以来,Oracle 捆绑了 Flashback Data Archive(他们的日志功能)免费提供所有版本,因此没有人应该构建自己的审计系统。 Find out more。当然,如果您使用的是 11g 或更早的早期版本,则无关紧要,但是(在时间上)它几乎是 2020 年,这些版本已经过时了。 【参考方案1】:

总是调用else 的直接问题是因为您直接使用索引变量r,而不是查找相关的列名:

for r in v_tab_col_nt.first..v_tab_col_nt.last
loop
    if updating(v_tab_col_nt(r)) then
        insert into data_table values(1,'i am updating '||v_tab_col_nt(r));
    else
        insert into data_table values(2,'i am inserting '||v_tab_col_nt(r));
    end if;
end loop;

您在创建表时也只显示了一个id 列,所以当r2 时,它总是会说它正在插入name,从不更新。更重要的是,如果您确实有一个 name 列并且仅针对给定的 id 更新该列,则此代码将在未更改时将 id 显示为插入。您需要将插入/更新拆分为单独的块:

if updating then
    for r in v_tab_col_nt.first..v_tab_col_nt.last loop
        if updating(v_tab_col_nt(r)) then
            insert into data_table values(1,'i am updating '||v_tab_col_nt(r));
        end if;
    end loop;
else /* inserting */
    for r in v_tab_col_nt.first..v_tab_col_nt.last loop
        insert into data_table values(2,'i am inserting '||v_tab_col_nt(r));
    end loop;
end if;

即使该列不存在,它仍然会说它正在插入 name,但我认为这是一个错误,我猜你会尝试从 user_tab_columns 填充名称列表,如果你真的想尝试让它充满活力。


我同意(至少其中一些)其他人的观点,即您可能最好使用一个获取整行而不是单个列的副本的审计表。您的反对意见似乎是单独列出更改的列的复杂性。当您需要逐列数据时,您仍然可以通过对审计表进行反透视,通过一些工作来获得此信息。例如:

create table temp12(id number, col1 number, col2 number, col3 number);
create table temp12_audit(id number, col1 number, col2 number, col3 number,
    action char(1), when timestamp);

create or replace trigger temp12_trig
before update or insert on temp12
for each row
declare
    l_action char(1);
begin
    if inserting then
        l_action := 'I';
    else
        l_action := 'U';
    end if;

    insert into temp12_audit(id, col1, col2, col3, action, when)
    values (:new.id, :new.col1, :new.col2, :new.col3, l_action, systimestamp);
end;
/

insert into temp12(id, col1, col2, col3) values (123, 1, 2, 3);
insert into temp12(id, col1, col2, col3) values (456, 4, 5, 6);
update temp12 set col1 = 9, col2 = 8 where id = 123;
update temp12 set col1 = 7, col3 = 9 where id = 456;
update temp12 set col3 = 7 where id = 123;

select * from temp12_audit order by when;

        ID       COL1       COL2       COL3 A WHEN
---------- ---------- ---------- ---------- - -------------------------
       123          1          2          3 I 29/06/2012 15:07:47.349
       456          4          5          6 I 29/06/2012 15:07:47.357
       123          9          8          3 U 29/06/2012 15:07:47.366
       456          7          5          9 U 29/06/2012 15:07:47.369
       123          9          8          7 U 29/06/2012 15:07:47.371

因此,对于所采取的每项操作,您都有一个审核行,两个插入和三个更新。但是您希望看到更改的每一列的单独数据。

select distinct id, when,
    case
        when action = 'I' then 'Record inserted'
        when prev_value is null and value is not null
            then col || ' set to ' || value
        when prev_value is not null and value is null
            then col || ' set to null'
        else col || ' changed from ' || prev_value || ' to ' || value
    end as change
from (
    select *
    from (
        select id,
            col1, lag(col1) over (partition by id order by when) as prev_col1,
            col2, lag(col2) over (partition by id order by when) as prev_col2,
            col3, lag(col3) over (partition by id order by when) as prev_col3,
            action, when
        from temp12_audit
    )
    unpivot ((value, prev_value) for col in (
        (col1, prev_col1) as 'col1',
        (col2, prev_col2) as 'col2',
        (col3, prev_col3) as 'col3')
    )
)
where value != prev_value
    or (value is null and prev_value is not null)
    or (value is not null and prev_value is null)
order by when, id;

        ID WHEN                      CHANGE
---------- ------------------------- -------------------------
       123 29/06/2012 15:07:47.349   Record inserted
       456 29/06/2012 15:07:47.357   Record inserted
       123 29/06/2012 15:07:47.366   col1 changed from 1 to 9
       123 29/06/2012 15:07:47.366   col2 changed from 2 to 8
       456 29/06/2012 15:07:47.369   col1 changed from 4 to 7
       456 29/06/2012 15:07:47.369   col3 changed from 6 to 9
       123 29/06/2012 15:07:47.371   col3 changed from 3 to 7

5 条审计记录变成了 7 条更新;三个更新语句显示修改的五列。如果你会经常使用它,你可以考虑把它变成一个视图。

所以让我们稍微分解一下。核心是这个内部选择,它使用lag() 从该id 的先前审计记录中获取该行的先前值:

        select id,
            col1, lag(col1) over (partition by id order by when) as prev_col1,
            col2, lag(col2) over (partition by id order by when) as prev_col2,
            col3, lag(col3) over (partition by id order by when) as prev_col3,
            action, when
        from temp12_audit

这为我们提供了一个临时视图,其中包含所有审计表列和滞​​后列,然后用于 unpivot() 操作,您可以使用它,因为您已将问题标记为 11g:

    select *
    from (
        ...
    )
    unpivot ((value, prev_value) for col in (
        (col1, prev_col1) as 'col1',
        (col2, prev_col2) as 'col2',
        (col3, prev_col3) as 'col3')
    )

现在我们有了一个包含id, action, when, col, value, prev_value 列的临时视图;在这种情况下,因为我只有三列,它的行数是审计表中的三倍。最后,外部选择过滤器仅包含值已更改的行,即 value != prev_value 所在的行(允许空值)。

select
    ...
from (
    ...
)
where value != prev_value
    or (value is null and prev_value is not null)
    or (value is not null and prev_value is null)

我使用case 只是打印一些东西,但当然你可以对数据做任何你想做的事情。 distinct 是必需的,因为审计表中的 insert 条目也在非透视视图中转换为三行,并且我在第一个 case 子句中为所有三行显示相同的文本。

【讨论】:

感谢您的良好解释,我从没想过这可以用 unpivot 完成,但问题是这是否可以优化,因为一天中有数百万笔交易发生,如果我调用此查询然后这会被优化吗?除了在触发器中使用简单的 if-else 值得一提的是,自从 11.2.0.4 以来,Oracle 捆绑了 Flashback Data Archive(他们的日志功能)免费提供所有版本,因此没有人应该构建自己的审计系统。 Find out more【参考方案2】:

为什么不让生活更轻松,并在更新任何列中的任何数据时插入整行。因此,主表上的任何更新(或通常删除)都会首先将原始行复制到审计表中。因此,您的审计表将具有与主表相同的布局,但具有额外的几个跟踪字段,例如:

create or replace trigger my_tab_tr
before update or delete
on my_tab
referencing new as new and old as old
for each row
declare
  l_type varchar2(3);
begin
  if (updating) then
    l_type = 'UPD';
  else
    l_type = 'DEL';
  end if;

insert into my_tab_audit(
 col1,
 col2,
 audit_type,
 audit_date) 
values (
 :old.col1,
 :old.col2,
 l_type,
 sysdate
);
end;

在审计表中添加你喜欢的列,这只是一个典型的例子

【讨论】:

:但是我如何在列级别跟踪这个,在我的更新查询中我可以有 50 列或只有一列,我需要 GUI 中的这个。在你的解决方案中,你只跟踪旧的和新的值不是列名,我在示例中提到了属性 :这可以很容易地用每列 50 个 if -else 来完成,但我只想只更新列。 @GauravSoni 这种方法的简单之处在于您可以跟踪对主表的所有更改,无论是 1 列的数据更改还是 50 列。例如,您可以根据特定日期范围查询 UPD 类型(更新)的审计表,并查看随时间发生的变化。【参考方案3】:

我看到逐个字段审计的唯一方法是检查每个字段的 :OLD 和 :NEW 值,并将适当的记录写入审计表。您可以通过在触发器中设置一个子例程来半自动化此操作,您可以将适当的值传递给该子例程,但是我相信您必须以一种或另一种方式为每个单独的字段编写代码。除非其他人有一个绝妙的方法来使用某种我不知道的反射 API 来做到这一点(并且“我不知道的”每天都适用于更多的东西,或者看起来 :-)。

选择是审计单个字段还是审计整行(我通常称之为“历史”表)取决于您打算如何使用数据。在这种情况下,如果需要报告个别领域的变化,我同意逐个领域的审计似乎更合适。在其他情况下(例如,数据提取必须在任何给定日期可重现),逐行审计或“历史表”方法更适合。

无论审计级别如何(逐字段或逐行),都需要仔细编写比较逻辑以处理 NULL/NOT NULL 情况,以免您被比较咬伤:OLD.FIELD1 = :NEW.FIELD1 其中一个值(或两者)为 NULL,并且最终没有采取适当的操作,因为 NULL 不等于任何东西,甚至它本身。不要问我是怎么知道的... :-)

出于好奇,在 INSERT 发生时将创建的单行中为 OLD_VALUE 和 NEW_VALUE 放入什么?

分享和享受。

【讨论】:

+1 @Bob,我相信你是对的,测试更新所做更改的唯一方法(在 UPDATE FOR EACH ROW 触发器内)是比较 :NEW.col 和 :OLD。列值。 (当然,除非您仔细注意到,Oracle 做出了一些令人难以置信的更改或引入了惊人的新 API)。我也同意你的 cmets 关于“行”历史在某些情况下是一种更合适的方法。非常好的答案。 @spencer7593 - 感谢您的好心 cmets(和支持 :-)。这让我有机会重新阅读这个答案,这样做我意识到我忘记指出在测试逻辑中处理 NULL/NOT NULL 情况的重要性,所以我编辑了答案以包含这个。【参考方案4】:

我喜欢这样做的方式:

    创建一个与现有原始文件平行的审计表 桌子。 向此审计表添加时间戳和用户列。 每当插入或更新原始表时,只需插入 进入审计表。 audi 表应该有一个触发器来设置时间戳和用户值 - 所有其他值都作为新值进来。

那么你可以随时查询谁做了什么,什么时候做了。

【讨论】:

:我需要从 GUI 访问这个审计表,并且基于此我需要根据列级别的变化显示结果,所以当我比较旧值和新值时,这个设计将是非优化的如果从前端调用它 这只是一个查询......但取决于你。【参考方案5】:

一个非常非正统的解决方案: (仅当您有权访问系统表时,至少具有 SELECT 权限)

    你知道你的桌子的名字。标识表所有者的 ID。通过用户(=所有者)的名称在 SYS.USER$ 中查找它。

    在 SYS.OBJ$ 中通过 OWNER#(= 所有者 ID)和 NAME(= 表名)查找表的对象 ID(= OBJ#)。

    通过 OBJ# 在 SYS.COL$ 中查找组成表的列。您将找到所有列、它们的 ID (COL#) 和名称 (NAME)。

    使用在这些列的集合上移动的游标编写一个 UPDATE 触发器。您只需编写一次循环的核心。

    最后:我不提供代码,因为详细信息可能因 Oracle 版本而异。

这是真正的动态 SQL 编程。我碰巧在相当大的企业系统上使用它(团队领导不知道它)并且它有效。它快速可靠。 缺点:特权;可运输性;负责任的人考虑不好。

【讨论】:

您为什么要查看那些底层 SYS 表而不是 user_tab_columsall_tab_columns 视图?前三个步骤可以是对其中一个视图的简单查询,当然不会有权限问题?

以上是关于使用 oracle 触发器审计 50 列的主要内容,如果未能解决你的问题,请参考以下文章

在 Oracle 中审计 DML 更改

oracle12c 中的审计触发器正在编译错误

对审计列使用触发器

使用触发器进行审计列

从 oracle 中的触发器进行审计

如何使用触发器将基表的所有更新列添加到审计表的多行?