Skip to Main Content
  • Questions
  • Dynamic Triggers to audit updated columns of a table.

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Khushru.

Asked: September 11, 2000 - 9:49 am UTC

Last updated: May 11, 2011 - 10:47 am UTC

Version: 8.1.6

Viewed 10K+ times! This question is

You Asked

I want to audit insert and update actions on the table. The system in question is still in the alpha stage of development and hence the table structure keeps on changes. we do not want to constantly keep on changing the trigger.

The structure of the AUDIT_TRAIL table is

Table_name varchar2(30)
Table_action varchar2(10) ;; values insert or update
Session_id number
Tab_Column_name varchar2(30)
Old_value varchar2(4000)
New_value varchar2(4000)
Date_time_stamp date

Can we solve the problem without using a static PL/SQL Query as describe in one of your previous solutions ?


and Tom said...

There is no opportunity to use dynamic sql to access the :new and :old bind values in a trigger.

What I do in cases like this is use SQL to write SQL. After you alter or modify a table -- you would as a matter of procedure, run a script to generate the trigger for you based on the new set of columns. It maintains the trigger code for you.

See
</code> http://asktom.oracle.com/pls/asktom/f?p=100:11:::::P11_QUESTION_ID:59412348055 <code>for a full implementation of this concept -- very similar to the table you have above already.


