create trigger on INSERT IN table for specific values - oracle

I am trying to crate a trigger in oracle 12c that will execute procedure if inserted values contain specific values.
What I am trying to do is after certain tables (EVENTS, MARKS, STAGE) are all refreshed, only then I want the trigger to run REFRESH_MVS(); procedure.
And they are stored in COUNTS table after they are refreshed. So I am checking if new INSERT in COUNTS has keyword: EVENTS, MARKS, STAGE.
Is this the way to do it?
CREATE or replace TRIGGER MV_REFRESH
AFTER INSERT ON COUNTS
FOR EACH ROW
DECLARE
MODEL_NAME varchar2(20);
BEGIN
select MODEL INTO MODEL_NAME from COUNTS;
IF(MODEL_NAME = 'EVENTS' AND MODEL_NAME = 'MARKS' AND MODEL_NAME = 'STAGE')
THEN
REFRESH_MVS();
END IF;
END;
After compiling it successfully if I run INSERT:
INSERT INTO COUNTS
values ('EVENTS', '11658495', '0.11', '17-MAR-14', '17-MAR-14');
It throws error:
Error starting at line 3 in command:
INSERT INTO COUNTS
values ('EVENTS', '11658495', '0.11', '17-MAR-17', '17-MAR-17')
Error report:
SQL Error: ORA-04091: table COUNTS is mutating, trigger/function may not see it
ORA-06512: at "MV_REFRESH", line 5
ORA-04088: error during execution of trigger 'MV_REFRESH'
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.

It does not appear that what you are trying to do is sensible. If you can explain the business problem you are trying to solve, we can probably assist you in coming up with a more appropriate technical implementation.
In general, in a row-level trigger on a table, you cannot query the table in question. It does not appear that you have any need to query the table in this trigger, though. My guess is that you just need to use the :new.model_name. But if that's the case, the logic doesn't make sense-- it is, of course, impossible for a single attribute to have three different values at the same time.
IF(:NEW.MODEL_NAME = 'EVENTS' AND
:NEW.MODEL_NAME = 'MARKS' AND
:NEW.MODEL_NAME = 'STAGE')
THEN
REFRESH_MVS();
END IF;
Perhaps you meant OR rather than AND
IF(:NEW.MODEL_NAME = 'EVENTS' OR
:NEW.MODEL_NAME = 'MARKS' OR
:NEW.MODEL_NAME = 'STAGE')
THEN
REFRESH_MVS();
END IF;
which could be simplified
IF(:NEW.MODEL_NAME IN( 'EVENTS', 'MARKS', 'STAGE') )
THEN
REFRESH_MVS();
END IF;
Now, you'll also get the mutating table exception if the refresh_mvs tries to query the counts table. If the current implementation tries to query counts, you would need to change the procedure to accept as parameters whatever data it requires from the current row that is being inserted.
If refresh_mvs is actually refreshing materialized views, that implies that it is at least doing implicit commits. That would create further issues because you cannot commit in a trigger (unless the trigger is defined as an autonomous transaction which would not be appropriate here).

Related

Mutating Trigger Error with Trigger in Oracle PL/SQL [duplicate]

I get an error (ORA-04091: table DBPROJEKT_AKTIENDEPOT.AKTIE is mutating, trigger/function may not see it) when executing my trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
AFTER
INSERT OR UPDATE OF TAGESKURS
OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
bfr number;
Begin
bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
UPDATE AKTIE
SET BILANZ = TAGESKURS - WERT_BEIM_EINKAUF;
IF bfr < -50
THEN
DBMS_OUTPUT.PUT_LINE('ACHTUNG: The value (Nr: '||:new.AKTIEN_NR||') is very low!');
END IF;
END;
I want to check the value "BILANZ" after calculating it, wether it is under -50.
Do you have any idea why this error is thrown?
Thanks for any help!
There are several issues here:
Oracle does not allow you to perform a SELECT/INSERT/UPDATE/DELETE against a table within a row trigger defined on that table or any code called from such a trigger, which is why an error occurred at run time. There are ways to work around this - for example, you can read my answers to this question and this question - but in general you will have to avoid accessing the table on which a row trigger is defined from within the trigger.
The calculation which is being performed in this trigger is what is referred to as business logic and should not be performed in a trigger. Putting logic such as this in a trigger, no matter how convenient it may seem to be, will end up being very confusing to anyone who has to maintain this code because the value of BILANZ is changed where someone who is reading the application code's INSERT or UPDATE statement can't see it. This calculation should be performed in the INSERT or UPDATE statement, not in a trigger. It considered good practice to define a procedure to perform INSERT/UPDATE/DELETE operations on a table so that all such calculations can be captured in one place, instead of being spread out throughout your code base.
Within a BEFORE ROW trigger you can modify the values of the fields in the :NEW row variable to change values before they're written to the database. There are times that this is acceptable, such as when setting columns which track when and by whom a row was last changed, but in general it's considered a bad idea.
Best of luck.
You are modifying the table with the trigger. Use a before update trigger:
CREATE OR REPLACE TRIGGER Aktien_Bilanz_Berechnung
BEFORE INSERT OR UPDATE OF TAGESKURS OR INSERT OR UPDATE OF WERT_BEIM_EINKAUF
ON AKTIE
FOR EACH ROW
DECLARE
v_bfr number;
BEGIN
v_bfr := :new.TAGESKURS - :new.WERT_BEIM_EINKAUF;
:new.BILANZ := v_bfr;
IF v_bfr < -50 THEN
Raise_Application_Error(-20456,'ACHTUNG: The value (Nr: '|| :new.AKTIEN_NR || ') is very low!');
END IF;
END;

