ORA-04091 Mutating Table error during AFTER INSERT Trigger - oracle

I have the following AFTER INSERT trigger on a table called DM_USER_ROLE
create or replace TRIGGER "DM_USER_ROLE_T1"
AFTER
insert on "DM_USER_ROLE"
for each row
DECLARE
v_cert_enrolment_id number;
v_user_role_id number;
begin
v_cert_enrolment_id := "DM_CERTIFICATION_ENROLMEN_SEQ".nextval;
v_user_role_id := :new.USER_ROLE_ID;
/*
When a user is assigned a role, we create an enrolment record
in DM Certification record linked to this user/role combination.
We also insert into the DM_COURSE_ENROLMENT table the courses
associated with the certfication
*/
--FIRST AN ENROLMENT RECORD IS CREATED IN DM_CERTIFICATION_ENROLMENT
INSERT INTO DM_CERTIFICATION_ENROLMENT
(CERTIFICATION_ENROLMENT_ID, ALLOCATED_DT, DEADLINE_DATE, STATUS, USER_ROLE_ID)
VALUES
(
v_cert_enrolment_id,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled',
v_user_role_id
);
--COURSES LINKED TO THE CERTIFICATION ARE INSERTED INTO DM_COURSE_ENROLMENT
INSERT INTO DM_COURSE_ENROLMENT
(
CERTIFICATION_ENROLMENT_ID,
COURSE_ID,
ALLOCATED_DT,
DEADLINE_DT,
STATUS
)
SELECT v_cert_enrolment_id,
COURSE.COURSE_ID,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled'
FROM DM_CERTIFICATION_COURSE COURSE
WHERE CERTIFICATION_ID =
(
SELECT C.CERTIFICATION_ID FROM
DM_CERTIFICATION_ENROLMENT A,
DM_USER_ROLE B,
DM_ROLE_CERTIFICATION C
WHERE
A.USER_ROLE_ID = B.USER_ROLE_ID
AND
B.ROLE_ID = C.ROLE_ID
AND
A.CERTIFICATION_ENROLMENT_ID = v_cert_enrolment_id
);
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SUBSTR(SQLERRM, 1, 2000));
end;
I need to populate 2 separate tables when an insert happens in this table, and I thought AFTER INSERT triggers avoided issues with mutating tables?
I am not sure what is causing it, perhaps the read in the second INSERT statement from DM_USER_ROLE, which is where this trigger is initiated...but I was under the impression AFTER INSERTs were safe to avoid mutations, as the update has already happened.
Error is:
ORA-04091: table AZLEARN_BACKUP.DM_USER_ROLE is mutating,
trigger/function may not see it
The first insert happens, the second one does not.
This article led me to believe AFTER triggers were safe.
http://www.dba-oracle.com/t_avoiding_mutating_table_error.htm
-------UPDATE---------------
I changed it to do row by row insert using two parameterised cursors and it worked...still not sure what the error was:
create or replace TRIGGER "DM_USER_ROLE_T1"
AFTER
insert on "DM_USER_ROLE"
for each row
DECLARE
v_cert_enrolment_id number;
v_user_role_id number;
v_role_id number;
v_certification_id number;
cursor certs_for_role(p_role_id number) is
select * from DM_ROLE_CERTIFICATION where ROLE_ID = p_role_id;
r_certs_for_role certs_for_role%rowtype;
cursor courses_for_certs(p_cert_id number) is
select * from DM_CERTIFICATION_COURSE where CERTIFICATION_ID = p_cert_id;
r_courses_for_certs courses_for_certs%rowtype;
begin
v_user_role_id := :new.USER_ROLE_ID;
v_role_id := :new.ROLE_ID;
open certs_for_role(v_role_id);
loop
fetch certs_for_role into r_certs_for_role;
exit when certs_for_role%notfound;
v_cert_enrolment_id := "DM_CERTIFICATION_ENROLMEN_SEQ".nextval;
INSERT INTO DM_CERTIFICATION_ENROLMENT
(CERTIFICATION_ENROLMENT_ID, ALLOCATED_DT, DEADLINE_DATE, STATUS, USER_ROLE_ID, CERTIFICATION_ID)
VALUES
(
v_cert_enrolment_id,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled',
v_user_role_id,
r_certs_for_role.CERTIFICATION_ID
);
open courses_for_certs(r_certs_for_role.CERTIFICATION_ID);
loop
fetch courses_for_certs into r_courses_for_certs;
exit when courses_for_certs%notfound;
INSERT INTO DM_COURSE_ENROLMENT
(
CERTIFICATION_ENROLMENT_ID,
COURSE_ID,
ALLOCATED_DT,
DEADLINE_DT,
STATUS
)
VALUES
(
v_cert_enrolment_id,
r_courses_for_certs.COURSE_ID,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled'
);
end loop;
close courses_for_certs;
end loop;
close certs_for_role;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SUBSTR(SQLERRM, 1, 2000));
end;

