java.sql.BatchUpdateException: ORA-04091 on BEFORE INSERT TRIGGER - oracle

I'm getting a curious error on a BEFORE INSERT TRIGGER, which I can't understand. Even after reading multiple questions posted here with similar problems.
failed to process "method": category_id = 'foo' and request_id = '99'
error: java.sql.BatchUpdateException: ORA-04091: table SCHEMA.ANIMAL_TABLE
is mutating, trigger/function may not see it ORA-06512: at
"SCHEMA.TRIGGER_NAME", line 7 ORA-04088: error during execution of
trigger 'SCHEMA.TRIGGER_NAME'
Here is the trigger:
CREATE OR REPLACE TRIGGER TRIGGER_NAME
BEFORE INSERT ON animal_table FOR EACH ROW WHEN (NEW.animal_type = 'cats')
DECLARE base_animal_id NUMBER(19,0); base_amount NUMBER(19,0);
BEGIN
SELECT animal_nbr INTO base_animal_id
FROM animal_table
WHERE category_id = :NEW.category_id AND summary_id = :NEW.summary_id
AND animal_type = 'special';
SELECT animal_amount INTO base_amount
FROM animal_table
WHERE category_id = :NEW.category_id AND summary_id = :NEW.summary_id
AND animal_type = 'special';
IF :NEW.category_id = 'foo' THEN
:NEW.animal_info1 := base_animal_id;
:NEW.animal_info2 := base_amount;
:NEW.animal_info3 := '00';
END IF;
END;
I know the rules regarding modifications on the same table which the trigger is being held, but I also red something that it should work when changing new columns and only for the :NEW fields. I also thought it may be missing the UPDATE as trigger event, but that was not the case. Can anyone help me please? As I am new to triggers and PL/SQL.

The error message has nothing to do with updating the table. You cannot SELECT from the table that is currently being changed in a ROW level trigger.
The only workaround for this is to write the new rows into a intermediate table in the row level trigger. Then create a statement level trigger that processes all rows that have been written into the intermediate table (most probably only a single UPDATE statement with a subselect).
You might get away without the row level trigger and the intermediate table if you can identify the rows to be post-processed inside the statement level trigger e.g. by checking animal_type = 'cats' and category_id = 'foo'.
If that is the case, the following trigger (untested!!!) might do what you want (instead of the one you have)
CREATE OR REPLACE TRIGGER TRIGGER_NAME
AFTER INSERT ON animal_table
BEGIN
UPDATE animal_table
SET (animal_info1,
animal_info2,
animal_info3) = (SELECT animal_nbr, animal_amount, '00'
FROM animal_table t2
WHERE t2.category_id = animal_table.category_id
AND t2.sumary_id = animal_table.summary_id
AND t2.animal_type = 'special'
)
WHERE animal_type = 'cats'
AND category_id = 'foo'
END;
Another more general PL/SQL thing: You don't need to run one SELECT for each column you want to retrieve, you can do that in a single select statement if the conditions are the same:
SELECT animal_nbr, animal_amount
INTO base_animal_id, base_amount
FROM animal_table
WHERE category_id = :NEW.category_id
AND summary_id = :NEW.summary_id
AND animal_type = 'special';
(Note the two columns in the select and into list)

Related

i made trigger to update data when people make action in form

trigger code good but when make action this error appear
error ORA-04091: table OR_HR.SELL is mutating, trigger/function may not see it.
trigger code
create or replace TRIGGER "QUALITY_EDIT"
AFTER INSERT OR UPDATE ON Sell
FOR EACH ROW
BEGIN
UPDATE DRUG
SET QUANTITY =
(SELECT (DRUG.QUANTITY - SELL.QUANTITY ) FROM Sell
JOIN Drug
ON SELL.DRUG_ID = DRUG.DRUG_ID) ;
END;
How can i solve this problem?
You can't select from table which is just being updated (or inserted into), it is mutating and trigger can't see it.
Lucky you, you don't have to select from sell, use something like this (the :new pseudorecord) instead:
create or replace trigger quality_edit
after insert or update on sell
for each row
begin
update drug d set
d.quantity = d.quantity - :new.quantity
where d.drug_id = :new.drug_id;
end;
/

