Skip to Main Content

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Kim.

Asked: January 03, 2011 - 4:58 pm UTC

Last updated: April 25, 2011 - 1:18 pm UTC

Version: 10.2.0.4

Viewed 1000+ times

You Asked

Hi Tom,

I'm trying to get the value from the ":OLD.xxxxx" column in a before delete trigger. The problem is I don't know the name of the column, as the column(s) are the ones defined in the primary key.

create or replace trigger bef_del_pl_item
before delete on pl_item
for each row
Declare
cursor find_pk_col is
select cc.column_name c_name
from user_cons_columns cc, user_constraints c
where c.TABLE_NAME = 'PL_ITEM'
and cc.TABLE_NAME = c.TABLE_NAME
and c.CONSTRAINT_TYPE = 'P'
and cc.CONSTRAINT_NAME = c.CONSTRAINT_NAME;
string varchar2(2000) :='';
field varchar2(30);
first_col boolean := true;
Begin
for rec in find_pk_col loop
field := ':old.' ||rec.c_name;
dbms_output.put_line(field); -- for test. Now I have the column name
if not first_col then
string := string ||'#';
end if;
string := string || rec.c_name || ':' || field; --it's here I wants the contents of the :old value from column "field". How to get that?
dbms_output.put_line(string); -- for test
end loop;
End;
/

So the outcome of "string should look something like "column_name:value" and if there is more than one column in the primary key then "column_name:value#column_name:value ....."

Later I will replace the "table_name" in my cursor then a variable, as you have described in http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:542325000346769711

Thanks for your help Tom.

Br
Kim

Yes you are right. The goal is to write a generic code. We have around 75 tables, where we (a customer) wants to capture all deletes from those tables. They wants user, date, table and then the primary key to be kept in an audit table. So I try to write some generic code (before delete trigger) to each table that can capture these information and put them into our "audit table". It will save a lot of work to have this generic and not to manual create each trigger. To capture the table name, I have used your code form the link showen above.

Thanks,
Kim

and Tom said...

. It will save a lot of work to have this generic and not to manual create each trigger.

No it won't - simply because *it cannot work*.

However, what WILL work and will work well would be to write something that GENERATES the trigger.

something like this:

ops$tkyte%ORA11GR2> create table t1( x int, y int, z int, primary key(x,y) );

Table created.

ops$tkyte%ORA11GR2> insert into t1 values ( 1, 2, 3 );

1 row created.

ops$tkyte%ORA11GR2> create table t2( a int, b int, c int, primary key(a) );

Table created.

ops$tkyte%ORA11GR2> insert into t2 values ( 1, 2, 3 );

1 row created.

ops$tkyte%ORA11GR2> create table t3( a int, b int, c int, primary key(c,a,b) );

Table created.

ops$tkyte%ORA11GR2> insert into t3 values ( 1, 2, 3 );