The reason is simply you cannot select from table DM_USER_ROLE where the ROW LEVEL trigger is based on.
In your first solution you have a
SELECT ...
FROM DM_USER_ROLE ...
This is not allowed. Your second trigger does not select table DM_USER_ROLE thus it is working.
The advise in linked page is correct but misleading when they state 'Use an "after" or "instead of" trigger' - It should be more precisely 'Use an "after statement" or "instead of" trigger' . Oracle provides triggers based on following actions:
DML statements (INSERT, UPDATE, DELETE) on a particular table or view
DDL statements (CREATE or ALTER primarily)
Database events, such as logon/logoff, errors, or startup/shutdown
You have a DML trigger which can have different timing points:
Before the triggering statement executes
After the triggering statement executes
Before each row that the triggering statement affects
After each row that the triggering statement affects
Compound Trigger -> this one combines the four triggers listed above
INSTEAD OF Trigger (only for views)
Many people miss the difference between a statement level trigger and a row level trigger.
Row Level Triggers have keyword FOR EACH ROW and run for each row like the keyword implies. If you skip the FOR EACH ROW keyword, then the trigger is executed only once for each statement, no matter how many rows are affected by your INSERT/UPDATE/DELETE statement.

The most likely cause of a mutating table error is the misuse of triggers. Here is a typical example:
1.you insert a row in table A
2.a trigger on table A (for each row) executes a query on table A, for example to compute a summary column
3.Oracle throws an ORA-04091: table A is mutating, trigger/function may not see it
This is an expected and normal behaviour, Oracle wants to protect you from yourself since Oracle guarantees:
•(i) that each statement is atomic (i.e will either fail or succeed
completely)
•(ii) that each statement sees a consistent view of the data
Now in your trigger, when you do the second insert, it's doing a join on the DM_USER_ROLE table to fetch the records and that's the reason you face
ORA-04091: table AZLEARN_BACKUP.DM_USER_ROLE is mutating,
trigger/function may not see it

Related

How to rollback a column and its trigger in plsql?

I have a litte task. Firstly I added a column in my table with specific constraints. Then I added a trigger for other jobs.
But I need a rollbacksql and have no idea what to proceed. Can anybody help or give an advice about it? I am adding my sql snippet.
ALTER TABLE FCBSADM.GL_DEF ADD GL_TP NUMBER;
ALTER TABLE FCBSADM.GL_DEF ADD CONSTRAINTS CH_COL CHECK (GL_TP between 1 and 10);
CREATE OR REPLACE TRIGGER GL_DEF_GL_TP_TRG
BEFORE INSERT OR
DELETE OR
UPDATE OF CDATE, CMPNY_DEF_ID, CUSER, DESCR, GL_DEF_ID, MNY_TP_ID, ST, UDATE, UUSER, GL_TP
ON FCBSADM.GL_DEF
FOR EACH ROW
DECLARE
cnt number := 0;
BEGIN
IF INSERTING
THEN
IF :NEW.GL_TP = 2
THEN
SELECT 1 into cnt from dual where exists( select *
FROM LOOKUP_GLCODE_IND_CEZA lookup
WHERE lookup.indirim_glcode = :NEW.gl_Def_id);
IF (cnt = 1) THEN
raise_application_error
(-20101, 'Please insert record into LOOKUP_GLCODE_IND_CEZA before inserting GL_DEF');
END IF;
END IF;
END IF;
END;
What do you want to rollback? Adding a column and creating a trigger? If so, drop them, both.
alter table gl_def drop column gl_tp;
drop trigger gl_def_gl_tp_trg;
A trigger and newly added table can be rolled back only by using drop and alter statements. This is ok if its being done inside a script that executes only a few times. But is highly inefficient if both drop and alter are called frequently for n number of records.

SQL Error: ORA-04091 while executing Trigger

