Running multiple insert and update statements inside oracle trigger - oracle

I have an oracle trigger to insert data to another table. If i use single update query than it works however if i user multiple commands like update and insert it fails with error following error.
ORA-04091: table ADMIN_SMS_HANDLER is mutating, trigger/function may not see it
ORA-06512: at "ADMIN_SMS_TRIG", line 16
ORA-04088: error during execution of trigger 'ADMIN_SMS_TRIG'
This works.
CREATE OR REPLACE TRIGGER ADMIN_SMS_TRIG AFTER
INSERT ON ADMIN_SMS_HANDLER
FOR EACH ROW
DECLARE BEGIN
INSERT INTO SMS (
SMSID,
ANUMBER,
BNUMBER,
MSG,
APP
) VALUES (
SMSSEQ.NEXTVAL,
:NEW.SMS_FROM,
:NEW.SMS_TO,
:NEW.SMS_TEXT,
'APP'
);
END;
But this is not working
CREATE OR REPLACE TRIGGER ADMIN_SMS_TRIG AFTER
INSERT ON ADMIN_SMS_HANDLER
FOR EACH ROW
DECLARE BEGIN
INSERT INTO SMS (
SMSID,
ANUMBER,
BNUMBER,
MSG,
APP
) VALUES (
SMSSEQ.NEXTVAL,
:NEW.SMS_FROM,
:NEW.SMS_TO,
:NEW.SMS_TEXT,
'app'
);
UPDATE ADMIN_SMS_HANDLER
SET
SENT_DATE = SYSDATE,
SENT_STATUS = 1,
UPDATED_BY = 'trigger',
UPDATED_DATE = SYSDATE
WHERE
ID = :NEW.ID;
END;

The issue has nothing to do with multiple statements. The issue is that a row-level trigger (generally) cannot reference the table on which it is defined.
Assuming that the intention is to have the trigger automatically populate some columns, you would do that by directly setting values in the :new pseudo-record, i.e.
:new.sent_date := sysdate;
:new.sent_status := 1;
But since you want to modify data in the current row, you'd need to use a before insert trigger rather than an after insert trigger. Something like
CREATE OR REPLACE TRIGGER ADMIN_SMS_TRIG
BEFORE INSERT ON ADMIN_SMS_HANDLER
FOR EACH ROW
DECLARE
BEGIN
INSERT INTO SMS (
SMSID,
ANUMBER,
BNUMBER,
MSG,
APP
) VALUES (
SMSSEQ.NEXTVAL,
:NEW.SMS_FROM,
:NEW.SMS_TO,
:NEW.SMS_TEXT,
'app'
);
:new.SENT_DATE := SYSDATE;
:new.SENT_STATUS := 1;
:new.UPDATED_BY := 'trigger';
:new.UPDATED_DATE := SYSDATE;
END;

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

How to get the main ID of updated row and use it before DML in trigger

I'm blocked with the development of an Oracle trigger (11g). The goal of the trigger is to store some data in an audit trail table. One of the fields that is populated in this table is an XML block generated by a function in a package.
This function has 1 argument: The ID of the row to be updated / deleted.
So I first wrote this trigger:
CREATE OR REPLACE TRIGGER TRG_TEMPLATE_FORMAT
BEFORE UPDATE OR DELETE
ON TEMPLATE_FORMAT
FOR EACH ROW
DECLARE
v_TEMPLATE_XML CLOB;
v_TEMPLATE_NAME VARCHAR2(128);
v_TEMPLATE_FULL_NAME VARCHAR2(128);
BEGIN
IF UPDATING THEN
SELECT TEMPLATE_NAME INTO v_TEMPLATE_NAME FROM TEMPLATES WHERE TEMPLATE_ID =: OLD.TEMPLATE_ID;
SELECT TEMPLATE_FULL_NAME INTO v_TEMPLATE_FULL_NAME FROM TEMPLATES WHERE TEMPLATE_ID = :OLD.TEMPLATE_ID;
v_TEMPLATE_XML := PKG_TEMPLATE_MANAGEMENT.GET_TEMPLATE_XML(:OLD.TEMPLATE_ID);
INSERT INTO TEMPLATES_BACK(
TEMPLATE_ID,
TEMPLATE_FULL_NAME,
TEMPLATE_NAME,
EVENT_TYPE,
EVENT_TIMESTAMP,
OLD_XML_TEMPLATE_SOURCE
)
VALUES(
:OLD.TEMPLATE_ID,
v_TEMPLATE_FULL_NAME,
v_TEMPLATE_NAME,
'UPDATE ON TEMPLATE_FORMAT',
SYSDATE,
v_TEMPLATE_XML
);
ELSIF DELETING THEN
SELECT TEMPLATE_NAME INTO v_TEMPLATE_NAME FROM TEMPLATES WHERE TEMPLATE_ID = :OLD.TEMPLATE_ID;
SELECT TEMPLATE_FULL_NAME INTO v_TEMPLATE_FULL_NAME FROM TEMPLATES WHERE TEMPLATE_ID = :OLD.TEMPLATE_ID;
v_TEMPLATE_XML := PKG_TEMPLATE_MANAGEMENT.GET_TEMPLATE_XML(:OLD.TEMPLATE_ID);
INSERT INTO TEMPLATES_BACK(
TEMPLATE_ID,
TEMPLATE_FULL_NAME,
TEMPLATE_NAME,
EVENT_TYPE,
EVENT_TIMESTAMP,
OLD_XML_TEMPLATE_SOURCE
)
VALUES(
:OLD.TEMPLATE_ID,
v_TEMPLATE_FULL_NAME,
v_TEMPLATE_NAME,
'DELETE FROM TEMPLATE_FORMAT',
SYSDATE,
v_TEMPLATE_XML
);
END IF;
END;
/
As the TEMPLATE_FORMAT table is used in the GET_TEMPLATE_XML() Function, of course, I get the old "good" "Table is mutating" error...
I thought about using a compound trigger after searching a bit, and call my function in the BEFORE STATEMENT block. Problem is that the :OLD / :NEW binds, can't be used in that block
Final Goal of this XML is to capture the values of several records, in this XML before any modification, so it can be easily rolled back.
Is there another way I could have missed to handle this case ?
Thanks in advance
Solved by adding pragma autonomous_transaction in called function (PKG_TEMPLATE_MANAGEMENT.GET_TEMPLATE_XML).
Thanks again #ekochergin

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.

