使用 oracle 触发器审计 50 列
Posted
技术标签:
【中文标题】使用 oracle 触发器审计 50 列【英文标题】:auditing 50 columns using oracle trigger 【发布时间】:2020-04-18 11:24:49 【问题描述】:我需要在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
的情况下,只有主键(主表)即id
和cre_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
列,所以当r
是2
时,它总是会说它正在插入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_colums
或 all_tab_columns
视图?前三个步骤可以是对其中一个视图的简单查询,当然不会有权限问题?以上是关于使用 oracle 触发器审计 50 列的主要内容,如果未能解决你的问题,请参考以下文章