SELECT CASE to use NOT EXISTS in Oracle failing

I have been trying to find a solution to use an If_Exists() style statement in Oracle PL SQL. I am trying to create a trigger which checks to see if a certain airsoft gun exists in the guns table when a member tries to input a new gun owned in the gunsOwned table. If the gun does not exist in the guns table, then it must be inputted to the table before the gun owned is inputted to the gunsOwned table or it will violate referential integrity as the Make and Model in gunsOwned are foreign keys to the Make and Model in the Guns table. However I keep getting Trigger created with compilation errors, and all of my attribute names are correct, so don't know why the select case statement is not working. Here is the code:
CREATE TRIGGER updateGuns
BEFORE INSERT ON GunsOwned
FOR EACH ROW
DECLARE
MemberAddingGun NUMBER;
NewMake VARCHAR2(30);
NewModel VARCHAR2(30);
BEGIN
MemberAddingGun := :NEW.OwnerID;
NewMake := :NEW.MakeOwned;
NewModel := :NEW.ModelOwned;
SELECT CASE gunExists
WHEN NOT EXISTS(SELECT Make, Model FROM Guns WHERE Make=NewMake AND Model=NewModel)
THEN
INSERT INTO Guns VALUES(NewMake, NewModel);
END
UPDATE Member
SET NumOfGuns = NumOfGuns+1
WHERE MemberID = MemberAddingGun;
END updateGuns;
.
RUN;
Could anyone help?
Thanks!
Use simple INSERT ... SELECT ... WHERE instead of CASE or IF statements:
INSERT INTO Guns( colname1, colname2 )
SELECT NewMake, NewModel FROM dual
WHERE NOT EXISTS(
SELECT null FROM Guns WHERE Make=NewMake AND Model=NewModel
);
BTW - on multiuser environment checking for not-existence of a record will always fail, since not commited records are not visible to SQL, and you will get duplicate records in Guns table.
In such a case you need some kind of synchronization.
There are a couple of options. First, you can handle this using a MERGE statement:
CREATE TRIGGER updateGuns
BEFORE INSERT ON GunsOwned
FOR EACH ROW
BEGIN
MERGE INTO GUNS
USING (SELECT MAKE, MODEL FROM GUNS) g
ON (g.MAKE = :NEW.MAKEOWNED AND g.MODEL = :NEW.MODELOWNED)
WHEN NOT MATCHED THEN
INSERT (MAKE, MODEL)
VALUES (:NEW.MAKEOWNED, :NEW.MODELOWNED);
UPDATE Member
SET NumOfGuns = NumOfGuns+1
WHERE MemberID = :NEW.OWNERID;
END UPDATEGUNS;
In this case the MERGE acts as a conditional INSERT, only adding a new row to GUNS if the specified make and model don't already exist in the table.
Alternatively, assuming that MAKE and MODEL are either the primary key or are a unique key on GUNS you can just go ahead and do the INSERT, trap the DUP_VAL_ON_INDEX exception thrown if a duplicate is found, and proceed merrily on your way:
CREATE TRIGGER updateGuns
BEFORE INSERT ON GunsOwned
FOR EACH ROW
BEGIN
BEGIN
INSERT INTO GUNS
(MAKE, MODEL)
VALUES
VALUES (:NEW.MAKEOWNED, :NEW.MODELOWNED);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
NULL; -- ignore the DUP_VAL_ON_INDEX exception
END;
UPDATE Member
SET NumOfGuns = NumOfGuns+1
WHERE MemberID = :NEW.OWNERID;
END UPDATEGUNS;
Personally, I don't like ignoring exceptions - I'd rather write code which doesn't raise exceptions - but it's your choice.
Best of luck.
Just use IF after setting up an appropriate flag:
DECLARE
v_flag number;
BEGIN
SELECT (CASE WHEN EXISTS (SELECT 1
FROM Guns
WHERE Make = :New.MakeOwned AND Model = :New.Model AND rownum = 1;
)
THEN 1 ELSE 0
END)
INTO v_flag
FROM DUAL;
IF v_flag = 0
THEN
INSERT INTO Guns(Make, Model) VALUES (:New.Make, :New.Model);
END IF;
UPDATE Member
SET NumOfGuns = NumOfGuns + 1
WHERE MemberID = :New.OwnerId;
END; -- updateGuns
I see no advantage to copying the fields in :NEW to local variables. In fact, it makes the code a bit harder to follow, because the reader has to check if the values are different from the values in the :NEW record.
That said, an alternative is to have a unique index on Guns(Make, Model), attempt an insert and just ignore the error using exceptions.