Rating

  (10 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

A reader, February 26, 2002 - 7:17 am UTC

How do you include the p_key(rowid)into the audit table from the table that being audit?

Tom Kyte
February 26, 2002 - 9:12 am UTC

the same way you capture the old_value and new_value. Just modify the code generator to pass it along when it passes them.

A reader, February 26, 2002 - 10:29 am UTC

just modify the code??
I tried to get the column_id from the user_tabs_colums
and including the ID in the insert statement but i cant
get it right.
How do you meen?
Thanks in advance.

Tom Kyte
February 26, 2002 - 10:48 am UTC

don't see what column id has to do with it.

take the query:

select ' audit_pkg.check_val( ''&1'', ''' || column_name ||
''', ' || ':new.' || column_name || ', :old.' ||
column_name || ');'
from user_tab_columns where table_name = upper('&1')
/



modify it to something like:

select ' audit_pkg.check_val( ''&1'', ''' || column_name ||
''', ' || ':new.' || column_name || ', :old.' ||
column_name || ', :new.ROWID );'
from user_tab_columns where table_name = upper('&1')
/


then make the audit_pkg take the rowid as the last parameter for each function:

create or replace package audit_pkg
as
procedure check_val( l_tname in varchar2,
l_cname in varchar2,
l_new in varchar2,
l_old in varchar2,
l_rowid in varchar2
);


and record the rowid.



A reader, February 26, 2002 - 12:03 pm UTC

When i try to make a update i get this message:
ORA-01722: invalid number
ORA-06512: at "x.AUDIT_PKG", line 14
ORA-06512: at "x.AUD#IM", line 6
ORA-04088: error during execution of trigger 'x.AUD#IM'
Line 14:
insert into auditim2(im_id, SID,Timestamp,who, tname,cname,old,new) values(l_rowid,userenv('sessionid'), sysdate, user, upper(l_tname), upper(l_cname),l_old, l_new);
at "x.AUD#IM", line 6:
audit_pkg.check_val('IM','anmarkning', :new.anmarkning, :old.anmarkning,'rowid');

I would be greatfull if you could tell me what is wrong.


Tom Kyte
February 26, 2002 - 12:34 pm UTC

You know what -- if you don't know what the code is doing or how it does it (and cannot figure it out) -- it might be best if you didn't use it.


You did not follow the instructions above (else you would have :new.rowid not 'rowid' in the insert statement).  I don't know what else you did wrong.  Just to put it to bed, here you go.  Just CUT AND PASTE this.  Try to understand what it does and how it does it.

create table audit_tbl
(   timestamp    date,
    who          varchar2(30),
    tname        varchar2(30),
    cname        varchar2(30),
    old          varchar2(2000),
    new          varchar2(2000),
    rid          rowid
)
/

create or replace package audit_pkg
as
    procedure check_val( l_tname in varchar2,
                         l_cname in varchar2,
                         l_new in varchar2,
                         l_old in varchar2,
                         l_rowid in varchar2 );

    procedure check_val( l_tname in varchar2,
                         l_cname in varchar2,
                         l_new in date,
                         l_old in date,
                         l_rowid in varchar2 );

    procedure check_val( l_tname in varchar2,
                         l_cname in varchar2,
                         l_new in number,
                         l_old in number,
                         l_rowid in varchar2 );
end;
/


create or replace package body audit_pkg
as

procedure check_val( l_tname in varchar2,
                     l_cname in varchar2,
                     l_new in varchar2,
                     l_old in varchar2,
                     l_rowid in varchar2  )
is
begin
    if ( l_new <> l_old or
         (l_new is null and l_old is not NULL) or
         (l_new is not null and l_old is NULL) )
    then
        insert into audit_tbl values
        ( sysdate, user, upper(l_tname), upper(l_cname),
                             l_old, l_new, l_rowid );
    end if;
end;

procedure check_val( l_tname in varchar2, l_cname in varchar2,
             l_new in date, l_old in date,
                     l_rowid in varchar2  )
is
begin
    if ( l_new <> l_old or
         (l_new is null and l_old is not NULL) or
         (l_new is not null and l_old is NULL) )
    then
        insert into audit_tbl values
        ( sysdate, user, upper(l_tname), upper(l_cname),
          to_char( l_old, 'dd-mon-yyyy hh24:mi:ss' ),
          to_char( l_new, 'dd-mon-yyyy hh23:mi:ss' ), l_rowid );
    end if;
end;

procedure check_val( l_tname in varchar2, l_cname in varchar2,
             l_new in number, l_old in number,
                     l_rowid in varchar2  )
is
begin
    if ( l_new <> l_old or
         (l_new is null and l_old is not NULL) or
         (l_new is not null and l_old is NULL) )
    then
        insert into audit_tbl values
        ( sysdate, user, upper(l_tname), upper(l_cname),
                                 l_old, l_new, l_rowid );
    end if;
end;

end audit_pkg;
/


And the script to generate the trigger would be:

set serveroutput on
set feedback off
set verify off
set embedded on
set heading off
spool tmp.sql

prompt create or replace trigger aud#&1
prompt after update on &1
prompt for each row
prompt begin

select '    audit_pkg.check_val( ''&1'', ''' || column_name ||
          ''', ' || ':new.' || column_name || ', :old.' ||
             column_name || ', :new.rowid);'
from user_tab_columns where table_name = upper('&1')
/
prompt end;;
prompt /

spool off
set feedback on
set embedded off
set heading on
set verify on

@tmp


The test I used is:

ops$tkyte@ORA817DEV.US.ORACLE.COM> create table dept as select * from scott.dept where rownum = 1;

Table created.

ops$tkyte@ORA817DEV.US.ORACLE.COM> @test dept
create or replace trigger aud#dept
after update on dept
for each row
begin
    audit_pkg.check_val( 'dept', 'DEPTNO', :new.DEPTNO, :old.DEPTNO, :new.rowid);
    audit_pkg.check_val( 'dept', 'DNAME', :new.DNAME, :old.DNAME, :new.rowid);
    audit_pkg.check_val( 'dept', 'LOC', :new.LOC, :old.LOC, :new.rowid);
end;
/

Trigger created.

ops$tkyte@ORA817DEV.US.ORACLE.COM> update dept set deptno = deptno+1;

1 row updated.

ops$tkyte@ORA817DEV.US.ORACLE.COM> update dept set dname = lower(dname);

1 row updated.

ops$tkyte@ORA817DEV.US.ORACLE.COM> update dept set loc = initcap(loc), dname = upper(dname);

1 row updated.

ops$tkyte@ORA817DEV.US.ORACLE.COM> @printtbl8 'select * from audit_tbl';
TIMESTAMP                     : 26-feb-2002 12:29:11
WHO                           : OPS$TKYTE
TNAME                         : DEPT
CNAME                         : DEPTNO
OLD                           : 10
NEW                           : 11
RID                           : AAAGl7AABAAAFSaAAA
-----------------
TIMESTAMP                     : 26-feb-2002 12:29:11
WHO                           : OPS$TKYTE
TNAME                         : DEPT
CNAME                         : DEPTNO
OLD                           : 20
NEW                           : 21
RID                           : AAAGl7AABAAAFSaAAB
-----------------
TIMESTAMP                     : 26-feb-2002 12:29:11
WHO                           : OPS$TKYTE
TNAME                         : DEPT
CNAME                         : DEPTNO
OLD                           : 30
NEW                           : 31
RID                           : AAAGl7AABAAAFSaAAC
....
-----------------
TIMESTAMP                     : 26-feb-2002 12:32:56
WHO                           : OPS$TKYTE
TNAME                         : DEPT
CNAME                         : LOC
OLD                           : NEW YORK
NEW                           : New York
RID                           : AAAGl9AABAAAFSaAAA
-----------------

PL/SQL procedure successfully completed. 

A reader, April 07, 2003 - 5:49 pm UTC


Problem Regarding the Dynamic Trigger posted in your website

Hariharan, May 05, 2011 - 11:23 pm UTC

When i am executing the trigger error is occuring

  1  create or replace trigger aud#&1
  2  after update on &1
  3  for each row
  4  begin
  5  select '    audit_pkg.check_val( ''&1'', ''' || column_name ||
  6            ''', ' || ':new.' || column_name || ', :old.' ||
  7               column_name || ');'
  8  from user_tab_columns where table_name = upper('&1');
  9* end;
SQL> /
Enter value for 1: AMTERMAS
old   1: create or replace trigger aud#&1
new   1: create or replace trigger aud#AMTERMAS
Enter value for 1: AMTERMAS
old   2: after update on &1
new   2: after update on AMTERMAS
Enter value for 1: AMTERMAS
old   5: select '    audit_pkg.check_val( ''&1'', ''' || column_name ||
new   5: select '    audit_pkg.check_val( ''AMTERMAS'', ''' || column_name ||
Enter value for 1: AMTERMAS
old   8: from user_tab_columns where table_name = upper('&1');
new   8: from user_tab_columns where table_name = upper('AMTERMAS');

Warning: Trigger created with compilation errors.

SQL> SHO ERROS
SP2-0158: unknown SHOW option "ERROS"
SQL> SHO ERRORS
Errors for TRIGGER AUD#AMTERMAS:

LINE/COL ERROR
-------- -----------------------------------------------------------------
2/1      PLS-00428: an INTO clause is expected in this SELECT statement

Please provide solution for the above as soon as possible.

Tom Kyte
May 06, 2011 - 10:30 am UTC

re-read the original answer please. focus on that bit that says:

And the script to generate the trigger would be:


If you read slower, it would let you code faster ultimately.

I have no idea where you got the idea the trigger looked like that. I wrote a script to CREATE a trigger, there is no code that even remotely resembles what you posted here on this page.

dynamic sql inside trigger

A reader, May 10, 2011 - 2:22 am UTC

Hi tom,

even i am not very clear about why inside a trigger, dynamic does not work with :new and :old, but i am thinking below way should work, can you help to explain why still hit error? If i does not comment out below line, it works fine, but i am thinking the dynamic sql has been
constructed successfully as we can see the right result of dbms_output, so really strange for me and turned me to crazy

code...

create table test(id number);
create table log(id number);

create or replace trigger trg1 after insert on test
for each row
declare
v_str varchar2(100);
v_tmp varchar2(10);
begin
v_tmp:=:new.id;
v_str:='insert into log values('||v_tmp||');';
dbms_output.put_line(v_str);
--v_str:='insert into log values(999)';
execute immediate v_str;
end;
Tom Kyte
May 10, 2011 - 8:59 am UTC

even i am not very clear about why inside a trigger, dynamic does not work with
:new and :old,


because they do???

couple of things here.

a) USE BIND VARIABLES
b) USE BIND VARIABLES
...
z) USE BIND VARIABLES