I am trying to execute my trigger:
CREATE OR REPLACE TRIGGER Trg_video_bfr_delete
AFTER DELETE ON CMS_VIDEO
FOR EACH ROW
DECLARE
CODE varchar2(60);
BEGIN
SELECT CODE INTO CODE
from CMS_VIDEO
WHERE CODE = :OLD.CODE;
IF CODE IS NOT NULL THEN
INSERT INTO ASSET_DELETE_INDEX(CODE,ASSET_TYPE,IW_VPATH,LANGUAGE,MODIFIED_DTE)
VALUES (CODE,'Video',:old.IW_VPATH,:old.CONTENT_LANGUAGE,sysdate);
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
INSERT INTO ASSET_DELETE_INDEX (CODE,ASSET_TYPE,IW_VPATH,LANGUAGE,MODIFIED_DTE)
VALUES (CODE,'Video',null,:old.CONTENT_LANGUAGE,sysdate);
COMMIT;
END Trg_video_bfr_delete;
/
But I am getting the following error while executing a delete command on the table
Error report -
SQL Error: ORA-04091: table LSDS.CMS_VIDEO is mutating, trigger/function may not see it
ORA-06512: at "LSDS.TRG_VIDEO_BFR_DELETE", line 6
ORA-04088: error during execution of trigger 'LSDS.TRG_VIDEO_BFR_DELETE'
04091. 00000 - "table %s.%s is mutating, trigger/function may not see it"
*Cause: A trigger (or a user defined plsql function that is referenced in
this statement) attempted to look at (or modify) a table that was
in the middle of being modified by the statement which fired it.
*Action: Rewrite the trigger (or function) so it does not read that table.
Could anyone please help?
You have written trigger AFTER DELETE ON CMS_VIDEO. This trigger will fire when DELETE is performed from CMS_VIDEO table. So when CMS_VIDEO table is being modified, you cannot modify or query the same table in any of your triggers, procedures or functions.
CREATE OR REPLACE TRIGGER Trg_video_bfr_delete
AFTER DELETE ON CMS_VIDEO
FOR EACH ROW
BEGIN
IF :old.CODE IS NOT NULL THEN
INSERT INTO ASSET_DELETE_INDEX(CODE, ASSET_TYPE, IW_VPATH, LANGUAGE, MODIFIED_DTE)
VALUES (:old.CODE, 'Video', :old.IW_VPATH, :old.CONTENT_LANGUAGE, sysdate);
END IF;
END Trg_video_bfr_delete;
/
This is because you are deleting record from table and same table is used as reference inside trigger. This is case of "Mutating Error". You should switch to statement level trigger is it fits your requirement or you can use compound trigger which has multiple entry point and are capable of handling such scenarios.
Please refer https://oracle-base.com/articles/9i/mutating-table-exceptions
As per the snippet provided, I dont see any point in doing a SELECT on mutating TABLE.
We can easily ise :OLD.CODE to fetch the value. Hope below snippet helps.
CREATE OR REPLACE TRIGGER Trg_video_bfr_delete AFTER
DELETE ON CMS_VIDEO FOR EACH ROW
-- DECLARE CODE VARCHAR2(60);
BEGIN
-- SELECT CODE INTO CODE FROM CMS_VIDEO WHERE CODE = :OLD.CODE;
IF CODE IS NOT NULL THEN
INSERT
INTO ASSET_DELETE_INDEX
(
CODE,
ASSET_TYPE,
IW_VPATH,
LANGUAGE,
MODIFIED_DTE
)
VALUES
(
:old.code,
'Video',
:old.IW_VPATH,
:old.CONTENT_LANGUAGE,
sysdate
);
END IF;
EXCEPTION
WHEN OTHERS THEN
INSERT
INTO ASSET_DELETE_INDEX
(
CODE,
ASSET_TYPE,
IW_VPATH,
LANGUAGE,
MODIFIED_DTE
)
VALUES
(
:old.code,
'Video',
NULL,
:old.CONTENT_LANGUAGE,
sysdate
);
COMMIT;
END Trg_video_bfr_delete;

trigger after insert on table

