Trigger wont release locks - oracle

I am working on a project that will be replacing an existing older project. On one side I am taking data from external views and on the other I am taking the existing production data. The data between the two systems was supposed to have been in-sync, but they werent at any meaningful level. For the most part this has not been a huge issue as I have largely merged them where possible. In some cases the rows were matched on employee id's, and in others using only surname and birthdate.
The external view data always has two pieces of distinct information, an employee ID and another identifier. The Production data will always have one distinct ID and thats file_number. Sometimes production will have the external view keys, but this is not normal. These keys are the primary keys on either side, not surname and birthdate.
Its this last comparison which has caused the issue as we have several requirements where we are only allowed to join on this criteria, such as when the employee exists in the production side gets an entry on the external views. As surname and birthdate are not particularly distinct, I created an exclusion table for these records that would otherwise cause issues but are valid (twins for example). The exclusion table takes all of the offending records from both sides.
As I was unable to figure out a way to get some kind of constraint where these duplicate records would automatically get entered into the exclusion table (have poor control over data entry), I turned to triggers.
Error
Error report -
SQL Error: ORA-04021: timeout occurred while waiting to lock object
ORA-06512: at "USER.EXCLUSION_TRG", line 4
ORA-04088: error during execution of trigger 'USER.EXCLUSION_TRG'
ORA-06512: at "USER.VIEW_DUPLICATE_TRG", line 4
ORA-04088: error during execution of trigger 'USER.VIEW_DUPLICATE_TRG'
04021. 00000 - "timeout occurred while waiting to lock object %s%s%s%s%s"
*Cause: While waiting to lock a library object, a timeout is occurred.
*Action: Retry the operation later.
First trigger for VIEW Table
CREATE OR REPLACE TRIGGER VIEW_DUPLICATE_TRG
AFTER INSERT OR UPDATE ON VIEW_PERSON
BEGIN
INSERT INTO VIEW_EXCLUSION_PERSON (EMPLID, PRI, COMMENTS)
select emplid, PRI, 'VIEW CREATED '||SYSDATE from (
select upper(CONVERT(last_name, 'US7ASCII')) LAST_NAME, birthdate,first_name,emplid, pri, count(*) over (partition by upper(CONVERT(last_name, 'US7ASCII')), birthdate) duplicate_count from VIEW_PERSON
) K where duplicate_count > 1
and NOT EXISTS (select emplid from exclusion_person Z WHERE K.EMPLID=Z.EMPLID);
END;
Second trigger for Prod table
CREATE OR REPLACE TRIGGER PROD_DUPLICATE_TRG
AFTER INSERT OR UPDATE ON BACKGROUND_INFO
BEGIN
INSERT INTO EXCLUSION_PERSON (FILE_NUMBER, COMMENTS)
SELECT FILE_NUMBER, 'PROD CREATED '||SYSDATE
FROM BACKGROUND_INFO
WHERE (SURNAME, BIRTHDATE) IN
(SELECT SURNAME, BIRTHDATE
FROM BACKGROUND_INFO
GROUP BY SURNAME, BIRTHDATE
HAVING COUNT(*) > 1
)
AND FILE_NUMBER NOT IN (SELECT FILE_NUMBER FROM exclusion_person WHERE FILE_NUMBER IS NOT NULL);
END;
Third trigger for Exclusion table
CREATE OR REPLACE TRIGGER EXCLUSION_TRG
AFTER INSERT ON EXCLUSION_PERSON
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE 'ALTER TRIGGER EXCLUSION_TRG DISABLE';
merge into EXCLUSION_PERSON E
using (select file_number, DECODE(TRIM(PRI), '99999999', NULL, PRI) PRI from administrative_info) A
on (E.PRI=A.PRI)
when matched then update set E.file_number = A.file_number, E.COMMENTS = E.COMMENTS||', MATCHED ON PRI ON '||SYSDATE
WHERE E.FILE_NUMBER IS NULL AND E.PRI IS NOT NULL AND A.PRI IS NOT NULL;
MERGE INTO VIEW_EXCLUSION_PERSON E
USING (SELECT FILE_NUMBER, EMPLID FROM VIEW_PERSON) P
ON (P.EMPLID = E.EMPLID)
WHEN MATCHED THEN UPDATE SET
E.FILE_NUMBER = P.FILE_NUMBER,
E.COMMENTS = E.COMMENTS||' MATCHED FROM PERSON '||SYSDATE
WHERE E.FILE_NUMBER IS NULL AND E.EMPLID IS NOT NULL AND P.FILE_NUMBER IS NOT NULL;
EXECUTE IMMEDIATE 'ALTER TRIGGER VIEW_DUPLICATE_TRG DISABLE';
MERGE INTO VIEW_PERSON P
USING (SELECT FILE_NUMBER, EMPLID FROM EXCLUSION_PERSON ) E
ON (P.EMPLID = E.EMPLID)
WHEN MATCHED THEN UPDATE SET P.FILE_NUMBER = E.FILE_NUMBER
WHERE P.FILE_NUMBER IS NULL AND E.EMPLID IS NOT NULL AND E.FILE_NUMBER IS NOT NULL;
EXECUTE IMMEDIATE 'ALTER TRIGGER VIEW_DUPLICATE_TRG ENABLE';
EXECUTE IMMEDIATE 'ALTER TRIGGER EXCLUSION_TRG ENABLE';
END;
So the issue seems to be that the first trigger VIEW_DUPLICATER_TRG is locking something and not releasing that lock when EXCLUSION_TRG is attempting to run. When I go looking for this %S%S%S%S%S object, I just cant find it, and none of my code is calling this object.