Table is mutating, trigger/function may not see it ORA-06512 [duplicate]

This question already has answers here:
Oracle trigger after insert or delete
(3 answers)
Closed 7 years ago.
I have two tables called DetailRental and Video. VID_NUM is the PK of Video and the FK of DetailRental.
What this code wants to achieve is when the Detail_Returndate or Detail_Duedate from DetailRental table changes(update or insert new row), the trigger will check the value of Detail_Returndate row by row. If its value is null, then the corresponding(according to VID_NUM) attribute VID_STATUS from Video table will change to "OUT".
The trigger has been created successfully. However, when I want to update the date. Oracle gives me error:
ORA-04091: table SYSTEM2.DETAILRENTAL is mutating, trigger/function may not see it
ORA-06512: at "SYSTEM2.TRG_VIDEORENTAL_UP", line 3
ORA-04088: error during execution of trigger 'SYSTEM2.TRG_VIDEORENTAL_UP'
1. UPDATE DETAILRENTAL
2. SET DETAIL_RETURNDATE = null
3. WHERE RENT_NUM = 1006 AND VID_NUM = 61367
Below is my code:
CREATE OR REPLACE TRIGGER trg_videorental_up
AFTER INSERT OR UPDATE OF DETAIL_RETURNDATE, DETAIL_DUEDATE ON DETAILRENTAL
FOR EACH ROW
AS
DECLARE
DTRD DATE;
BEGIN
SELECT DETAIL_RETURNDATE
INTO DTRD
FROM DETAILRENTAL;
IF DTRD IS NULL
THEN UPDATE VIDEO
SET VIDEO.VID_STATUS = 'OUT'
WHERE EXISTS
(SELECT DETAILRENTAL.VID_NUM
FROM DETAILRENTAL
WHERE DETAILRENTAL.VID_NUM = VIDEO.VID_NUM
);
END IF;
END;
Thank you very much!
problem solved here is the code:
CREATE OR REPLACE TRIGGER trg_videorental_up
AFTER INSERT OR UPDATE OF DETAIL_RETURNDATE, DETAIL_DUEDATE ON DETAILRENTAL
FOR EACH ROW
DECLARE DETAIL_RETURNDATE DATE;
BEGIN
IF :NEW.DETAIL_RETURNDATE IS NULL THEN UPDATE VIDEO SET VID_STATUS = 'OUT' WHERE VID_NUM = :NEW.VID_NUM;
ELSIF :NEW.DETAIL_RETURNDATE > SYSDATE THEN UPDATE VIDEO SET VID_STATUS = 'OUT' WHERE VID_NUM = :NEW.VID_NUM;
ELSIF :NEW.DETAIL_RETURNDATE <= SYSDATE AND TO_CHAR(DETAIL_RETURNDATE)!= '01/01/0001' THEN UPDATE VIDEO SET VID_STATUS = 'IN' WHERE VID_NUM = :NEW.VID_NUM;
ELSIF :NEW.DETAIL_RETURNDATE = '01/01/0001' THEN UPDATE VIDEO SET VID_STATUS = 'LOST' WHERE VID_NUM = :NEW.VID_NUM;
END IF;
END;
A good data model is one in which no redundant information is physically stored. If you can look at one (or more) values in a table.column and figure out what value should be in another table.column, then you've got a redundancy. In your case, a person can see a DETAILRENTAL.DETAIL_DUEDATE for VIDNUM 61367 is not null, and "know" that the VIDEO.STATUS field should be OUT.
Most easily fixed with something like:
1) Create a VIDEO_BASE table, with all VIDEO columns except VID_STATUS:
CREATE TABLE VIDEO_BASE AS
SELECT {list all columns except STATUS}
FROM VIDEO;
2) Drop original VIDEO table and create as a view, VIDEO, which shows all columns of VIDEO_BASE, plus exposes STATUS as a derived field:
CREATE OR REPLACE VIEW VIDEO
AS
SELECT V.*,
CASE WHEN
(
SELECT COUNT(*)
FROM
(
SELECT 'X'
FROM DETAILRENTAL D
WHERE D.VID_NUM = V.VID_NUM
AND DETAIL_RETURNDATE IS NOT NULL
AND ROWNUM <= 1
)
) > 0
THEN 'OUT'
ELSE NULL
END VID_STATUS
FROM VIDEO_BASE V;
In general, if you feel you need a trigger to keep two different tables in sync, you've got a data model problem. In my 15+ years experience with Oracle, the only best way to fix problematic triggers is to fix the data model - the surest way to know that all your triggers are working properly is when the number of triggers in your database is 0.
After reading through #KevinKirkpatrick's answer two or three times, I realise he's right - an individual video's in/out status is derivable from other information in the database. That said, you may have pragmatic reasons for doing it this way.
The bad news is that you can't select from a table within a row trigger on that same table - that's what the "mutating table" problem means. The good news is that in this case you don't really need to.
I don't have an Oracle installation I can test this on, so I make no guarantee of syntactic correctness, but it should be close enough to get you started.
CREATE OR REPLACE TRIGGER trg_videorental_up
AFTER INSERT OR UPDATE
OF detail_duedate, detail_returndate
ON detailrental
FOR EACH ROW
AS
BEGIN
IF :new.detail_returndate IS NULL
AND :new.detail_duedate IS NOT NULL
THEN
UPDATE video
SET status = 'OUT'
WHERE video_num = :new.video_num;
END IF;
END;