Oh my gosh - and I almost forgot - USE BIND VARIABLES.


ops$tkyte%ORA11GR2> create table test(id number);

Table created.

ops$tkyte%ORA11GR2> create table log(id number);

Table created.

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> create or replace trigger trg1 after insert on test
  2  for each row
  3  declare
  4          v_str varchar2(100);
  5  begin
  6          v_str:='insert into log(id) values( :x )';
  7          execute immediate v_str USING :new.id;
  8  end;
  9  /

Trigger created.

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> insert into test (id) values( 42 );

1 row created.

ops$tkyte%ORA11GR2> select * from log;

        ID
----------
        42



Suggestions:

a) use bind variables
b) you do not need dynamic sql from where I'm sitting, I don't see any need for it
c) :new and :old can be referenced just like any other variable can be in a trigger, as demonstrated
d) get a better grasp of PL/SQL before you set off developing real life production code - I hope you have a really good mentor to review all you develop for a while. The lack of bind variables here really scares me to DEATH.

trigger

sam, May 10, 2011 - 10:58 pm UTC

Tom:

<<c) :new and :old can be referenced just like any other variable can be in a trigger, as demonstrated >>

I could not set the :NEW values in after insert trigger before. it works in before insert though.



CREATE OR REPLACE TRIGGER TRG_TEST_AI
*
ERROR at line 1:
ORA-04084: cannot change NEW values for this trigger type