The major issue is here:
EXECUTE IMMEDIATE 'ALTER TRIGGER EXCLUSION_TRG DISABLE';
This tries to disable the trigger while the trigger itself is executing!
This will never work: you can't disable a trigger while it's executing. So to try to change its state from within its own execution will be blocked.
You can see this with the following:
create table t (
c1 int
);
create or replace trigger trg
after insert on t
declare
pragma autonomous_transaction;
begin
execute immediate 'alter trigger trg disable';
end;
/
insert into t values ( 1 );
The insert will be stuck, waiting to try and disable the trigger. But the trigger is executing. So you can't disable it. AAAAARGGGH!
This whole process needs redesigning. Ideally without any triggers!

Related

Is there any way we can skip the account which is raising exception i.e. duplicate entry of that account and other account get inserted in oracle?

Suppose we have a table which consist number of records for the user accounts. Now we are running a procedure which is picking accounts from 1 system to source i.e. oracle. We have added constraints to the table in oracle for ID which should be unique. In that procedure we have added an exception which will throw the error when unique constraint is violated everytime and procedure will fail.
Now my question is- Is there any way procedure can skip that account which is already present in the source table which is causing exception and rest insert go fine and procedure will be completed successfully?
I try to raise the exception but procedure is getting failed after raising the exception for unique key constraint error.
You didn't explain how exactly you're doing it, so I'll presume it is some kind of a cursor FOR loop which fetches user accounts from source table and inserts them into a target table.
Use inner (within the loop) begin-exception-end block which will handle exception (basically, you'll just ignore it) and proceed with another user account.
Something like this:
begin
for cur_r in (select user_account, ... from source_table) loop
-- start inner BEGIN-EXCEPTION-END block
begin
insert into target_table (...) values (cur_r.user_account, ...);
-- handle exception
exception
when dup_val_on_index then
-- ignore it
null;
end;
-- end innser BEGIN-EXCEPTION-END block
end loop;
end;
On the other hand, can't you avoid duplicates in query?
for cur_r in (select user_account, ...
from source_table s
where not exists (select null
from target_table t
where t.user_account = s.user_account
)
) loop
[EDIT] You commented that you're either updating or inserting values - that's actually what merge (also known as upsert) does:
merge into target_table a
using source_table b
on (a.user_account = b.user_account)
when matched then update set
a.name = b.name,
a.address = b.address
when not matched then insert (user_account, name, address)
values (b.user_accout, b.name, b.address);
You can also use DBMS_ERRLOG package to create a table where the errors will be logged, there is an example in LiveSQL: https://livesql.oracle.com/apex/livesql/file/content_JNEICX6W0LNOA88CQIXO9A22A.html and discussion in AskTom https://asktom.oracle.com/pls/apex/asktom.search?tag=error-logging-using-log-errors-into

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.

How to write triggers to enforce business rules?

I want to create triggers for practicing PL/SQL and I sorta got stuck with these two ones, which I'm sure they're simple, but I can't get a hold of this code.
The first trigger forbids an employee to have a salary higher than the 80% of their boss (The code is incomplete because I don't know how to continue):
CREATE OR REPLACE TRIGGER MAX_SALARY
BEFORE INSERT ON EMP
FOR EACH ROW
P.BOSS EMP.JOB%TYPE := 'BOSS'
P.SALARY EMP.SAL%TYPE
BEGIN
SELECT SAL FROM EMP
WHERE
JOB != P.BOSS
...
And the second one, there must not be less than two employees per department
CREATE TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE EMPNO
EMPLOYEES NUMBER(2,0);
BEGIN
SELECT COUNT(EMPNO)INTO EMPLOYEES FROM EMP
WHERE DEPTNO = DEPT.DEPTNO;
IF EMPLOYEES < 2 THEN
DBMS_OUTPUT.PUT_LINE('There cannot be less than two employees per department');
END IF;
END;
I really don't know If I'm actually getting closer or far from it altogether...
which I'm sure they're simple
Actually these tasks are not simple for triggers. The business logic is simple, and the SQL to execute the business logic is simple, but implementing it in triggers is hard. To understand why you need to understand how triggers work.
Triggers fire as part of a transaction, which means they are are applied to the outcome of a SQL statement such as an insert or an update. There are two types of triggers, row level and statement level triggers.
Row-level triggers fire once for every row in the result set we can reference values in the current row, which is useful for evaluating row-level rules.. But we cannot execute DML against the owning table: Oracle hurls ORA-04088 mutating table exception, because such actions violate transactional integrity.
Statement level triggers fire exactly once per statement. Consequently they are useful for enforcing table-level rules but crucially they have no access to the result set, which means they don’t know which records have been affected by the DML.
Both your business rules are table level rules, as they require the evaluation of more than one EMP record. So, can we enforce them through triggers? Let’s start with the second rule:
there must not be less than two employees per department
We could implement this with an trigger AFTER statement trigger like this:
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select * from dept) loop
SELECT COUNT(EMPNO) INTO EMPLOYEES
FROM EMP
where i.DEPTNO = EMP.DEPTNO;
IF EMPLOYEES < 2 THEN
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
END IF;
end loop;
END;
/
Note this trigger uses RAISE_APPLICATION_ERROR() instead of DBMS_OUTPUT.PUT_LINE(). Raising an actual exception is always the best approach: messages can be ignored but exceptions must be handled.
The problem with this approach is that it will fail any update or delete of any employee, because the classic SCOTT.DEPT table has a record DEPTNO=40 which has no child records in EMP. So maybe we can be cool with departments which have zero employees but not with those which have just one?
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
This will enforce the rule. Unless of course somebody tries to insert one employee into department 40:
insert into emp
values( 2323, 'APC', ‘DEVELOPER', 7839, sysdate, 4200, null, 40 )
/
We can commit this. It will succeed because our trigger doesn’t fire on insert. But some other user’s update will subsequently fail. Which is obviously bobbins. So we need to include INSERT in the trigger actions.
CREATE or replace TRIGGER MIN_LIMIT
AFTER INSERT or DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
Unfortunately now we cannot insert one employee in department 40:
ORA-20042: problem with dept #40. There cannot be less than two employees per department
ORA-06512: at "APC.MIN_LIMIT", line 10
ORA-06512: at "SYS.DBMS_SQL", line 1721
We need to insert two employees in a single statement:
insert into emp
select 2323, 'APC', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual union all
select 2324, 'ANGEL', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual
/
Note that switching existing employees to a new department has the same limitation: we have to update at least two employees in the same statement.
The other problem is that the trigger may perform badly, because we have to query the whole table after every statement. Perhaps we can do better? Yes. A compound trigger (Oracle 11g and later) allows us to track the affected records for use in a statement level AFTER trigger. Let’s see how we can use one to implement the first rule
No employee can have a salary higher than the 80% of their boss
Compound triggers are highly neat. They allow us to share program constructs across all the events of the trigger. This means we can store the values from row-level events in a collection, which we can use to drive some SQL in an statement level AFTER code..
So this trigger fires on three events. Before a SQL statement is processed we initialise a collection which uses the projection of the EMP table. The code before row stashes the pertinent values from the current row, if the employee has a manager. (Obviously the rule cannot apply to President King who has no boss). The after code loops through the stashed values, looks up the salary of the pertinent manager and evaluates the employee's new salary against their boss's salary.
CREATE OR REPLACE TRIGGER MAX_SALARY
FOR INSERT OR UPDATE ON EMP
COMPOUND TRIGGER
type emp_array is table of emp%rowtype index by simple_integer;
emps_nt emp_array ;
v_idx simple_integer := 0;
BEFORE STATEMENT IS
BEGIN
emps_nt := new emp_array();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
v_idx := v_idx + 1;
if :new.mgr is not null then
emps_nt(v_idx).empno := :new.empno;
emps_nt(v_idx).mgr := :new.mgr;
emps_nt(v_idx).sal := :new.sal;
end if;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
null;
END AFTER EACH ROW;
AFTER STATEMENT IS
mgr_sal emp.sal%type;
BEGIN
for i in emps_nt.first() .. emps_nt.last() loop
select sal into mgr_sal
from emp
where emp.empno = emps_nt(i).mgr;
if emps_nt(i).sal > (mgr_sal * 0.8) then
raise_application_error(-20024, 'salary of empno ' || emps_nt(i).empno || ' is too high!');
end if;
end loop;
END AFTER STATEMENT;
END;
/
This code will check every employee if the update is universal, for instance when everybody gets a 20% pay rise...
update emp
set sal = sal * 1.2
/
But if we only update a subset of the EMP table it only checks the boss records it needs to:
update emp set sal = sal * 1.2
where deptno = 20
/
This makes it more efficient than the previous trigger. We could re-write trigger MIN_LIMIT as a compound trigger; that is left as an exercise for the reader :)
Likewise, each trigger fails as soon as a single violating row is found:
ORA-20024: salary of empno 7902 is too high!
ORA-06512: at "APC.MAX_SALARY", line 36
It would be possible to evaluate all affected rows, stash the violating row(s) in another collection then display all the rows in the collection. Another exercise for the reader.
Finally, note that having two triggers fire on the same event on the same table is not good practice. It's generally better (more efficient, easier to debug) to have one trigger which does everything.
An after thought. What happens to Rule #1 if one session increases the salary of an employee whilst simultaneously another session decreases the salary of the boss? The trigger will pass both updates but we can end up with a violation of the rule. This is an inevitable consequence of the way triggers work with Oracle's read-commit transaction consistency. There is no way to avoid it except by employing a pessimistic locking strategy and pre-emptively locking all the rows which might be affected by a change. That may not scale and is definitely hard to implement using pure SQL: it needs stored procedures. This is another reason why triggers are not good for enforcing business rules.
I'm using Oracle10g
That is unfortunate. Oracle 10g has been obsolete for almost a decade now. Even 11g is deprecated. However, if you really have no option but to stick with 10g you have a couple of options.
The first is to grind through the whole table, doing the lookups of each boss for every employee. This is just about bearable for a toy table such as EMP but likely to be a performance disaster in real life.
The better option is to fake compound triggers using the same workaround we all used to apply: write a package. We rely on global variables - collections - to maintain state across calls to packaged procedures, and have different triggers to make those calls. Basically you need one procedure call for each trigger and one trigger for each step in the compound trigger. #JustinCave posted an example of how to do this on another question; it should be simple to translate my code above to his template.
Please handle these kind of validations/business logic at application or at DB level using procedures/functions instead of using triggers which most of the times slows down the DML operations/statements on which the triggers are based.
If you handle business logic at application or procedure level then, the DB server will have to execute only DML statements; it does not have to execute TRIGGER-executing trigger involves handling exceptions; prior to that DML statement will place a lock on the table on which DML (except for INSERT statement-Exclusive shared lock) is being executed until TRIGGER is executed.

How can I get the inserted primary key value from AFTER INSERT trigger in Oracle?

My Oracle DB has a table DOC_WF_COMM and its primary key is DWFC_ID. Primary key value is based on a sequence called SQ_DOC_WF_COMM.
I have created a row level AFTER INSERT trigger on that table and inside the trigger I need to join the inserted record with some other tables like this:
create or replace TRIGGER TRG_DOC_WF_COMM_AFT_INS AFTER INSERT ON DOC_WF_COMM REFERENCING OLD AS OLD NEW AS NEW FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
L_SUBJECT VARCHAR2(300);
L_BODY CLOB;
L_PNT_CODE VARCHAR(100) := NULL;
L_DR_PRJ_ID NUMBER(12);
L_STR_EMAIL VARCHAR2(120);
L_DWFC_TO_USR_ID VARCHAR2(12);
L_PNT_ID NUMBER(12);
L_PNT_EMAIL_YN VARCHAR(1);
L_PNT_ACTIVE_YN VARCHAR(1);
L_PNT_NOTIFY_YN VARCHAR(1);
BEGIN
IF INSERTING THEN
L_PNT_CODE := 'WFNT_MESSAGE';
SELECT DR_PRJ_ID, STR_EMAIL, DWFC_TO_USR_ID INTO L_DR_PRJ_ID, L_STR_EMAIL, L_DWFC_TO_USR_ID
FROM DOC_WF_COMM
JOIN DOC_WF_USERS ON DWFU_ID = DWFC_DWFU_ID
JOIN DOC_WORKFLOW ON DWF_ID = DWFU_DWF_ID
JOIN DOCUMENT_REF ON DR_ID = DWF_DR_ID
JOIN ST_REGISTER ON STR_ID = DWFU_STR_ID
WHERE DWFC_ID = :NEW.DWFC_ID AND DWFC_RESPONSE IS NULL;
-- SOME QUERIES HERE
END IF;
END;
The trigger is compiled successfully and when I insert record into DOC_WF_COMM table I get this error:
ORA-01403: no data found ORA-06512
The error is :NEW.DWFC_ID in WHERE clause and I have change it to these values:
:OLD.DWFC_ID
SQ_DOC_WF_COMM.NEXTVAL
SQ_DOC_WF_COMM.CURRVAL
But no any luck. Any idea why this error is and how can I resolve it?
The problem is this line in your trigger:
PRAGMA AUTONOMOUS_TRANSACTION;
That means the trigger executes as an isolated transaction in a separate session, which means it cannot see the uncommitted state of any other session. Crucially this includes the session which fires the trigger, so the autonomous transaction cannot see the record you just inserted. Hence, NO_DATA_FOUND.
You haven't posted the whole trigger or explained what you're trying to do, so only you know why you have included the PRAGMA. However, the chances are you don't need it. Remove the PRAGMA (and the COMMIT) and your trigger should work just fine.
If I understood you correctly, create a local variable and put the next sequence value in there. Then it can be referenced throughout the code, always having the same value. Something like this:
declare
l_seq number := my_seq.nextval;
begin
insert into table_a (id, ...) values (l_seq, ...);
update table_b set id = l_seq where ...
select ... into ... from ... where id = l_seq;
end;
I changed the query inside the trigger to this, it is working fine
SELECT STR_PRJ_ID, STR_EMAIL, :NEW.DWFC_TO_USR_ID INTO L_DR_PRJ_ID, L_STR_EMAIL, L_DWFC_TO_USR_ID
FROM DOC_WF_USERS, ST_REGISTER
WHERE :NEW.DWFC_TO_USR_ID = DWFU_US_ID AND DWFU_STR_ID = STR_ID AND DWFU_ID = :NEW.DWFC_DWFU_ID;
Not sure why is that. If anyone can figure out the mistake in the query given in the question, please let me know. Thanks

ORA-04091 Mutating Table error during AFTER INSERT Trigger

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

Resources