Oracle SQL, avoiding errror "table is mutating" (trigger) - sql

i need trigger which will check if updated worker can be moved to other team
CREATE TABLE WORKERS
(
ID_WORKER NUMBER(4,0), --FK
ID_TEAM NUMBER(2,0) --FK
);
My trigger looks like:
CREATE OR REPLACE TRIGGER TEAM_LIMIT
BEFORE INSERT OR UPDATE OF ID_TEAM ON WORKERS
FOR EACH ROW
DECLARE
V_num NUMBER;
BEGIN
SELECT Count(*) INTO V_num FROM Worker WHERE ID_TEAM=:new.ID_TEAM;
IF V_num >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END;
this generate error : "table %s.%s is mutating, trigger/function may not see it" when row is updated. How to write this statements properly to not generate this kind of error?

You can use a compound trigger, it looks like this (not tested):
CREATE OR REPLACE TRIGGER TEAM_LIMIT
FOR INSERT OR UPDATE OF ID_TEAM ON WORKERS
COMPOUND TRIGGER
V_num NUMBER;
TYPE Row_TableType IS TABLE OF WORKERS.ID_TEAM%TYPE;
AffectedTeams Row_TableType;
BEFORE STATEMENT IS
BEGIN
AffectedTeams := Row_TableType(); -- init the table variable
END BEFORE STATEMENT;
-------------------
BEFORE EACH ROW IS
BEGIN
AffectedTeams.EXTEND;
AffectedTeams(AffectedTeams.LAST) := :NEW.ID_TEAM;
END BEFORE EACH ROW;
--------------------
AFTER STATEMENT IS
BEGIN
FOR i IN AffectedTeams.FIRST..AffectedTeams.LAST LOOP
SELECT Count(*) INTO V_num FROM Worker WHERE ID_TEAM=AffectedTeams(i);
IF V_num >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END LOOP;
END AFTER STATEMENT;
END TEAM_LIMIT;
/

You can use a statement trigger. Statement triggers are only fired once for each statement executed rather than once for each row affected. Statement triggers are useful because they don't have the restriction that they can't query the table on which the trigger is declared. They don't have access to the :OLD and :NEW row values but with a little thought you can still accomplish what you're trying to do:
CREATE OR REPLACE TRIGGER TEAM_LIMIT
BEFORE INSERT OR UPDATE OF ID_TEAM ON WORKERS
-- Note: no FOR EACH ROW - therefore, this is a statement trigger
DECLARE
nMax_team_count NUMBER;
BEGIN
SELECT MAX(TEAM_COUNT)
INTO nMax_team_count
FROM (SELECT ID_TEAM, COUNT(*) AS TEAM_COUNT
FROM WORKERS
GROUP BY ID_TEAM));
IF nMax_team_count >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END TEAM_LIMIT;
Instead of looking at the team of the particular work which has been updated we find the counts of workers on each team, then extract the largest count, and if it's more than four we raise the appropriate exception.
Share and enjoy.

Related

SQL - Count number of rows in a table before updating it