Oracle - Fetch returns more than requested number of rows - using triggers

So I am trying to use triggers to basically set some rules.. If anyone has an ID number lower than 3, he will have to pay only 100 dollars, but if someone has an ID above that, he will have to pay more. I did some research and have been told to use triggers and that triggers are very useful when fetching multiple rows. So I tried doing that but it didn't work. Basically the trigger gets created but then when i try to add values, I get the following error:-
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "S.PRICTICKET", line 6
ORA-04088: error during execution of trigger 'S.PRICTICKET'
here is what i did to make the trigger:-
CREATE OR REPLACE TRIGGER PRICTICKET BEFORE INSERT OR UPDATE OR DELETE ON PAYS FOR EACH ROW ENABLE
DECLARE
V_PRICE PAYS.PRICE%TYPE;
V_ID PAYS.ID%TYPE;
V_NAME PAYS.NAME%TYPE;
BEGIN
SELECT ID,NAME INTO V_ID,V_NAME FROM PAYS;
IF INSERTING AND V_ID<3 THEN
V_PRICE:=100;
INSERT INTO PAYS(ID,NAME,PRICE) VALUES (V_ID,V_NAME,V_PRICE);
ELSIF INSERTING AND V_ID>=3 THEN
V_PRICE:=130;
INSERT INTO PAYS(ID,NAME,PRICE) VALUES (V_ID,V_NAME,V_PRICE);
END IF;
END;
and the thing is, when i execute this code, i actually do get a message saying the trigger has been compiled. but when when i try to insert values into the table by using the following code, i get the error message I mentioned above.
INSERT INTO PAYS(ID,NAME) VALUES (19,'SS');
You're getting the error you specified, ORA-01422, because you're returning more than one row with the following SELECT:
SELECT ID,NAME INTO V_ID,V_NAME FROM PAYS;
You need to restrict the result set. For example, I'll use the :NEW psuedorecord to grab the row's new ID value, which if unique, will restrict the SELECT to one row:
SELECT ID,NAME INTO V_ID,V_NAME FROM PAYS WHERE ID = :NEW.ID;
Here is the Oracle docs on using triggers: https://docs.oracle.com/database/121/TDDDG/tdddg_triggers.htm#TDDDG99934
However, I believe your trigger has other issues, please see my comments and we can discuss.
EDIT: Based on our discussion.
ORA-04088: error during execution of trigger
Using INSERT inside a BEFORE INSERT trigger on the same table will create an infinite loop. Please consider using an AFTER INSERT and change your INSERTS to UPDATES, or an INSTEAD OF INSERT.
Additionally, remove DELETE from the trigger definition. That makes no sense in this context.
Let's begin clearing up a few things. You were told "triggers are very useful when fetching multiple rows" this is, as a general rule and without additional context, false. There are 4 types of DML triggers:
Before Statement - fires 1 time for the statement regardless of the number of rows processed.
Before Row - fires once for each row processed during the statement before old and new values are merged into a single set of values. At this point you are allowed to change the values in the columns.
After Row - fires once for row processed during the statement after merging old and new values into a single set of values. At this point you cannot change the column values.
After statement - fires once for the statement regardless of the number of rows processed.
Keep in mind that the trigger is effectively part of the statement.
A trigger can be fired for Insert, Update, or Delete. But, there is no need to fire on each. In this case as suggested, remove the Delete. But also the Update as your trigger is not doing anything with it. (NOTE: there are compound triggers, but they contain segments for each of the above).
In general a trigger cannot reference the table that it is fired upon. See error ORA-04091.
If you're firing a trigger on an Insert it cannot do an insert into that same table (also see ORA-04091) and even if you get around that the Insert would fire the trigger, creating a recursive and perhaps a never ending loop - that would happen here.
Use :New.column_name and :Old.column_name as appropriate to refer to column values. Do not attempt to select them.
Since you are attempting to determine the value of a column you must use a Before trigger.
So applying this to your trigger the result becomes:
CREATE OR REPLACE TRIGGER PRICTICKET
BEFORE INSERT ON PAYS
FOR EACH ROW ENABLE
BEGIN
if :new.id is not null
if :new.ID<3 then
:new.Price :=100;
else
:new.Price := 130;
end if ;
else
null; -- what should happen here?
end if ;
END PRICTICKET ;

