Is there an easy to to iterate over all :NEW values from an Oracle database trigger execution? - oracle

I am attempting to write a generic trigger that will provide all of the :NEW values for the row inserted. Ultimately I want to turn them into XML and insert the XML string into a binary field on another table.
There are a variable number of columns in each table - many times over 100 fields and over 100 tables in all, so individual mapping to XML per table is extremely time consuming.
Is there a way to reference the :NEW pseudorecord as a collection of column values - or perhaps a way to pass the whole :NEW record to a Stored Procedure that could pass it to a Java function (hosted on the database) that might make the individual values iterable?
I've found an example here:
https://docs.oracle.com/database/121/LNPLS/triggers.htm
Create history table and trigger:
CREATE TABLE tbl_history ( d DATE, old_obj t, new_obj t)
/
CREATE OR REPLACE TRIGGER Tbl_Trg
AFTER UPDATE ON tbl
FOR EACH ROW
BEGIN
INSERT INTO tbl_history (d, old_obj, new_obj)
VALUES (SYSDATE, :OLD.OBJECT_VALUE, :NEW.OBJECT_VALUE);
END Tbl_Trg;
/
This seems to imply there is some sort of way it is storing all of the values as a variable, but this appears to put them directly back into a database table. I want to get the 'text' values of the column values listed.

You can create a stored procedure to create your trigger
for table tbl like
create table tbl (id number, value varchar2(10));
and an history table like
create table tbl_history (d date,id number, value varchar2(10));
you can create your trigger like this
create or replace procedure CREATE_TRIGGER IS
trig_str VARCHAR2(32767);
col_str VARCHAR2(32767) := '(d';
values_str VARCHAR2(32767) := '(sysdate';
begin
trig_str := 'CREATE OR REPLACE TRIGGER Tbl_Trg AFTER UPDATE ON tbl FOR EACH ROW'||chr(10)||
'BEGIN'||chr(10)||chr(9)||'INSERT INTO tbl_history ';
for col in (
SELECT column_name FROM all_tab_columns where table_name = 'TBL'
) loop
col_str := col_str||','||col.column_name;
values_str := values_str||','||':OLD.'||col.column_name;
end loop;
col_str := substr(col_str,1,length(col_str)-1)||')';
values_str := substr(values_str,1,length(values_str)-1)||')';
trig_str := trig_str||col_str||' VALUES '||values_str||';'||chr(10)||'END;';
execute immediate trig_str;
END;
/
With an history table with old and new values it's a bit more complicated but same idea

Related

Update of column value within a trigger