create or replace trigger trg_test_ai
after insert on test
for each row

declare

v_val1 number(10);
v_val2 varchar2(10);

begin


SELECT col1,col2 into v_val1,v_val2
from M
where control_no = :NEW.control_no

:NEW.col1 := v_val1;
:NEW.col2 := v_val2;


end;
/

Tom Kyte
May 11, 2011 - 9:26 am UTC

I could not set the :NEW values in after insert trigger before. it works in
before insert though.


Sam - I said "referenced". That means to look at them.


Think about why you cannot set the :new values AFTER insert. Think about it. And check out the documentation - it is not a hidden fact.

trigger

sam, May 11, 2011 - 9:51 am UTC

Tom:

I did check documentation before but i did not find the answer.

http://download.oracle.com/docs/cd/B28359_01/appdev.111/b28370/triggers.htm

is it because of data integrity issue?

It seems you can accomplish same thing with before insert instead of after insert most of the time. right?
for auditing though i prefer after insert.
Tom Kyte
May 11, 2011 - 10:47 am UTC

sam,

think about it.

You are in an AFTER trigger. If you are in an AFTER trigger - has the insert

a) happened?
b) not happened?


If the insert has in fact happened - it is done already - what meaning would changing :new have? The row is *already* inserted.


from that chapter:
http://docs.oracle.com/docs/cd/B28359_01/appdev.111/b28370/triggers.htm#CIHHEJCD

Because the trigger uses the BEFORE keyword, it can access the new values before they go into the table, and can change the values if there is an easily-corrected error by assigning to :NEW.column_name.


http://docs.oracle.com/docs/cd/B28359_01/appdev.111/b28370/triggers.htm#CIHIEFAE

Only the BEFORE EACH ROW section can change the value of :NEW.





It seems you can do things in a before insert that you cannot do in an after insert Sam, you can modify values.

Does that mean "you can accomplish same thing with before insert instead of after insert most of the time."??? I would say:

NO

You use a before for each row trigger IF and ONLY IF you need to modify the :new values.

You use an after for each row trigger in ALL CASES where you do not.



If you wrote a "before insert for each row" trigger for auditing you've done it completely wrong. Not just for "it is my opinion" reasons - but because someone else could come along and put a trigger on the table that modifies the :new values AFTER your trigger fires. You would end up auditing the wrong information. You have to wait until the AFTER trigger - when the data stabilizes - when the data is known - to audit.

ONLY code that needs to modify :new values should be in before for each row
ALL other code needs to be in an after for each row (data validations, everything)



Yet another reason I hate triggers. Not many people know how they actually work.

triggers

Sam, May 13, 2011 - 10:44 am UTC

Tom:

Great educational info that is hard to find in docs.
Makes a lot of sense too.

I wish you can write a book just on triggers and when to use and not to use and the side effects of them.

Thank you!

Trigger not getting compile

Somdutt Harihar, February 16, 2018 - 6:22 am UTC

Hi Tom,

prompt create or replace trigger aud#&1
prompt after update on &1
prompt for each row
prompt begin

select ' audit_pkg.check_val( ''&1'', ''' || column_name ||
''', ' || ':new.' || column_name || ', :old.' ||
column_name || ', :new.rowid);'
from user_tab_columns where table_name = upper('&1')
/
prompt end;;

PLS - 00428 : an into clause is expected in this select statement.

please rebuild this trigger again.

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library