create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
declare
nmarcacaoa number;
ncartaoa number;
begin
select nmarcacao into nmarcacaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
select ncartao into ncartaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
insert_pagamentos(nmarcacaoa, ncartaoa); --this is a procedure
exception when others then
raise_application_error(-20001, 'Error in Trigger!!!');
end addpagamento;
when i try to run the insert statement to the table "marcacoes_refeicoes" this procedure gives error: like the table is mutating
create or replace
procedure insert_pagamentos
(nmarcacaoa in number, ncartaoa in number)
AS
BEGIN
insert into pagamentos (nmarcacao, datapagamento, ncartao) values (nmarcacaoa, sysdate, ncartaoa);
commit;
END INSERT_PAGAMENTOS;
Short (oversimplified) answer:
You can't modify a table in a trigger that changes the table.
Long answer:
http://www.oracle-base.com/articles/9i/mutating-table-exceptions.php has a more in-depth explanation, including suggestions how to work around the problem.
You're hitting the mutating-table problem because you're selecting from the same table the trigger is on, but what you seem to be trying to do doesn't make sense. Your queries to get a value for nmarcacaoa and ncartaoa will return a no_data_found or too_many_rows error unless the table had exactly 2 rows in it before your insert:
select nmarcacao from marcacoes_refeicoes
where rownum < (select count(*) from marcacoes_refeicoes);
will return all rows except one; and the one that's excluded will be kind of random as you have no ordering. Though the state is undetermined within the trigger, so you can't really how many rows there are, and it won't let you do this query anyway. You won't normally be able to get a single value, anyway, and it's not obvious which value you actually want.
I can only imagine that you're trying to use the values from the row you are currently inserting, and putting them in your separate payments (pagamentos) table. If so there is a built-in mechanism to do that using correlation names, which lets you refer to the newly inserted row as :new (by default):
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
begin
insert_pagamentos(:new.nmarcacaoa, :new.ncartaoa);
end addpagamento;
The :new is referring to the current row, so :new.nmarcacaoa is the nmarcacaoa being inserted. You don't need to (and can't) get that value from the table itself. (Even with the suggested autonomous pragma, that would be a separate transaction and would not be able to see your newly inserted and uncommitted data).
Or you can just do the insert directly; not sure what the procedure is adding here really:
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
begin
insert into pagamentos(nmarcacao, datapagamento, ncartao)
values (:new.nmarcacaoa, sysdate, :new.ncartaoa);
end addpagamento;
I've removed the exception handler as all it was doing was masking the real error, which is rather unhelpful.
Editing this answer in view of the comments below:
You can use PRAGMA AUTONOMOUS_TRANSACTION to get rid of the error, but DONOT USE it as it will NOT solve any purpose.
Use PRAGMA AUTONOMOUS_TRANSACTION:
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
declare
PRAGMA AUTONOMOUS_TRANSACTION;
nmarcacaoa number;
ncartaoa number;
begin
select nmarcacao into nmarcacaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
select ncartao into ncartaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
insert_pagamentos(nmarcacaoa, ncartaoa); --this is a procedure
exception when others then
raise_application_error(-20001, 'Error in Trigger!!!');
end addpagamento;
However in this case , select count(*) from marcacoes_refeicoes will give you the new count after the current insertion into the table.

Trigger for insert after

I need to insert a 'regist' in Historic data table , when I insert something in 'ALUGUER' the trigger should insert one row in 'HISTORICO':
My Trigger:
create or replace
trigger ADD_HISTORICO
AFTER INSERT
ON ALUGUER
FOR EACH ROW
DECLARE
cod_aluguer_p NUMBER(6,0);
cod_veiculo_p NUMBER(6,0);
BEGIN
SELECT ID_ALUGUER INTO cod_aluguer_p
FROM ALUGUER;
SELECT COD_VEICULO INTO cod_veiculo_p
FROM ALUGUER;
INSERT INTO HISTORICO(ID_ENTRADA,DESCRICAO,DATA_REGISTO,NOVO_VEICULO,NOVO_ALUGUER)
VALUES(SEQ_HISTORICO.nextval,'NOVA DESCRIÇÃO','21/11/2013',cod_veiculo_p,cod_aluguer_p);
END;
Error report:
SQL Error:
ORA-04091: BDDAD_DL1.ALUGUER table is mutating, trigger can
not read or modify
ORA-06512: at "BDDAD_DL1.ADD_HISTORICO", line 6
ORA-04088: error during execution of trigger 'BDDAD_DL1.ADD_HISTORICO'
04091. 00000 -. "Table% s% s is mutating, trigger / function may not see it"
* Cause: A trigger (or a user defined plsql function that is referenced in
            this statement) attempted to look at (or modify) a table que was
            in the middle of being modified by the statement Which fired it.