Oracle PL/SQL: Calling a procedure from a trigger

I get this error when ever I try to fire a trigger after insert on passengers table. this trigger is supposed to call a procedure that takes two parameters of the newly inserted values and based on that it updates another table which is the booking table. however, i am getting this error:
ORA-04091: table AIRLINESYSTEM.PASSENGER is mutating, trigger/function may not see it
ORA-06512: at "AIRLINESYSTEM.CALCULATE_FLIGHT_PRICE", line 11 ORA-06512: at
"AIRLINESYSTEM.CALCULATE_FLIGHT_PRICE", line 15 ORA-06512: at
"AIRLINESYSTEM.CALCULATE_FLIGHT_PRICE_T1", line 3 ORA-04088: error during execution of
trigger 'AIRLINESYSTEM.CALCULATE_FLIGHT_PRICE_T1' (Row 3)
I complied and tested the procedure in the SQL command line and it works fine. The problem seems to be with the trigger. This is the trigger code:
create or replace trigger "CALCULATE_FLIGHT_PRICE_T1"
AFTER
insert on "PASSENGER"
for each row
begin
CALCULATE_FLIGHT_PRICE(:NEW.BOOKING_ID);
end;​​​​​
Why is the trigger isn't calling the procedure?
You are using database triggers in a way they are not supposed to be used. The database trigger tries to read the table it is currently modifying. If Oracle would allow you to do so, you'd be performing dirty reads.
Fortunately, Oracle warns you for your behaviour, and you can modify your design.
The best solution would be to create an API. A procedure, preferably in a package, that allows you to insert passengers in exactly the way you would like it. In pseudo-PL/SQL-code:
procedure insert_passenger
( p_passenger_nr in number
, p_passenger_name in varchar2
, ...
, p_booking_id in number
, p_dob in number
)
is
begin
insert into passenger (...)
values
( p_passenger_nr
, p_passenger_name
, ...
, p_booking_id
, p_dob
);
calculate_flight_price
( p_booking_id
, p_dob
);
end insert_passenger;
/
Instead of your insert statement, you would now call this procedure. And your mutating table problem will disappear.
If you insist on using a database trigger, then you would need to avoid the select statement in cursor c_passengers. This doesn't make any sense: you have just inserted a row into table passengers and know all the column values. Then you call calculate_flight_price to retrieve the column DOB, which you already know.
Just add a parameter P_DOB to your calculate_flight_price procedure and call it with :new.dob, like this:
create or replace trigger calculate_flight_price_t1
after insert on passenger
for each row
begin
calculate_flight_price
( :new.booking_id
, :new.dob
);
end;
Oh my goodness... You are trying a Dirty Read in the cursor. This is a bad design.
If you allow a dirty read, it return the wrong answer, but also it returns an answer that never existed in the table. In a multiuser database, a dirty read can be a dangerous feature.
The point here is that dirty read is not a feature; rather, it's a liability. In Oracle Database, it's just not needed. You get all of the advantages of a dirty read—no blocking—without any of the incorrect results.
Read more on "READ UNCOMMITTED isolation level" which allows dirty reads. It provides a standards-based definition that allows for nonblocking reads.
Other way round
You are misusing the trigger. I mean wrong trigger used.
you insert / update a row in table A and a trigger on table A (for each row) executes a query on table A (through a procedure)??!!!
Oracle throws an ORA-04091 which is an expected and normal behavior, Oracle wants to protect you from yourself since it guarantees that each statement is atomic (i.e will either fail or succeed completely) and also that each statement sees a consistent view of the data
You would expect the query (2) not to see the row inserted on (1). This would be in contradiction
Solution: -- use before instead of after
CREATE OR REPLACE TRIGGER SOMENAME
BEFORE INSERT OR UPDATE ON SOMETABLE