Edit a row you just inserted in the db

I got an example that better explains the situation. I have a table A
CREATE TABLE A( ID NUMBER, VAL NVARCHAR2(255) )
and I create a trigger that does an update on the row it's just inserted
CREATE OR REPLACE TRIGGER XXX
AFTER INSERT
ON A
FOR EACH ROW
DECLARE
BEGIN
UPDATE A SET VAL = 'LOL' WHERE ID = :NEW.ID;
END;
When I perform an insert
INSERT INTO A VALUES(1, 'XX')
I get
ORA-04091: table name is mutating, trigger/function may not see it
Is there a workaround?
You don't need an update, just assign the new value in a BEFORE trigger.
CREATE OR REPLACE TRIGGER XXX
BEFORE INSERT --<< You need a BEFORE trigger for this to work.
ON A
FOR EACH ROW
BEGIN
:new.val := 'LOL';
END;

How to call a Procedure which uses the same table in after trigger

I want to use the same table data after deleting the data which fails in following method.
The issue I faced is the latest change is not getting committed before the after trigger is completed.
create table test_tbl(id_ number, type_ varchar2(100) , count_ number);
create table test_count_tbl(type varchar2(100), count_ number) ;
begin
insert into test_tbl(id_ , type_ , count_ ) values (1,'type1', 10 );
insert into test_tbl(id_ , type_ , count_ ) values (2,'type1', 20 );
insert into test_tbl(id_ , type_ , count_ ) values (3,'type2', 10 );
insert into test_tbl(id_ , type_ , count_ ) values (4,'type2', 40 );
insert into test_tbl(id_ , type_ , count_ ) values (5,'type3', 10 );
insert into test_tbl(id_ , type_ , count_ ) values (6,'type3', 60 );
commit;
end;
create or replace procedure test_count_update_p( p_type_ in varchar2)
is
begin
MERGE INTO test_count_tbl D
USING (select type_, sum(count_) count_sum_
from test_tbl
where type_ = p_type_
group by type_ ) S ON (D.type = S.count_sum_)
WHEN MATCHED THEN UPDATE SET D.count_ = S.count_sum_
-- DELETE WHERE (S.salary > 8000)
WHEN NOT MATCHED THEN INSERT (D.type, D.count_)
VALUES (S.type_, S.count_sum_);
commit;
end ;
CREATE OR REPLACE TRIGGER test_tbl_trigger
AFTER INSERT OR DELETE OR UPDATE ON test_tbl
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
L_TYPE VARCHAR2(100);
BEGIN
if DELETING THEN
L_TYPE := :OLD.TYPE_;
end if;
IF UPDATING OR INSERTING THEN
L_TYPE := :NEW.TYPE_;
end if;
test_count_update_p(L_TYPE);
COMMIT;
END;
Do the following to see the exact issue..
begin
insert into test_tbl(id_ , type_ , count_ ) values (7,'type4', 60 );
commit;
end;
select * from test_tbl ;
record is inserted to the table.
select * from test_count_tbl ;
record is not counted in the this table yet.
begin
delete test_tbl where id_ = 7;
commit ;
end;
select * from test_tbl ;
deleted the record.
select * from test_count_tbl ;
Counted the record which is not available in the table test_tbl;
You can't.
A normal row-level trigger cannot query the table the trigger is defined on because that would raise a mutating table exception. I'm assuming that's why you have declared your trigger to use an autonomous transaction (an autonomous transaction for anything other than persistent logging is almost certainly an error). If you do that, however, your trigger cannot see the uncommitted changes made by the triggering transaction. That's the problem you're encountering now.
An alternative would be to use a compound trigger. You'd declare a collection of test_table.type_%type, you would add the values that are changing to this collection in the row-level portion of your trigger, and then you would iterate over the elements in the collection in the after-statement portion of your trigger. A statement-level trigger is allowed to query the table on which the trigger is defined so you can call your procedure from the after-statement portion of your compound trigger.
Your best action is to drop the TEST_COUNT_TBL table altogether. Just create a view by that name:
create view TEST_COUNT_TBL as
select type_ Type, sum( count_ ) Count
from test_tbl
group by type_;
Then you will always have accurate, up-to-date information at your beck and call but never have to worry about doing strange and wonderful things with triggers.
Used Compound Trigger and removed PRAGMA AUTONOMOUS_TRANSACTION; as well as removed commit statements. Still committed data could access and do the calculations in an external function and write to a separate table.

Resources