Im doing a college project on Oracle SQL and I keep getting the error "table (...) is mutating, trigger/function may not see it" when I try to update a table. My participateTeams table has 3 columns (eventName varchar(), teamId number, teamPlace number).
The Objective here is that when I update the teamPlace of a given team in an event, the place MUST be lower or equal than the number of participant teams in that same event.
My trigger is as follows:
Before update on participateTeams
for each row
Declare participants number;
Begin
select count(*) into participants from participateteams where :New.eventName = participateteams.eventName;
if(:NEW.teamplace>participants) then
RAISE_APPLICATION_ERROR(-20250,'O lugar da equipa é inválido');
end if;
End;
/
From what I've researched it's because I'm trying to read the same table that called the trigger.
I have also have tried exporting the select part of the code into a function, but the issue persists.
The participants in the event are the teams theirselves and not the individuals that form those same teams. Example: If team A, team B and team C participate in an event E, the count for that same event E should be 3.
Any tips? Thank you.
The mutating table error is just a pain. And what you are trying to do is rather tricky. Here is one approach:
Add a column to teams with the count of participants.
Maintain this column with insert/update, and delete triggers on participateteams.
Write a user-defined function to fetch the count for a given team.
Add a check constraint in participateteams using the user-defined function.
The logically correct trigger for what you want to achieve is this :-
CREATE OR REPLACE TRIGGER TRIG_CHK
AFTER INSERT OR UPDATE OF teamPlace ON participateteams
FOR EACH ROW
DECLARE
participants NUMBER;
BEGIN
select count(*) into participants from participateteams where :New.eventName =
participateteams.eventName;
if(:NEW.teamplace>participants) then
RAISE_APPLICATION_ERROR(-20250,'O lugar da equipa é inválido');
end if;
End;
update participateteams set teamPlace = 2 where eventName = 'B';
But when it gives Mutating table error when trying to update table participateteams,to solve this tricky situation , what I did is :-
step1 :Declare the table columns needed in a package header
CREATE OR REPLACE PACKAGE PKG_TEAMS AS
v_eventName participateteams.eventName%type;
v_teamPlace participateteams.teamPlace%type;
end;
step2 : Initialize the variables in a row level trigger
CREATE OR REPLACE TRIGGER TRG_row
AFTER INSERT OR UPDATE OF teamPlace
ON participateteams
FOR EACH ROW
BEGIN
PKG_TEAMS.v_eventName := :NEW.eventName;
PKG_TEAMS.v_teamPlace := :NEW.teamPlace;
END;
step3 :Now ,rather than using :NEW pseudo columns, using globally initialized variables
CREATE OR REPLACE TRIGGER TRG_stmt
AFTER INSERT OR UPDATE OF teamPlace ON participateteams
DECLARE
v_participants NUMBER;
BEGIN
SELECT COUNT(*) INTO v_participants FROM participateteams
WHERE eventName = PKG_TEAMS.v_eventName;
if(PKG_TEAMS.v_teamPlace>v_participants) then
RAISE_APPLICATION_ERROR(-20250,'CANNOT UPDATE,AGAINST RULES');
end if;
End;

Oracle trigger: SELECT INTO, no data found

I have the following table that describes which chemical elements each planet is composed of using percentage.
CREATE TABLE elem_in_planet
(
id_planet INTEGER,
element_symbol CHAR(2),
percent_representation NUMBER CONSTRAINT NN_elem_in_planet NOT NULL,
CONSTRAINT PK_elem_in_planet PRIMARY KEY (id_planet, element_symbol),
CONSTRAINT FK_planet_has_elem FOREIGN KEY (id_planet) REFERENCES planet (id_planet),
CONSTRAINT FK_elem_in_planet FOREIGN KEY (element_symbol) REFERENCES chemical_element (element_symbol)
);
I'm trying to make a trigger that warns users when they add a new element to a planet and the sum of elements in that planet exceeds 100%. I came up with this.
CREATE OR REPLACE TRIGGER elem_in_planet_check
AFTER INSERT OR UPDATE ON elem_in_planet
FOR EACH ROW
DECLARE
sum_var NUMBER;
PRAGMA autonomous_transaction;
BEGIN
SELECT SUM(percent_representation)
INTO sum_var
FROM elem_in_planet
WHERE id_planet = :NEW.id_planet
GROUP BY id_planet;
EXCEPTION
WHEN NO_DATA_FOUND THEN
sum_var := 0;
IF sum_var > 100 THEN
DBMS_OUTPUT.put_line('WARNING: Blah blah.');
END IF;
END;
/
This code seems to throw the NO_DATA_FOUND exception every single time, even though I have inserted test data and when I run the SQL query alone, it works as expected.
I'm new to this and don't understand what I'm doing wrong.
Thank you for any advice.
You have NOT inserted the row into the table, 2 reasons.
The trigger runs as part of the insert statement which has not
completed. So the row does not exist.
You specified "PRAGMA autonomous_transaction" (AKA the create
untraceable bug here statement), did you previously get a mutating
table exception. So you cannot see any data inserted/updated/deleted
by the current transaction.Further if an error did occur the row would still be inserted as you did not raise an error or re-raise the existing error. I suggest you familiarize yourself with the PLSQL block structure. For now you may want to try:
You could use an after statement trigger or after statement section of compound trigger to make this test, do raise_application_error if sum > 100;
BTW as it stands your "if on sum_var > 100" runs only when an error occurs. Anything after the "EXCEPTION" and before END for that block runs only when a error occurs.
create or replace trigger elem_in_planet_check
after insert or update on elem_in_planet
declare
error_detected boolean := False;
begin
for planet in
(
select id_planet, sum_var
from (select id_planet, sum(percent_representation) sum_var
from elem_in_planet
group by id_planet
)
where sum_var > 100
)
loop
dbms_output.put_line('Planet ' || planet.id_planet || ' at '|| planet.sum_var || '% resources exceed 100%');
error_detected:= True;
end loop;
if error_detected then
Raise_application_error('-20001', 'Planet resources cannot exceed 100%');
end if;
end elem_in_planet_check;