* Action: Rewrite the trigger (or function) so it does not read that table.
You are trying to read a record from the table that the trigger has fired on. This is a no-no. So, this code:
SELECT ID_ALUGUER INTO cod_aluguer_p
FROM ALUGUER;
SELECT COD_VEICULO INTO cod_veiculo_p
FROM ALUGUER;
is not allowed. Also, it makes no sense, as there is no WHERE clause on the select, so
all rows would be returned. Moot point, as you can't do this anyway. What you want to do is reference the :NEW values of the triggered row istead. Example:
cod_aluguer_p := :new.ID_ALUGUER ;
cod_veiculo_p L= :NEW.COD_VEICULO;
Further, you don't even needs those local variables, and just use :new directly.
INSERT INTO HISTORICO(ID_ENTRADA,DESCRICAO,DATA_REGISTO,NOVO_VEICULO,NOVO_ALUGUER)
VALUES(SEQ_HISTORICO.nextval,'NOVA DESCRIÇÃO','21/11/2013',:NEW.COD_VEICULO,:new.ID_ALUGUER );
I would recommend to read the Oracle docs on triggers
No SELECT is required, just refer to the :NEW values in the trigger. Also, you can use TRUNC(SYSDATE) to get the current date assuming that's what you intended:
create or replace
trigger ADD_HISTORICO
AFTER INSERT
ON ALUGUER
FOR EACH ROW
BEGIN
INSERT INTO HISTORICO(ID_ENTRADA,DESCRICAO,DATA_REGISTO,
NOVO_VEICULO,NOVO_ALUGUER)
VALUES(SEQ_HISTORICO.nextval,'NOVA DESCRIÇÃO',
TRUNC(SYSDATE),:NEW.COD_VEICULO ,:NEW.ID_ALUGUER );
END;

Create trigger to insert into another table

I have some problem executing the trigger below:
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varReadNo Int;
varMeterID Int;
varCustID Varchar(10);
BEGIN
SELECT SeqReadNo.CurrVal INTO varReadNo FROM DUAL;
Select MeterID INTO varMeterID
From Reading
Where ReadNo = varReadNo;
Select CustID INTO varCustID
From Address A
Join Meter M
on A.postCode = M.postCode
Where M.MeterID = varMeterID;
INSERT INTO BILL VALUES
(SEQBILLNO.NEXTVAL, SYSDATE, 'UNPAID' , 100 , varCustID , SEQREADNO.CURRVAL);
END;
Error Message:
*Cause: A trigger (or a user defined plsql function that is referenced in
this statement) attempted to look at (or modify) a table that was
in the middle of being modified by the statement which fired it.
*Action: Rewrite the trigger (or function) so it does not read that table.
Does it mean that I am not suppose to retrieve any details from table Reading?
I believe that the issue occur as I retrieving data from Table Reading while it is inserting the value:
SELECT SeqReadNo.CurrVal INTO varReadNo FROM DUAL;
Select MeterID INTO varMeterID
From Reading
Where ReadNo = varReadNo;
It's there anyway to resolve this? Or is there a better method to do this? I need to insert a new row into table BILL after entering the table Reading where the ReadNo need to reference to the ReadNo I just insert.
You cannot retrieve records from the same table in a row trigger. You can access values from actual record using :new and :old (is this your case?). The trigger could then be rewritten to
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varCustID Varchar(10);
BEGIN
Select CustID INTO varCustID
From Address A
Join Meter M
on A.postCode = M.postCode
Where M.MeterID = :new.MeterID;
INSERT INTO BILL VALUES
(SEQBILLNO.NEXTVAL, SYSDATE, 'UNPAID' , 100 , varCustID , SEQREADNO.CURRVAL);
END;
If you need to query other record from READING table you have to use a combination of statement triggers, row trigger and a PLSQL collection. Good example of this is on AskTom.oracle.com
Make sure that you have the necessary permissions on all the tables and access to the sequences you're using in the insert.
I haven't done Oracle in awhile, but you can also try querying dba_errors (or all_errors) in order to try and get more information on why your SP isn't compiling.
Something to the tune of:
SELECT * FROM dba_errors WHERE owner = 'THEOWNER_OF_YOUR_SP';
Add exception handling in your trigger and see what is happening, by doing it would be easy for you to track the exceptions.
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varCustID Varchar(10);
BEGIN
-- your code
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20298)));
END;
we can combined insert and select statement
CREATE OR REPLACE TRIGGER AFTERINSERTCREATEBILL
AFTER INSERT
ON READING
FOR EACH ROW
DECLARE
varCustID Varchar(10);
BEGIN
insert into bill
values
select SEQBILLNO.NEXTVAL,
SYSDATE,
'UNPAID' ,
100 ,
CustID,SEQREADNO.CURRVAL
From Address A
Join Meter M
on A.postCode = M.postCode
Where M.MeterID = :new.MeterID;
END;
try the above code.

Resources