Mutating Error on a trigger to calculate average cost

I have two tables, PRODUCTS, and STATE_PRICE. The prices for each product vary by state. The PRODUCTS table tracks the average cost of each product across all states. I am trying to write a trigger that will update the average price of the item in the PRODUCTS table when the a price is inserted, updated, or deleted in the STATE_PRICE table. I have written the following trigger, which compiles, but when I test it out, I get a mutating error message. I understand the concept of the mutating error, that I am trying to update a table that a trigger is being performed on, but my I am actually trying to update the PRODUCTS table while the trigger is being performed on the STATE_PRICE table.
create or replace trigger trg_avg_cost
after insert or update or delete on state_price
for each row
declare
w_price state_price.list_price%type;
w_product state_price.productid%type;
begin
w_price := :new.list_price;
w_product := :new.productid;
update products
set avg_cost_per_unit = (select avg(w_price) from state_price
where productid = w_product);
end;
/
The specific error message I get says:
Error report:
SQL Error: ORA-04091: table STATE_PRICE is mutating, trigger/function may not see it
ORA-06512: at "TRG_AVG_COST", line 9
ORA-04088: error during execution of trigger 'TRG_AVG_COST'
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.
May be there is a refrential integrity constraint ( on productid), which can also raise the same error. If thats the case the below link can help you to avoid the error.
http://asktom.oracle.com/pls/asktom/ASKTOM.download_file?p_file=6551198119097816936
Within a row trigger no SQL statement can access the table on which the trigger exists. Your SELECT AVG(W_PRICE) FROM STATE_PRICE WHERE PRODUCTID = W_PRODUCT is what's causing the error. The classic way to get around this limitation is to use a compound trigger - documentation here. Also see my answer to this StackOverflow question for an example of implementing a compound trigger.
Share and enjoy.

ORA-04091: table [blah] is mutating, trigger/function may not see it