Updating the record of same table when new record is inserted or updated in oracle

I am new to learning Oracle. I have a task in which I need to update value of any previous record if new record contains its reference.
Table structure is as below :
Review_Table
(review_id number pk,
review_name varchar2,
previous_review number null,
followup_review number null
)
Here previous_review and followup_review columns are objects of same table i.e Review_table.
Now consider we have two records in Review_table A and B, A does not have any previous or followup review. When user creates/updates the record B and he selects record A as previous record, then we want to automatically update (via trigger) the value of A record's followup review with B's Review ID.
I have tried writing following trigger
create or replace trigger "REVIEW_T1"
AFTER insert or update on "REVIEW_TABLE"
for each row
begin
update REVIEW_TABLE
set review_follow_up_review = :new.REVIEW_ID
where REVIEW_ID = :new.REVIEW_PREVIOUS_REVIEW;
end;
But I am getting error as : REVIEW_TABLE is mutating, trigger/function may not see it ORA-06512
I have tried searching everything but was unable to find any solution for it
TL;DR: No trigger, no mutating. Do not use trigger to change another row in the same table.
I absolutely agree with #StevenFeuerstein's comment:
I also suggest not using a trigger at all. Instead, create a package that contains two procedures, one to insert into table, one to update. And within these procedures, implement the above logic. Then make sure that the only way developers and apps can modify the table is through this package (don't grant privs on the table, only execute on the package).
Take a look at the following example.
Prepare the schema:
create table reviews (
id number primary key,
name varchar2 (32),
previous number,
followup number
);
create or replace procedure createNextReview (name varchar2, lastId number := null) is
lastReview reviews%rowtype;
nextReview reviews%rowtype;
function getLastReview (lastId number) return reviews%rowtype is
begin
for ret in (
select * from reviews where id = lastId
for update
) loop return ret; end loop;
raise_application_error (-20000, 'last review does not exist');
end;
procedure insertReview (nextReview reviews%rowtype) is
begin
insert into reviews values nextReview;
exception when others then
raise_application_error (-20000, 'cannot insert next review');
end;
procedure setFollowUp (nextId number, lastId number) is
begin
update reviews set
followup = nextId
where id = lastId
;
exception when others then
raise_application_error (-20000, 'cannot update last review');
end;
begin
if lastId is not null then
lastReview := getLastReview (lastId);
end if;
nextReview.id := coalesce (lastReview.id, 0)+1;
nextReview.name := name;
nextReview.previous := lastId;
insertReview (nextReview);
if lastReview.Id is not null then
setFollowUp (nextReview.id, lastReview.Id);
end if;
exception when others then
dbms_output.put_line (
'createNextReview: '||sqlerrm||chr(10)||dbms_utility.format_error_backtrace ()
);
end;
/
Execute:
exec createNextReview ('first review')
exec createNextReview ('next review', 1)
See the outcome of work done:
select * from reviews;
ID NAME PREVIOUS FOLLOWUP
---------- ---------------- ---------- ----------
1 first review 2
2 next review 1
First you need to read about triggers, mutating table error and compound triggers: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/triggers.htm#LNPLS2005
Your trigger is AFTER UPDATE OR INSERT. Means if you run UPDATE OR INSERT statements on this table, the trigger will fire. But you are trying to update the same table again inside your trigger, which is compl. wrong.
I think you can fix this by rewriting this as a before trigger, rather than an after trigger.

Oracle SQL Creating Trigger to Increment a Sequence Number For Each Row

I'm trying to create a trigger but I have learned I can not design it as in my first attempt, which I'm showing below. This will cause a 'mutating table' error due to selecting from the table as it is being modified. It actually didn't cause this error when inserting only one record at a time, but when I insert multiple records at once it does.
The purpose of the trigger is to count the number of records in the table where the customer is equal to the customer about to be inserted, and to set the new order_num value as count+1. I also have a public key value set by the trigger which draws from a sequence. This part works ok once I remove the order_num part of the trigger and the associated SELECT. How can I achieve what I am trying to do here? Thanks in advance.
CREATE OR REPLACE TRIGGER t_trg
BEFORE INSERT ON t
FOR EACH ROW
DECLARE
rec_count NUMBER(2,0);
BEGIN
SELECT COUNT(*) INTO rec_count
FROM t
WHERE customer_id = :NEW.customer_id;
:NEW.order_num:= rec_count+1;
:NEW.order_pk_id:= table_seq.NEXTVAL;
END;
Two triggers and temp table approach can provide solution to you seek, preventing mutating table error. However performance will most likely suffer.
create global temporary table cust_temp(customer_id number, cust_cnt number);
create or replace trigger t_trig1
before insert on t
declare
begin
insert into cust_temp select customer_id, count(*) from t group by customer_id;
end;
/
CREATE OR REPLACE TRIGGER t_trg2
BEFORE INSERT ON t
FOR EACH ROW
DECLARE
rec_count number;
BEGIN
BEGIN
SELECT cust_cnt INTO rec_count
FROM cust_temp
WHERE customer_id = :NEW.customer_id;
EXCEPTION when no_data_found then rec_count := 0;
END;
:NEW.order_num:= rec_count+1;
:NEW.order_pk_id:= table_seq.NEXTVAL;
update cust_temp set cust_cnt = rec_count + 1
where customer_id = :NEW.customer_id;
END;
/

Creating a trigger that deletes a row when an attribute becomes negative [oracle sql]?

I would like to create a trigger that will delete a row when one of its attributes becomes negative. So far I have this, but it doesn't appear to be valid sql:
CREATE OR REPLACE TRIGGER ZERO_COPIES_TRIGGER
after
update of counter_attribute
on my_table
referencing new as new
for each row when(new.copies < 0)
begin
delete from my_table where my_table.id = :new.id;
end;
This is not going to work. You can't perform DML on a table which is being manipulated by a row-level trigger. You will get a "mutating table" error.
To get the result you want, your best bet is to have a flag or indicator column to identify that the record is to be deleted. Then, have a separate job or process or whatever to actually perform the delete.
For the sake of completeness: another option is to have a statement trigger which scans the table and then performs the delete, as in:
CREATE OR REPLACE TRIGGER ZERO_COPIES_TRIGGER
AFTER UPDATE OF COUNTER_ATTRIBUTE ON MY_TABLE
BEGIN
FOR aROW IN (SELECT ID
FROM MY_TABLE
WHERE COPIES < 0)
LOOP
DELETE FROM MY_TABLE
WHERE ID = aROW.ID;
END LOOP;
END ZERO_COPIES_TRIGGER;
Or, if you really want to have some fun, you could use a compound trigger to handle the deletes, without requiring a table scan, while still avoiding the dreaded "MUTATING TABLE" error, as in:
CREATE OR REPLACE TRIGGER COMPOUND_ZERO_COPIES_TRIGGER
FOR UPDATE OF COUNTER_ATTRIBUTE ON MY_TABLE
COMPOUND TRIGGER
TYPE NUMBER_TABLE IS TABLE OF NUMBER;
tblDELETE_IDS NUMBER_TABLE;
BEFORE STATEMENT IS
BEGIN
tblDELETE_IDS := NUMBER_TABLE();
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
IF :NEW.COPIES < 0 THEN
tblDELETE_IDS.EXTEND;
tblDELETE_IDS(tblDELETE_IDS.LAST) := :NEW.ID;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
IF tblDELETE_IDS.COUNT > 0 THEN
FOR I IN tblDELETE_IDS.FIRST..tblDELETE_IDS.LAST LOOP
DELETE FROM MY_TABLE
WHERE ID = tblDELETE_IDS(I);
END LOOP;
END IF;
END AFTER STATEMENT;
END COMPOUND_ZERO_COPIES_TRIGGER;
Share and enjoy.