Attempting to solve mutating table issue with compound trigger

I am attempting to follow the instructions at Mutating Table Compound Trigger to update a parent table based on the id of a child table and avoid a mutating table error.
I need to obtain the parent id (BLATranscriptId) of the current record in the BLATChildren table and then after the update of the child record completes, I need to extract the current count that matches my criteria and perform an update on the parent table BLATranscript.
THIS ERROR HAS BEEN SOLVED - I'm getting an error that my bind variable "transcriptID" is bad in the "AFTER EACH ROW" portion of my code. I've verify that the BLATChildren.BLATranscriptId exists and is spelled correctly. Solution was to change AFTER EACH ROW to AFTER STATEMENT.
NEW ISSUE - Trigger is updating every record in the parent table, not just the matching parent record.
Any help would be greatly appreciated. Thank you.
CREATE OR REPLACE TRIGGER transcript_after_update
FOR UPDATE of enrollmentStatus,completionDate on blatChildren
COMPOUND TRIGGER
transcriptID number:=0;
BEFORE EACH ROW IS
BEGIN
transcriptID := :new.blaTranscriptid;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
update BLATranscript SET (enrollmentStatus, completionDate) = (select 'C',sysdate from dual)
where id in (select blat.id from BLATranscript blat
inner join BlendedActivity bla on blat.blendedactivityid=bla.id
where blat.id=transcriptID and minCompletion<=(
select count(countForCompletion) as total from blatChildren blac
inner join BlendedActivityMembership bam on blac.childActivityId=bam.childActivityId
where completionDate>=sysdate-acceptPrevWork
and blat.id=transcriptID
and blac.enrollmentStatus='C'));
END AFTER STATEMENT;
END;
/
You need for each row part to collect :new.blaTranscriptid int PL/SQL table
and after statement part that will perform update using this PL/SQL table.
Like:
CREATE OR REPLACE TRIGGER transcript_after_update
FOR UPDATE of enrollmentStatus,completionDate on blatChildren
COMPOUND TRIGGER
TYPE transcriptIDs_t IS TABLE OF blatChildren.blaTranscriptid%TYPE;
transcriptID transcriptIDs_t := transcriptIDs_t();
BEFORE EACH ROW IS
BEGIN
transcriptID.extend;
transcriptID(transcriptID.count) := :new.blaTranscriptid;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN 1..transcriptID.COUNT
LOOP
update BLATranscript SET (enrollmentStatus, completionDate) = (select 'C',sysdate from dual)
where id in (select blat.id from BLATranscript blat
inner join BlendedActivity bla on blat.blendedactivityid=bla.id
where blat.id = transcriptID(i) and minCompletion<=(
select count(countForCompletion) as total from blatChildren blac
inner join BlendedActivityMembership bam on blac.childActivityId=bam.childActivityId
where completionDate>=sysdate-acceptPrevWork
and blat.id=transcriptID(i)
and blac.enrollmentStatus='C'));
END LOOP;
END AFTER STATEMENT;
END;
/