Before insert or update of any columns I want to update 1 system column with standard hash MD5 of all table columns, trigger is attached to. My intention is not to tailor this trigger with enumeration of all columns for each trigger and have a function that returns concatenated list of columns per table.
Table DDL:
create table TEST (
id int,
test varchar(100),
"_HASH" varchar(32)
);
Here is my trigger DDL that I would love to work :
CREATE TRIGGER TEST_SYS_HASH_BEFORE_INSERT_OR_UPDATE
BEFORE INSERT OR UPDATE
ON TEST
FOR EACH ROW
DECLARE
var_columns VARCHAR2(10000);
BEGIN
var_columns := FUNC_LISTAGG_EXT(‘TEST');
EXECUTE IMMEDIATE 'SELECT STANDARD_HASH(' || var_columns || ', ''MD5'') from dual'
INTO :new."_HASH";
END;
However this is simply taking headers and set same hash for every row. If I should do this manually , trigger would look like this, what works as I desire, but create it for several tens of tables would be overwhelming
CREATE OR REPLACE TRIGGER TEST_SYS_HASH_BEFORE_INSERT_OR_UPDATE
BEFORE INSERT OR UPDATE
ON TEST
FOR EACH ROW
DECLARE
var_columns VARCHAR(10000);
BEGIN
var_columns := FUNC_LISTAGG_EXT('TEST');
SELECT STANDARD_HASH( :new."ID" || :new."TEST" , 'MD5' )
INTO :new."_HASH";
FROM DUAL;
END;
So my question is whether solution is achievable
Note:
FUNC_LISTAGG_EXT function returns concatenated list of columns from system view

Accessing old and new values without :OLD and :NEW in a trigger

As discussed here, I'm unable to use :OLD and :NEW on columns with collation other than USING_NLS_COMP. I'm trying to find a way around this but haven't been successful so far.
This is the original trigger:
CREATE OR REPLACE TRIGGER SYS$PERSONSSALUTATIONAU
AFTER UPDATE ON PERSONS
FOR EACH ROW
begin
State_00.Salutations_ToDelete(State_00.Salutations_ToDelete.Count + 1) := :old.SalutationTitle;
State_00.Salutations_ToInsert(State_00.Salutations_ToInsert.Count + 1) := :new.SalutationTitle;
end;
This is what I've tried:
CREATE OR REPLACE TRIGGER SYS$PERSONSSALUTATIONAU
FOR UPDATE ON Persons
COMPOUND TRIGGER
TYPE Persons_Record IS RECORD (
SalutationTitle NVARCHAR2(30)
);
TYPE Persons_Table IS TABLE OF Persons_Record INDEX BY PLS_INTEGER;
gOLD Persons_Table;
gNEW Persons_Table;
BEFORE EACH ROW IS BEGIN
SELECT SalutationTitle
BULK COLLECT INTO gOLD
FROM Persons
WHERE ID = :OLD.ID;
END BEFORE EACH ROW;
AFTER EACH ROW IS BEGIN
SELECT SalutationTitle
BULK COLLECT INTO gNEW
FROM Persons
WHERE ID = :NEW.ID;
END AFTER EACH ROW;
AFTER STATEMENT IS BEGIN
FOR i IN 1 .. gNEW.COUNT LOOP
State_00.Salutations_ToDelete(State_00.Salutations_ToDelete.Count + 1) := gOLD(i).SalutationTitle;
State_00.Salutations_ToInsert(State_00.Salutations_ToInsert.Count + 1) := gNEW(i).SalutationTitle;
END LOOP;
END AFTER STATEMENT;
END;
This results in error ORA-04091. I've also tried moving the select into the AFTER STATEMENT section which works, but there is no way to access the old values. If somebody has a solution for this it would be most appreciated.
EDIT:
I created a minimal reproducible example:
CREATE TABLE example_table (
id VARCHAR2(10),
name NVARCHAR2(100)
);
CREATE TABLE log_table (
id VARCHAR2(10),
new_name NVARCHAR2(100),
old_name NVARCHAR2(100)
);
CREATE OR REPLACE TRIGGER example_trigger
AFTER UPDATE ON example_table
FOR EACH ROW BEGIN
INSERT INTO log_table VALUES(:old.id, :new.name, :old.name);
END;
INSERT INTO example_table VALUES('01', 'Daniel');
-- this works as expected
UPDATE example_table SET name = ' John' WHERE id = '01';
SELECT * FROM log_table;
DROP TABLE example_table;
CREATE TABLE example_table (
id VARCHAR2(10),
-- this is the problematic part
name NVARCHAR2(100) COLLATE XCZECH_PUNCTUATION_CI
);
INSERT INTO example_table VALUES('01', 'Daniel');
-- here nothing is inserted into log_example, if you try to
-- recompile the trigger you'll get error PLS-00049
UPDATE example_table SET name = ' John' WHERE id = '01';
SELECT * FROM log_table;
DROP TABLE example_table;
DROP TABLE log_table;
DROP TRIGGER example_trigger;
In the discussion you reference a document concerning USING_NLS_COMP. That has nothing to do with the error you are getting. The error ORA-04091 is a reference to the table that fired the trigger (mutating). More to come on this. I am not saying you do not have USING_NLS_COMP issues, just that they are NOT causing the current error.
There are misconceptions shown in your trigger. Beginning with the name itself; you should avoid the prefix SYS. This prefix is used by Oracle for internal objects. While SYS prefix is not specifically prohibited at best it causes confusion. If this is actually created in the SYS schema then that in itself is a problem. Never use SYS schema for anything.
There is no reason to create a record type containing a single variable, then create a collection of that type, and finally define variables of the collection. Just create a collection to the variable directly, and define variables of the collection.
The bulk collect in the select statements is apparently misunderstood as used. I assume you want to collect all the new and old values in the collections. Bulk collect however will not do this. Each time bulk collect runs the collection used is cleared and repopulated. Result being the collection contains only the only the LAST population. Assuming id is unique the each collection would contain only 1 record. And now that brings us to the heart of the problem.
The error ORA-04091: <table name> is mutating, trigger/function may not see it results from attempting to SELECT from the table that fired the trigger; this is invalid. In this case the trigger fired due to a DML action on the persons table as a result you cannot select from persons in a row level trigger (stand alone or row level part of a compound trigger. But it is not needed. The pseudo rows :old and :new contain the complete image of the row. To get a value just reference the appropriate row and column name. Assign that to your collection.
Taking all into account we arrive at:
create or replace trigger personssalutation
for update
on persons
compound trigger
type persons_table is table of
persons.salutationtitle%type;
gold persons_table := persons_table();
gnew persons_table := persons_table();
before each row is
begin
gold.extend;
gold(gold.count) := :old.salutationtitle;
end before each row;
after each row is
begin
gnew.extend;
gold(gold.count) := :new.salutationtitle;
end after each row;
after statement is
begin
for i in 1 .. gnew.count loop
state_00.salutations_todelete(state_00.salutations_todelete.count + 1) := gold(i);
state_00.salutations_toinsert(state_00.salutations_toinsert.count + 1) := gnew(i);
end loop;
end after statement;
end personssalutation;
NOTE: Unfortunately you did not provide sample data, nor description of the functions in the AFTER STATEMENT section. Therefore the above is not tested.

Create insert record dynamically by changing pk of existing record for passed in table

I want to pass a table name and schema into a procedure, and have it generate insert, update and delete statements for the particular table. This is part of an automated testing solution (in a development environment) in which I need to test some change data capture. I want to make this dynamic as it is going to be need to be done for lots of different tables over a long period of time, and I need to call it via a REST request through ORDS, so don't want to have to make an endpoint for every table.
Update and delete are fairly easy, however I am struggling with the insert statement. Some of the tables being passed in have hundreds of columns with various constraints, fks etc. so I think it makes sense to just manipulate an existing record by changing only the primary key. I need to be able to modify the primary key to a new value known to me beforehand (e.g. '-1').
Ideally I would create a dynamic rowtype, and select into where rownum = 1, then loop round the primary keys found from all_constraints, and update the rowtype.pk with my new value, before inserting this into the table. Essentially the same as this but without knowing the table in advance.
e.g. rough idea
PROCEDURE manipulate_records(p_owner in varchar2, p_table in varchar2)
IS
cursor c_pk is
select column_name
from all_cons_columns
where owner = p_owner
and constraint_name in (select constraint_name
from all_constraints
where table_name = p_table
and constraint_type = 'P');
l_row tbl_passed_in%ROWTYPE --(I know this isn't possible but ideally)
BEGIN
-- dynamic sql or refcursor to collect a record
select * into tbl_passed_in from tablename where rownum = 1;
-- now loop through pks and reassign their values to my known value
for i in c_pk loop
...if matches then reassign;
...
end loop;
-- now insert the record into the table passed in
END manipulate_records;
I have searched around but haven't found any examples which fit this exact use case, where an unknown column needs to be modified and insert into a table.
Depending on how complex your procedure is, you might be able to store it as a template in a CLOB. Then pull it in, replace table and owner, then compile it.
DECLARE
prc_Template VARCHAR2(4000);
vc_Owner VARCHAR2(0008);
vc_Table VARCHAR2(0008);
BEGIN
vc_Table := 'DUAL';
vc_Owner := 'SYS';
-- Pull code into prc_Template from CLOB, but this demonstrates the concept
prc_Template := 'CREATE OR REPLACE PROCEDURE xyz AS r_Dual <Owner>.<Table>%ROWTYPE; BEGIN NULL; END;';
prc_Template := REPLACE(prc_Template,'<Owner>',vc_Owner);
prc_Template := REPLACE(prc_Template,'<Table>',vc_Table);
-- Create the procedure
EXECUTE IMMEDIATE prc_Template;
END;
Then you have the appropriate ROWTYPE available:
CREATE OR REPLACE PROCEDURE xyz AS r_Dual SYS.DUAL%ROWTYPE; BEGIN NULL; END;
But you can't create the procedure and run it in the same code block.

how to create record type using dynamic sql in oracle?

I have a table named a with column x.
create table A (x varchar2(4000));
insert into A values ('select p,q,r from o');
commit;
Now I want to create a record type dynamically on the basis of above sql i.e.
TYPE rc IS RECORD ( p o.p%type,
q o.q%type,
r o.r%type);
Please let me know how to create above record on runtime.
If its not possible, please suggest any work around.
Thank you.
if you just want to use a record of this table in your code you don't need to create a type, you can declare a rowtype variable as bellow
CREATE TABLE xx_emp (emp_id NUMBER, emp_name VARCHAR2(100));
DECLARE
l_emp_type xx_emp%ROWTYPE;
BEGIN
l_emp_type.emp_id := 10;
l_emp_type.emp_name := 'JOE';
END;
As you commented on a previous answer, I will assume that with "dinamically" you want to create it based on a query that may contain joins,
you can use cursor as bellow
DECLARE
-- here you declare a cursor named c_cursor_name
CURSOR c_cursor_name IS SELECT a.emp_name
, b.dept_name
FROM xx_emp a
, xx_Dept b
WHERE a.dept_id = b.dept_id;
-- here you can declare your record based on your cursor
r_recors_type c_cursor_name%ROWTYPE;
BEGIN
-- now you can fill your cursor as you want
r_recors_type.emp_name := 'JOE';
r_recors_type.dept_name := 'Marketing';
END;

How to use %ROWTYPE when inserting into Oracle table with identity column?

I have an Oracle 12c database with a table containing an identity column:
CREATE TABLE foo (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
bar NUMBER
)
Now I want to insert into the table using PL/SQL. Since in practice the table has many columns, I use %ROWTYPE:
DECLARE
x foo%ROWTYPE;
BEGIN
x.bar := 3;
INSERT INTO foo VALUES x;
END;
However, it give me this error:
ORA-32795: cannot insert into a generated always identity column
ORA-06512: at line 5
Since it is very good for code readability and maintainability, I do not want to stop using %ROWTYPE. Since I under no circumstances want to allow anything but the automatically generated ID's I do not want to lift the GENERATED ALWAYS restriction.
This article suggests that the only way to be able to use %ROWTYPE is to switch to GENERATED BY DEFAULT ON NULL. Is there no other way to fix this?
The only thing I can think of, since you're on 12c is to make the identity column INVISIBLE, like the code below.
The problem is that it makes getting a %ROWTYPE with the id a little more difficult, but it's doable.
Of course, it may also confuse other people using your table to not see a primary key!
I don't think I'd do this, but it is an answer to your question, for what that's worth.
DROP TABLE t;
CREATE TABLE t ( id number invisible generated always as identity,
val varchar2(30));
insert into t (val) values ('A');
DECLARE
record_without_id t%rowtype;
CURSOR c_with_id IS SELECT t.id, t.* FROM t;
record_with_id c_with_id%rowtype;
BEGIN
record_without_id.val := 'C';
INSERT INTO t VALUES record_without_id;
-- If you want ID, you must select it explicitly
SELECT id, t.* INTO record_with_id FROM t WHERE rownum = 1;
DBMS_OUTPUT.PUT_LINE(record_with_id.id || ', ' || record_with_id.val);
END;
/
SELECT id, val FROM t;
You can create a view and insert there:
CREATE OR REPLACE VIEW V_FOO AS
SELECT BAR -- all columns apart from virtual columns
FROM foo;
DECLARE
x V_FOO%ROWTYPE;
BEGIN
x.bar := 3;
INSERT INTO V_FOO VALUES x;
END;
I think that a mix of former answers - using view with invisible identity column - is a optimal way to accomplish this task:
create table foo (id number generated always as identity primary key, memo varchar2 (32))
;
create or replace view fooview (id invisible, memo) as select * from foo
;
<<my>> declare
r fooview%rowtype;
id number;
begin
r.memo := 'first row';
insert into fooview values r
returning id into my.id
;
dbms_output.put_line ('inserted '||sql%rowcount||' row(s) id='||id);
end;
/
inserted 1 row(s) id=1

Resources