1 row created.

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> create or replace procedure generate_trigger( p_tname in varchar2 )
  2  AUTHID CURRENT_USER
  3  as
  4      l_trigger  clob;
  5      l_create   clob;
  6      l_crlf     varchar2(2) := chr(10);
  7      cursor c is
  8                 select cc.column_name c_name
  9                   from  user_cons_columns cc, user_constraints c
 10                  where c.TABLE_NAME    = p_tname
 11                    and  cc.TABLE_NAME    = c.TABLE_NAME
 12                    and  c.CONSTRAINT_TYPE = 'P'
 13                    and  cc.CONSTRAINT_NAME = c.CONSTRAINT_NAME
 14                  order by cc.position;
 15  begin
 16      l_create := 'create table aud$' || dbms_assert.SQL_OBJECT_NAME( p_tname ) ||
 17                  ' as select user username, sysdate del_date, cast( ''x'' as varchar2(30) ) tname ';
 18      for x in C
 19      loop
 20          l_create := l_create || ', ' || x.c_name;
 21      end loop;
 22      l_create := l_create || ' from ' || p_tname || ' where 1=0';
 23      dbms_output.put_line( l_create );
 24          execute immediate l_create;
 25  
 26      l_trigger := 'create or replace trigger aud$' ||
 27                    dbms_assert.SQL_OBJECT_NAME( p_tname ) || l_crlf ||
 28                    'after delete on ' || p_tname || l_crlf ||
 29                    'for each row ' || l_crlf ||
 30                    'begin ' || l_crlf ||
 31                    '  insert into aud$' || p_tname || '( username, del_date, tname ';
 32      for x in C
 33      loop
 34          l_trigger := l_trigger || ', ' || x.c_name;
 35      end loop;
 36      l_trigger := l_trigger || ') values ' || l_crlf ||
 37                  '( USER, SYSDATE, ''' || p_tname || '''';
 38      for x in C
 39      loop
 40          l_trigger := l_trigger || ', :old.' || x.c_name;
 41      end loop;
 42      l_trigger := l_trigger || ');' || l_crlf ||
 43                   'end;';
 44  
 45      dbms_output.put_line( l_trigger );
 46          execute immediate l_trigger;
 47  end;
 48  /

Procedure created.

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> exec generate_trigger( 'T1' );
create table aud$T1 as select user username, sysdate del_date, cast( 'x' as
varchar2(30) ) tname , X, Y from T1 where 1=0
create or replace trigger aud$T1
after delete on T1
for each row 
begin 

insert into aud$T1( username, del_date, tname , X, Y) values 
( USER, SYSDATE,
'T1', :old.X, :old.Y);
end;

PL/SQL procedure successfully completed.

ops$tkyte%ORA11GR2> exec generate_trigger( 'T2' );
create table aud$T2 as select user username, sysdate del_date, cast( 'x' as
varchar2(30) ) tname , A from T2 where 1=0
create or replace trigger aud$T2
after delete on T2
for each row 
begin 

insert into aud$T2( username, del_date, tname , A) values 
( USER, SYSDATE,
'T2', :old.A);
end;

PL/SQL procedure successfully completed.

ops$tkyte%ORA11GR2> exec generate_trigger( 'T3' );
create table aud$T3 as select user username, sysdate del_date, cast( 'x' as
varchar2(30) ) tname , C, A, B from T3 where 1=0
create or replace trigger aud$T3
after delete on T3
for each row 
begin 

insert into aud$T3( username, del_date, tname , C, A, B) values 
( USER,
SYSDATE, 'T3', :old.C, :old.A, :old.B);
end;

PL/SQL procedure successfully completed.

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> delete from t1;

1 row deleted.

ops$tkyte%ORA11GR2> delete from t2;

1 row deleted.

ops$tkyte%ORA11GR2> delete from t3;

1 row deleted.

ops$tkyte%ORA11GR2> 
ops$tkyte%ORA11GR2> select * from aud$t1;

USERNAME                       DEL_DATE  TNAME
------------------------------ --------- ------------------------------
         X          Y
---------- ----------
OPS$TKYTE                      04-JAN-11 T1
         1          2


ops$tkyte%ORA11GR2> select * from aud$t2;

USERNAME                       DEL_DATE  TNAME
------------------------------ --------- ------------------------------
         A
----------
OPS$TKYTE                      04-JAN-11 T2
         1


ops$tkyte%ORA11GR2> select * from aud$t3;

USERNAME                       DEL_DATE  TNAME
------------------------------ --------- ------------------------------
         C          A          B
---------- ---------- ----------
OPS$TKYTE                      04-JAN-11 T3
         3          1          2




Generic code always seems so cool - but it isn't (really - it usually isn't). Static code performs a lot better (in general) and often can be easily generated as demonstrated.

(I got lucky today - I actually typed that code in as you see it - it compiled the first time and worked :) Must be my lucky day)

If it does not do precisely what you want - it gives you the framework.


It also demonstrates one of the RARE uses of authid current_user. That lets this stored procedure run with the privileges of the INVOKER - including their roles (like create table, create trigger) and makes it so it can work for *any user* in their schema:

ops$tkyte%ORA11GR2> connect scott/tiger
Connected.

scott%ORA11GR2> exec ops$tkyte.generate_trigger( 'EMP' );
create table aud$EMP as select user username, sysdate del_date, cast( 'x' as
varchar2(30) ) tname , EMPNO from EMP where 1=0
create or replace trigger aud$EMP
after delete on EMP
for each row 
begin 

insert into aud$EMP( username, del_date, tname , EMPNO) values 
( USER,
SYSDATE, 'EMP', :old.EMPNO);
end;

PL/SQL procedure successfully completed.

Rating

  (2 ratings)

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

Comments

A good solution, but perhaps not the best

Kurt Arthur, January 04, 2011 - 12:27 pm UTC

I know nothing about the original poster's problem domain, but perhaps a better solution is to get rid of the triggers in favor of developing and using transactional API packages.

The package methods could/should include the logic needed to audit the information being inserted, updated or deleted. There are a ton of other benefits to a well-designed API, too (some of which are mentioned in the
"Considering SQL as a Service" discussion at:

http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:672724700346558185

I figure any trigger that isn't in the database is one fewer trigger that will cause trouble later on.


Tom Kyte
January 04, 2011 - 1:29 pm UTC

I agree with you - in most part - however in this case, this is an example of a "safe" trigger.

It isn't being used to do some tricky magic.
It doesn't change the original statements effect.

I do like the XAPI (honestly, it is my approach..)

But in this case - a trigger might be appropriate because it is rather safe (no unusual side effects), it is straightforward and simple (a single insert) and given:

... We have around 75 tables, where we (a customer) wants to capture all deletes from those tables. ...

it sounds like they are trying to put this on an existing application - a 3rd party application - and changing the code wouldn't be possible.

Problem with implementing the above mentioned solution

subhadra menon, April 25, 2011 - 9:28 am UTC

Hi tom,
Based on the solution specified by you we have built a procedure which we will use to built or rbuild a trigger at run time suign using dynamic SQL the code of the above mentioned procedure is as attached.
create or replace

PROCEDURE GENERATE_TRIGGER
(
P_TNAME IN VARCHAR2
) AS

L_TRIGGER VARCHAR2(32767);
L_TRIGGER_INSERT VARCHAR2(32767);
L_TRIGGER_DELETE VARCHAR2(32767);
L_TRIGGER_UPDATE VARCHAR2(32767);
L_CRLF VARCHAR2(2) := CHR(10);
L_O_STRING VARCHAR2(100);
L_N_STRING VARCHAR2(100);
L_COLUMN_NAME VARCHAR2(30);
V_COUNT_INSERT NUMBER(10);
V_COUNT_UPDATE NUMBER(10);
V_COUNT_DELETE NUMBER(10);
V_CAL_STRING VARCHAR2(30);
V_FINAL_STRING VARCHAR2(32767);

BEGIN
L_TRIGGER := 'CREATE OR REPLACE TRIGGER AUDIT_'||
DBMS_ASSERT.SQL_OBJECT_NAME( P_TNAME ) || L_CRLF ||'BEFORE ';

SELECT COUNT(*) INTO V_COUNT_INSERT FROM AUDIT_TRAIL_MASTER ATM WHERE ATM.TABLE_NAME =P_TNAME AND ATM.ACTION_TYPE_CONFIG IN(1,3,5,7);
SELECT COUNT(*) INTO V_COUNT_UPDATE FROM AUDIT_TRAIL_MASTER ATM WHERE ATM.TABLE_NAME =P_TNAME AND ATM.ACTION_TYPE_CONFIG IN(2,3,6,7);
SELECT COUNT(*) INTO V_COUNT_DELETE FROM AUDIT_TRAIL_MASTER ATM WHERE ATM.TABLE_NAME =P_TNAME AND ATM.ACTION_TYPE_CONFIG IN(4,5,6,7);

CASE WHEN V_COUNT_INSERT > 0 THEN
IF (V_CAL_STRING IS NULL) THEN
V_CAL_STRING := ' INSERT';
ELSE
V_CAL_STRING:= V_CAL_STRING || ' OR INSERT';
END IF;
WHEN V_COUNT_UPDATE > 0 THEN
IF (V_CAL_STRING IS NULL) THEN
V_CAL_STRING := ' UPDATE';
ELSE
V_CAL_STRING:= V_CAL_STRING || ' OR UPDATE';
END IF;
WHEN V_COUNT_DELETE > 0 THEN
IF (V_CAL_STRING IS NULL) THEN
V_CAL_STRING := ' DELETE';
ELSE
V_CAL_STRING:= V_CAL_STRING || ' OR DELETE';
END IF;
END CASE;

V_CAL_STRING:= V_CAL_STRING || ' ON';

L_TRIGGER := L_TRIGGER || V_CAL_STRING||' '|| P_TNAME || L_CRLF ||
'FOR EACH ROW ' || L_CRLF ||
'DECLARE'||L_CRLF||
'V_OLD_STRING VARCHAR2(4000);'||L_CRLF||
'V_NEW_STRING VARCHAR2(4000);'||L_CRLF||
'BEGIN';
/*|| L_CRLF||
':1'|| L_CRLF||
':2'|| L_CRLF||
':3'|| L_CRLF||
'END;';*/

IF V_COUNT_INSERT > 0 THEN

L_TRIGGER_INSERT:='IF INSERTING THEN'||L_CRLF;

FOR X IN (SELECT COLUMN_NAME FROM USER_TAB_COLUMNS UTC,AUDIT_TRAIL_MASTER ATM
WHERE ATM.COLUMN_NUMBER = UTC.COLUMN_ID
AND ATM.TABLE_NAME = UTC.TABLE_NAME
AND ATM.ACTION_TYPE_CONFIG IN (1,3,5,7)
AND UTC.TABLE_NAME=P_TNAME)

LOOP

IF (L_N_STRING IS NULL) THEN
L_N_STRING := L_COLUMN_NAME ||'= :NEW.'||L_COLUMN_NAME;
ELSE
L_N_STRING:=L_N_STRING||';'||L_COLUMN_NAME||'= :NEW.'||L_COLUMN_NAME;
END IF;

END LOOP;

L_TRIGGER_INSERT:=L_TRIGGER_INSERT||L_CRLF||'V_NEW_STRING:='||L_N_STRING;

L_TRIGGER_INSERT:=L_TRIGGER_INSERT||'AUDIT_FUNCTION.AUDIT_ON_INSERT('||P_TNAME||',V_NEW_STRING)'||L_CRLF||'END IF;';

ELSE

L_TRIGGER_INSERT := L_CRLF;

END IF;

IF V_COUNT_DELETE > 0 THEN
L_TRIGGER_DELETE:='IF DELETING THEN'||L_CRLF;
FOR X IN (SELECT COLUMN_NAME FROM USER_TAB_COLUMNS UTC,AUDIT_TRAIL_MASTER ATM
WHERE ATM.COLUMN_NUMBER = UTC.COLUMN_ID
AND ATM.TABLE_NAME = UTC.TABLE_NAME
AND ATM.ACTION_TYPE_CONFIG IN (4,5,6,7)
AND UTC.TABLE_NAME=P_TNAME)
LOOP
--L_O_STRING:=L_O_STRING||L_COLUMN_NAME||'= :OLD.'||L_COLUMN_NAME||';';

IF (L_O_STRING IS NULL) THEN
L_O_STRING := L_COLUMN_NAME ||'= :OLD.'||L_COLUMN_NAME;
ELSE
L_O_STRING:=L_O_STRING||';'||L_COLUMN_NAME||'= :OLD.'||L_COLUMN_NAME;
END IF;

END LOOP;
L_TRIGGER_DELETE:=L_TRIGGER_DELETE ||L_CRLF|| 'V_OLD_STRING:=' || L_N_STRING;

L_TRIGGER_DELETE:=L_TRIGGER_DELETE || 'AUDIT_FUNCTION.AUDIT_ON_DELETE(' ||P_TNAME || ',V_OLD_STRING)'||L_CRLF||'END IF;' ;

ELSE

L_TRIGGER_DELETE := L_CRLF;

END IF;

IF V_COUNT_UPDATE > 0 then
L_TRIGGER_UPDATE:='IF UPDATING THEN'||L_CRLF;
FOR X IN (SELECT COLUMN_NAME FROM USER_TAB_COLUMNS UTC,AUDIT_TRAIL_MASTER ATM
WHERE ATM.COLUMN_NUMBER = UTC.COLUMN_ID
AND ATM.TABLE_NAME = UTC.TABLE_NAME
AND ATM.ACTION_TYPE_CONFIG IN (2,3,6,7)
AND UTC.TABLE_NAME=P_TNAME)
LOOP

--L_O_STRING:=L_O_STRING||L_COLUMN_NAME||'= :OLD.'||L_COLUMN_NAME||';';
IF (L_O_STRING IS NULL) THEN
L_O_STRING := L_COLUMN_NAME ||'= :OLD.'||L_COLUMN_NAME;
ELSE
L_O_STRING:=L_O_STRING||';'||L_COLUMN_NAME||'= :OLD.'||L_COLUMN_NAME;
END IF;

--L_N_STRING:=L_N_STRING||L_COLUMN_NAME||'= :NEW.'||L_COLUMN_NAME||';';

IF (L_N_STRING IS NULL) THEN
L_N_STRING := L_COLUMN_NAME ||'= :NEW.'||L_COLUMN_NAME;
ELSE
L_N_STRING:=L_N_STRING||';'||L_COLUMN_NAME||'= :NEW.'||L_COLUMN_NAME;
END IF;
end LOOP;
L_TRIGGER_UPDATE:=L_TRIGGER_UPDATE||L_CRLF||'V_OLD_STRING:='||L_O_STRING;
L_TRIGGER_UPDATE:=L_TRIGGER_UPDATE||L_CRLF||'V_NEW_STRING:='||L_N_STRING;
L_TRIGGER_UPDATE:=L_TRIGGER_UPDATE||'AUDIT_FUNCTION.AUDIT_ON_UPDATE('||P_TNAME||',V_OLD_STRING,V_NEW_STRING)'||L_CRLF||'END IF;' ;

ELSE

L_TRIGGER_UPDATE := L_CRLF;

END IF;
V_FINAL_STRING := L_TRIGGER|| L_TRIGGER_INSERT||L_TRIGGER_DELETE||L_TRIGGER_UPDATE||L_CRLF||'END;';
DBMS_OUTPUT.PUT(V_FINAL_STRING);
--EXECUTE IMMEDIATE V_FINAL_STRING;
END GENERATE_TRIGGER;

but execution fo this procedure gives the following error:
ORA-01031: insufficient privileges
ORA-06512: at "LANDLEASE.GENERATE_TRIGGER", line 153
ORA-06512: at line 6
01031. 00000 - "insufficient privileges"
*Cause: An attempt was made to change the current username or password
without the appropriate privilege. This error also occurs if
attempting to install a database without the necessary operating
system privileges.
When Trusted Oracle is configure in DBMS MAC, this error may occur
if the user was granted the necessary privilege at a higher label
than the current login.
*Action: Ask the database administrator to perform the operation or grant
the required privileges.
For Trusted Oracle users getting this error although granted the
the appropriate privilege at a higher label, ask the database
administrator to regrant the privilege at the appropriate label.

I dotn think this is a case fo some insufficient privilege could you please speecify were we are goign wrong....

Tom Kyte
April 25, 2011 - 1:18 pm UTC

http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551289900368934430

You need to have CREATE TRIGGER granted directly to you, not via a role.

More to Explore

CLOB/BLOB/etc

Complete documentation on Securefiles and Large Objects here