Update or insert based on if employee exist in table

Do want to create Stored procc which updates or inserts into table based on the condition if current line does not exist in table?
This is what I have come up with so far:
PROCEDURE SP_UPDATE_EMPLOYEE
(
SSN VARCHAR2,
NAME VARCHAR2
)
AS
BEGIN
IF EXISTS(SELECT * FROM tblEMPLOYEE a where a.ssn = SSN)
--what ? just carry on to else
ELSE
INSERT INTO pb_mifid (ssn, NAME)
VALUES (SSN, NAME);
END;
Is this the way to achieve this?
This is quite a common pattern. Depending on what version of Oracle you are running, you could use the merge statement (I am not sure what version it appeared in).
create table test_merge (id integer, c2 varchar2(255));
create unique index test_merge_idx1 on test_merge(id);
merge into test_merge t
using (select 1 id, 'foobar' c2 from dual) s
on (t.id = s.id)
when matched then update set c2 = s.c2
when not matched then insert (id, c2)
values (s.id, s.c2);
Merge is intended to merge data from a source table, but you can fake it for individual rows by selecting the data from dual.
If you cannot use merge, then optimize for the most common case. Will the proc usually not find a record and need to insert it, or will it usually need to update an existing record?
If inserting will be most common, code such as the following is probably best:
begin
insert into t (columns)
values ()
exception
when dup_val_on_index then
update t set cols = values
end;
If update is the most common, then turn the procedure around:
begin
update t set cols = values;
if sql%rowcount = 0 then
-- nothing was updated, so the record doesn't exist, insert it.
insert into t (columns)
values ();
end if;
end;
You should not issue a select to check for the row and make the decision based on the result - that means you will always need to run two SQL statements, when you can get away with one most of the time (or always if you use merge). The less SQL statements you use, the better your code will perform.
BEGIN
INSERT INTO pb_mifid (ssn, NAME)
select SSN, NAME from dual
where not exists(SELECT * FROM tblEMPLOYEE a where a.ssn = SSN);
END;
UPDATE:
Attention, you should name your parameter p_ssn(distinguish to the column SSN ), and the query become:
INSERT INTO pb_mifid (ssn, NAME)
select P_SSN, NAME from dual
where not exists(SELECT * FROM tblEMPLOYEE a where a.ssn = P_SSN);
because this allways exists:
SELECT * FROM tblEMPLOYEE a where a.ssn = SSN

Resources