I recently started working on a large complex application, and I've just been assigned a bug due to this error:
ORA-04091: table SCMA.TBL1 is mutating, trigger/function may not see it
ORA-06512: at "SCMA.TRG_T1_TBL1_COL1", line 4
ORA-04088: error during execution of trigger 'SCMA.TRG_T1_TBL1_COL1'
The trigger in question looks like
create or replace TRIGGER TRG_T1_TBL1_COL1
BEFORE INSERT OR UPDATE OF t1_appnt_evnt_id ON TBL1
FOR EACH ROW
WHEN (NEW.t1_prnt_t1_pk is not null)
DECLARE
v_reassign_count number(20);
BEGIN
select count(t1_pk) INTO v_reassign_count from TBL1
where t1_appnt_evnt_id=:new.t1_appnt_evnt_id and t1_prnt_t1_pk is not null;
IF (v_reassign_count > 0) THEN
RAISE_APPLICATION_ERROR(-20013, 'Multiple reassignments not allowed');
END IF;
END;
The table has a primary key "t1_pk", an "appointment event id"
t1_appnt_evnt_id and another column "t1_prnt_t1_pk" which may or may
not contain another row's t1_pk.
It appears the trigger is trying to make sure that nobody else with the
same t1_appnt_evnt_id has referred to the same one this row is referring to a referral to another row, if this one is referring to another row.
The comment on the bug report from the DBA says "remove the trigger, and perform the check in the code", but unfortunately they have a proprietary code generation framework layered on top of Hibernate, so I can't even figure out where it actually gets written out, so I'm hoping that there is a way to make this trigger work. Is there?
I think I disagree with your description of what the trigger is trying to
do. It looks to me like it is meant to enforce this business rule: For a
given value of t1_appnt_event, only one row can have a non-NULL value of
t1_prnt_t1_pk at a time. (It doesn't matter if they have the same value in the second column or not.)
Interestingly, it is defined for UPDATE OF t1_appnt_event but not for the other column, so I think someone could break the rule by updating the second column, unless there is a separate trigger for that column.
There might be a way you could create a function-based index that enforces this rule so you can get rid of the trigger entirely. I came up with one way but it requires some assumptions:
The table has a numeric primary key
The primary key and the t1_prnt_t1_pk are both always positive numbers
If these assumptions are true, you could create a function like this:
dev> create or replace function f( a number, b number ) return number deterministic as
2 begin
3 if a is null then return 0-b; else return a; end if;
4 end;
and an index like this:
CREATE UNIQUE INDEX my_index ON my_table
( t1_appnt_event, f( t1_prnt_t1_pk, primary_key_column) );
So rows where the PMNT column is NULL would appear in the index with the inverse of the primary key as the second value, so they would never conflict with each other. Rows where it is not NULL would use the actual (positive) value of the column. The only way you could get a constraint violation would be if two rows had the same non-NULL values in both columns.
This is perhaps overly "clever", but it might help you get around your problem.
Update from Paul Tomblin: I went with the update to the original idea that igor put in the comments:
CREATE UNIQUE INDEX cappec_ccip_uniq_idx
ON tbl1 (t1_appnt_event,
CASE WHEN t1_prnt_t1_pk IS NOT NULL THEN 1 ELSE t1_pk END);
I agree with Dave that the desired result probalby can and should be achieved using built-in constraints such as unique indexes (or unique constraints).
If you really need to get around the mutating table error, the usual way to do it is to create a package which contains a package-scoped variable that is a table of something that can be used to identify the changed rows (I think ROWID is possible, otherwise you have to use the PK, I don't use Oracle currently so I can't test it). The FOR EACH ROW trigger then fills in this variable with all rows that are modified by the statement, and then there is an AFTER each statement trigger that reads the rows and validate them.
Something like (syntax is probably wrong, I haven't worked with Oracle for a few years)
CREATE OR REPLACE PACKAGE trigger_pkg;
PROCEDURE before_stmt_trigger;
PROCEDURE for_each_row_trigger(row IN ROWID);
PROCEDURE after_stmt_trigger;
END trigger_pkg;
CREATE OR REPLACE PACKAGE BODY trigger_pkg AS
TYPE rowid_tbl IS TABLE OF(ROWID);
modified_rows rowid_tbl;
PROCEDURE before_stmt_trigger IS
BEGIN
modified_rows := rowid_tbl();
END before_each_stmt_trigger;
PROCEDURE for_each_row_trigger(row IN ROWID) IS
BEGIN
modified_rows(modified_rows.COUNT) = row;
END for_each_row_trigger;
PROCEDURE after_stmt_trigger IS
BEGIN
FOR i IN 1 .. modified_rows.COUNT LOOP
SELECT ... INTO ... FROM the_table WHERE rowid = modified_rows(i);
-- do whatever you want to
END LOOP;
END after_each_stmt_trigger;
END trigger_pkg;
CREATE OR REPLACE TRIGGER before_stmt_trigger BEFORE INSERT OR UPDATE ON mytable AS
BEGIN
trigger_pkg.before_stmt_trigger;
END;
CREATE OR REPLACE TRIGGER after_stmt_trigger AFTER INSERT OR UPDATE ON mytable AS
BEGIN
trigger_pkg.after_stmt_trigger;
END;
CREATE OR REPLACE TRIGGER for_each_row_trigger
BEFORE INSERT OR UPDATE ON mytable
WHEN (new.mycolumn IS NOT NULL) AS
BEGIN
trigger_pkg.for_each_row_trigger(:new.rowid);
END;
With any trigger-based (or application code-based) solution you need to
put in locking to prevent data corruption in a multi-user environment.
Even if your trigger worked, or was re-written to avoid the mutating table
issue, it would not prevent 2 users from simultaneously updating
t1_appnt_evnt_id to the same value on rows where t1_appnt_evnt_id is not
null: assume there are currenly no rows where t1_appnt_evnt_id=123 and
t1_prnt_t1_pk is not null:
Session 1> update tbl1
set t1_appnt_evnt_id=123
where t1_prnt_t1_pk =456;
/* OK, trigger sees count of 0 */
Session 2> update tbl1
set t1_appnt_evnt_id=123
where t1_prnt_t1_pk =789;
/* OK, trigger sees count of 0 because
session 1 hasn't committed yet */
Session 1> commit;
Session 2> commit;
You now have a corrupted database!
The way to avoid this (in trigger or application code) would be to lock
the parent row in the table referenced by t1_appnt_evnt_id=123 before performing the check:
select appe_id
into v_app_id
from parent_table
where appe_id = :new.t1_appnt_evnt_id
for update;
Now session 2's trigger must wait for session 1 to commit or rollback before it performs the check.
It would be much simpler and safer to implement Dave Costa's index!
Finally, I'm glad no one has suggested adding PRAGMA AUTONOMOUS_TRANSACTION to your trigger: this is often suggested on forums and works in as much as the mutating table issue goes away - but it makes the data integrity problem even worse! So just don't...
I had similar error with Hibernate. And flushing session by using
getHibernateTemplate().saveOrUpdate(o);
getHibernateTemplate().flush();
solved this problem for me. (I'm not posting my code block as I was sure that everything was written properly and should work - but it did not until I added the previous flush() statement). Maybe this can help someone.

Resources