I want to create a trigger that execute on update of a table.
in particular on update of a table i want to update another table via a trigger but if the trigger fails (REFERENTIAL INTEGRITY-- ENTITY INTEGRITY) i do not want to execute the update anymore.
Any suggestion on how to perform this?
Is it better to use a trigger or do it anagrammatically via a stored procedure?
Thanks
The DML in the trigger is part of the same action as the triggering DML. Both have to succeed or b oth fail. If the trigger raises an unhandled exception the entire statement gets rolled back.
Here is a trigger on T23 which copies the row into T42.
SQL> create or replace trigger t23_trg
2 before insert or update on t23 for each row
3 begin
4 insert into t42 values (:new.id, :new.col1);
5 end;
6 /
Trigger created.
SQL>
A successful inserrt into T23...
SQL> insert into t23 values (1, 'ABC')
2 /
1 row created.
SQL> select * from t42
2 /
ID COL
---------- ---
1 ABC
SQL>
But this one will fail because of a unique constraint on T42.ID. As you can see the triggering statement is rolled back too ...
SQL> insert into t23 values (1, 'XYZ')
2 /
insert into t23 values (1, 'XYZ')
*
ERROR at line 1:
ORA-00001: unique constraint (APC.T24_PK) violated
ORA-06512: at "APC.T23_TRG", line 2
ORA-04088: error during execution of trigger 'APC.T23_TRG'
SQL> select * from t42
2 /
ID COL
---------- ---
1 ABC
SQL> select * from t23
2 /
ID COL
---------- ---
1 ABC
SQL>
If the trigger fails, it will raise an exception ( unless you specifically tell it not to ), in which case, you would have the client rollback. It doesn't really matter if its done via a trigger or a SP ( although its often a good idea to keep a logical transaction within a SP, rather than spread it around triggers ).
Related
I have a PL/SQL package that provides a transaction API for creating an instance of an entity (say a new customer). The API involves several DML steps.
There is a view that exposes instances of this (customer) entity and there is an INSTEAD OF trigger on the view that calls the transaction API whenever someone inserts into the view.
Normally, I would like my transaction API to not know or care about whether it is being called from a trigger. I want it to work like a typical API (typical around here, anyway):
Establish savepoint
Do steps 1-3 of DML
Do NOT commit (leave that to caller / client)
On others rollback to savepoint
The problem is that an API like this fails if called from a trigger.
I understand why Oracle cannot allow us to commit or rollback in trigger. But why does Oracle not allow us to rollback to savepoint that the trigger established?
How can I write my API so that:
It cannot have side-effects if any DML step fails halfway through
It's successful work is commited when the caller / client commits (i.e., autonomous transaction is a no-go)
It does not rely on the caller to raise_application_error if it fails. (Obviously, if I could rely on trigger callers to do this and on client code to then rollback, I won't need to worry about side-effects).
I'm a bit confused by your statement. PL/SQL programs automatically have a "natural" savepoint at their commencement, so that if it fails, it will rollback any changes it made, but leave existing changes in the current transaction untouched, eg
SQL> create table t ( x int );
Table created.
SQL>
SQL> create or replace
2 procedure my_api(p_fail boolean) is
3 v int;
4 begin
5 insert into t values (4);
6
7 insert into t values (5);
8
9 if p_fail then
10 v := 1/0;
11 end if;
12
13 insert into t values (6);
14 end;
15 /
Procedure created.
SQL>
SQL> insert into t values (1);
1 row created.
SQL> insert into t values (2);
1 row created.
SQL> insert into t values (3);
1 row created.
SQL> exec my_api(false);
PL/SQL procedure successfully completed.
SQL>
SQL> select * from t;
X
----------
1
2
3
4
5
6
6 rows selected.
API worked, so pre-API changes PLUS the API changes are all there. I'll roll back now to an empty state and let the API fail during execution
SQL>
SQL> rollback;
Rollback complete.
SQL> select * from t;
no rows selected
SQL>
SQL> insert into t values (1);
1 row created.
SQL> insert into t values (2);
1 row created.
SQL> insert into t values (3);
1 row created.
SQL> exec my_api(true);
BEGIN my_api(true); END;
*
ERROR at line 1:
ORA-01476: divisor is equal to zero
ORA-06512: at "MCDONAC.MY_API", line 9
ORA-06512: at line 1
SQL>
SQL> select * from t;
X
----------
1
2
3
My API inserts are rolled back but the pre-API inserts are still there.
I Have Two Tables; TBL_EMPDETAILS (empdetails_id, EMP_SALARY) and TBL_SERVICE (empdetails_id, Salary, Date_Appointed). The idea is that when i update the tbl_service (which basically is salary history) it should update TBL_EMPDETAILS to the most recent Salary.
I've created a TRIGGER But i keep getting MUTATION ERROR. From my research i have seen recommended compound triggers but i am unsure. I also tried pragma autonomous_transaction; befor the bgin statement but encountered "DEADLOCK ERROR"
create or replace trigger Update_Salary
before insert or update on "TBL_SERVICE"
for each row
declare
x number ;
y number ;
z date ;
m date;
begin
x := :NEW."SALARY";
y := :NEW."EMPDETAILS_ID";
z := :NEW."DATE_APPOINTED";
Select max(DATE_APPOINTED)
into m From TBL_SERVICE Where Empdetails_id = y ;
IF z >= m
THEN
update tbl_empdetails Set EMP_SALARY = x Where Empdetails_id = y ;
End If;
commit;
end;
I Expect that when i add a row to the TBL_SERVICE for eg. (empdetails_id, Salary, Date_Appointed) = (100, $500 , 20-Jul-2019) it should update the TBL_EMPDETAILS (empdetails_id, EMP_SALARY) to (100, $500)
Mutation Error -ORA-04091
Deadlock Error -ORA-00060
So i Think the COMPOUND TRIGGER LOOKS LIKE THE ROUTE TO GO... I TRIED CODE BELOW BUT IM STILL MISSING SOMETHING :(
create or replace TRIGGER "RDC_HR".Update_Salary
FOR UPDATE OR INSERT ON "RDC_HR"."TBL_SERVICE"
COMPOUND TRIGGER
m date ;
AFTER EACH ROW IS
begin
Select max(DATE_APPOINTED) into m From TBL_SERVICE
Where Empdetails_id = :NEW."EMPDETAILS_ID" ;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
IF (:NEW."DATE_APPOINTED") >= m THEN
update tbl_empdetails Set EMP_SALARY = :NEW."SALARY"
Where Empdetails_id = :NEW."EMPDETAILS_ID" ;
End If;
END AFTER STATEMENT;
end Update_Salary;
How about merge?
SQL> create table tbl_empdetails (empdetails_id number, emp_salary number);
Table created.
SQL>
SQL> create table tbl_service (empdetails_id number, salary number, date_appointed date);
Table created.
SQL>
SQL> create or replace trigger trg_biu_ser
2 before insert or update on tbl_service
3 for each row
4 begin
5 merge into tbl_empdetails e
6 using (select :new.empdetails_id empdetails_id,
7 :new.salary salary,
8 :new.date_appointed date_appointed,
9 (select max(s1.date_appointed)
10 from tbl_service s1
11 where s1.empdetails_id = :new.empdetails_id
12 ) da
13 from dual
14 ) x
15 on (x.empdetails_id = e.empdetails_id)
16 when matched then update set e.emp_salary = :new.salary
17 where :new.date_appointed > x.da
18 when not matched then insert (empdetails_id , emp_salary)
19 values (:new.empdetails_id, :new.salary);
20 end;
21 /
Trigger created.
SQL>
Testing:
SQL> -- initial value
SQL> insert into tbl_service values (1, 100, sysdate);
1 row created.
SQL> -- this is now the highest salary
SQL> insert into tbl_service values (1, 200, sysdate);
1 row created.
SQL> -- this won't be used because date is "yesterday", it isn't the most recent
SQL> insert into tbl_service values (1, 700, sysdate - 1);
1 row created.
SQL> -- this will be used ("tomorrow")
SQL> insert into tbl_service values (1, 10, sysdate + 1);
1 row created.
SQL> -- a new employee
SQL> insert into tbl_service values (2, 2000, sysdate);
1 row created.
SQL>
The final result:
SQL> select * From tbL_service order by empdetails_id, date_appointed;
EMPDETAILS_ID SALARY DATE_APPOINTED
------------- ---------- -------------------
1 700 24.07.2019 15:00:21
1 100 25.07.2019 15:00:08
1 200 25.07.2019 15:00:15
1 10 26.07.2019 15:00:27
2 2000 25.07.2019 15:00:33
SQL> select * from tbl_empdetails order by empdetails_id;
EMPDETAILS_ID EMP_SALARY
------------- ----------
1 10
2 2000
SQL>
There are a few basic issues with the trigger as shown.
First, it contains a COMMIT. There shouldn't be a COMMIT in a trigger, since the transaction is still in flight.
The larger problem is that you are accessing the table on which the trigger was created within the trigger:
Select max(DATE_APPOINTED)
into m From TBL_SERVICE Where Empdetails_id = y ;
A row-level trigger cannot query or modify the base table. This is what is causing the mutating table error.
There are a few approaches to handle this.
If you want to use a trigger, you will need to defer the part that queries the base table to a time after the row-level trigger is complete.
This is done using a statement-level trigger or a compound trigger.
A row-level trigger can communicate "work to do" by storing state in a variable in a package, a following statement-level trigger can then inspect the package variables and do work based on the content.
The compound trigger mechanism is a way of putting the row and statement triggers in one code unit, along with the package bits. It is a way of writing the whole thing with one chunk of code (compound trigger) rather than three (row trigger, package, statement trigger).
Here is a detailed writeup of using Compound Triggers: Get rid of mutating table trigger errors with the compound trigger
As mentioned, moving the code out of triggers and into a stored procedure is certainly an option.
So I am trying to have a trigger fire and insert data into various tables. However, the process will differ so i was going to create two separate triggers. I've learned thus far how to have a trigger fire after every insert into a table. How can i have a trigger fire ONLY if the IDs are correlated to a certain primary key (id) from another table? I want the trigger to only fire on survey_cycles correlated to Form_IDs of '777' from the Form table. Form_ID and Survey_Cycle are joined at form_id. The bare bones table structure for both of these tables are below:
*Survey_Cycle:*
survey_Cycle_id
survey_form_id
*Survey_Form:*
Survey_Form_Id
My current trigger code is below:
create or replace TRIGGER Survey_Sample
AFTER INSERT
ON Survey_Cycle
FOR EACH ROW
DECLARE
Survey_Cycle_Id Number;
pSurvey_Cycle_Id Number;
BEGIN
Insert into Survey_Cycle_Sample
(Survey_Cycle_ID, Stat_Method_Id, Create_Dt, Create_User_Id, Modify_Dt, Modify_User_Id, Effective_Dt, Inactive_Dt, Survey_Cycle_Sample_Tx)
Values
(:NEW.Survey_Cycle_Id, 0, trunc(sysdate, 'HH'), 1, null, null, null, null, null);
Insert into Survey_Cycle_Period
(Survey_Cycle_Id, Survey_Cycle_Period_Open_Dt, Survey_Cycle_Period_Close_Dt, Survey_Period_Type_Cd, Create_Dt, Create_User_Id, Modify_Dt, Effective_Dt, Inactive_Dt, Survey_Cycle_Period_Due_Dt, Survey_Cycle_Actual_Close_Dt)
Values
(:NEW.Survey_Cycle_Id, trunc(sysdate, 'HH'), trunc(sysdate + 1), 'Initial', sysdate, 1, null, null, null, sysdate - 1, null);
END;
Perhaps you could create a trigger which calls a stored procedure, and let it decide whether do (or not) additional processing. Here's an example:
Sample tables:
SQL> create table survey_cycle
2 (survey_cycle_id number constraint pk_sc primary key,
3 survey_form_id number
4 );
Table created.
SQL> create table survey_form
2 (survey_form_id number constraint pk_sf primary key);
Table created.
SQL>
A procedure, which - for the ID passed to it - checks whether such an ID exists in the SURVEY_FORM table. If not, SELECT will fail (i.e. raise NO_DATA_FOUND) so the procedure won't do anything. If it exists, it'll execute additional code (such as inserts into two other tables; instead of that, I'm just displaying the appropriate message).
SQL> create or replace procedure p_survey (par_survey_form_id in number)
2 is
3 l_survey_form_id survey_form.survey_form_id%type;
4 begin
5 select survey_form_id
6 into l_survey_form_id
7 from survey_form
8 where survey_form_id = par_survey_form_id;
9
10 dbms_output.put_line('Here goes INSERT INTO survey_cycle_sample and survey_cycle_period');
11 exception
12 when no_data_Found then
13 -- there's no ID like PAR_SURVEY_FORM_ID, so - do nothing
14 null;
15 end;
16 /
Procedure created.
The trigger is very simple:
SQL> create or replace trigger trg_ai_survey_cycle
2 after insert on survey_cycle
3 for each row
4 begin
5 p_survey(:new.survey_form_id);
6 end;
7 /
Trigger created.
SQL>
Finally, testing: 777 is the "existing" ID which will cause additional inserts.
SQL> set serveroutput on
SQL> insert into survey_form values (777);
1 row created.
SQL> --
SQL> insert into survey_cycle values (1, 100);
1 row created.
SQL> insert into survey_cycle values (2, 777);
Here goes INSERT INTO survey_cycle_sample and survey_cycle_period
1 row created.
SQL>
Yes, I ask numerous questions because I learn more here than from the books. I've created a simple code block that produces desired output but it seems to simple for the current learning block i'm in. Would this code work for anyone that attempts to update the iddonor for a donor table if the id already exists? Haven't learned procedures or functions yet but can guess that would be a more sensible method. Does what i have thus far satisfy and exception handler if a condition arises or should I add more in the declaration? Appreciate the suggestions and learning points if provided.
My Code:
BEGIN
INSERT INTO dd_donor (iddonor)
VALUES (305)
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
DBMS_OUTPUT.PUT_LINE('ID already Exists');
END;
DUP_VAL_ON_INDEX exception is raised when you try to store duplicate values in a column that is supported by a unique index.
Let's see a simple example. I have created a table with single column and made it the primary key, which will be supported by an implicit unique index.
Setup
SQL> CREATE TABLE t(ID NUMBER);
Table created.
SQL> ALTER TABLE t ADD CONSTRAINT t_uk PRIMARY KEY(ID);
Table altered.
SQL> INSERT INTO t(ID) VALUES(1);
1 row created.
SQL> COMMIT;
Commit complete.
SQL> SELECT * FROM t;
ID
----------
1
SQL> BEGIN
2 INSERT INTO t(ID) VALUES(1);
3 END;
4 /
BEGIN
*
ERROR at line 1:
ORA-00001: unique constraint (LALIT.T_UK) violated
ORA-06512: at line 2
So, the unique constraint is violated. Let's see how to capture DUP_VAL_ON_INDEX exception:
Test case
SQL> SET serveroutput ON
SQL> BEGIN
2 INSERT INTO t(ID) VALUES(1);
3 EXCEPTION
4 WHEN DUP_VAL_ON_INDEX THEN
5 DBMS_OUTPUT.PUT_LINE('Duplicate value on index');
6 END;
7 /
Duplicate value on index
PL/SQL procedure successfully completed.
SQL>
By the way, the DBMS_OUTPUT was only for demo purpose, ideally you wouldn't have it in your production code.
Simple one. I´m a bit of a newvbie with PLSql and oracle's error messages are never too helpful.
I want to do a simple trigger to update a column with the current date i.e. 'modified date' column of a table. Getting an odd error though.
The idea is simple
create table test1 (tcol varchar2(255), tcol2 varchar2(255))
CREATE OR REPLACE TRIGGER testTRG
AFTER INSERT OR UPDATE ON test1
FOR EACH ROW
BEGIN
update test1
set tcol2 = to_char(sysdate)
where tcol = :OLD.tcol;
END;
insert into test1 (tcol) values ('test1');
this pops up the error:
ORA-04091: table RAIDBIDAT_OWN.TEST1 is mutating, trigger/function may not see it
ORA-06512: at "RAIDBIDAT_OWN.TESTTRG", line 2
ORA-04088: error during execution of trigger 'RAIDBIDAT_OWN.TESTTRG'
Would anyone have a quick solution for this?
cheers,
f.
Your situation:
SQL> create table test1 (tcol varchar2(255), tcol2 varchar2(255))
2 /
Table created.
SQL> CREATE OR REPLACE TRIGGER testTRG
2 AFTER INSERT OR UPDATE ON test1
3 FOR EACH ROW
4 BEGIN
5 -- Your original trigger
6 update test1
7 set tcol2 = to_char(sysdate)
8 where tcol = :OLD.tcol;
9 END;
10 /
Trigger created.
SQL> insert into test1 (tcol) values ('test1');
insert into test1 (tcol) values ('test1')
*
ERROR at line 1:
ORA-04091: table [schema].TEST1 is mutating, trigger/function may not see it
ORA-06512: at "[schema].TESTTRG", line 3
ORA-04088: error during execution of trigger '[schema].TESTTRG'
Tony's suggestion is almost right, but unfortunately it doesn't compile:
SQL> CREATE OR REPLACE TRIGGER testTRG
2 AFTER INSERT OR UPDATE ON test1
3 FOR EACH ROW
4 BEGIN
5 -- Tony's suggestion
6 :new.tcol2 := sysdate;
7 END;
8 /
CREATE OR REPLACE TRIGGER testTRG
*
ERROR at line 1:
ORA-04084: cannot change NEW values for this trigger type
Because you can only change NEW values in before-each-row triggers:
SQL> create or replace trigger testtrg
2 before insert or update on test1
3 for each row
4 begin
5 :new.tcol2 := sysdate;
6 end;
7 /
Trigger created.
SQL> insert into test1 (tcol) values ('test1');
1 row created.
SQL> select * from test1
2 /
TCOL
------------------------------------------------------------------------------------------
TCOL2
------------------------------------------------------------------------------------------
test1
13-09-2010 12:37:24
1 row selected.
Regards,
Rob.
The trigger should simply read:
CREATE OR REPLACE TRIGGER testTRG
BEFORE INSERT OR UPDATE ON test1
FOR EACH ROW
BEGIN
:new.tcol2 := to_char(sysdate);
END;
There is no requirement to issue another update of the same row (and as you have found, you cannot).
It is more usual to use DATE columns to store dates:
create table test1 (tcol varchar2(255), tcol2 date);
CREATE OR REPLACE TRIGGER testTRG
BEFORE INSERT OR UPDATE ON test1
FOR EACH ROW
BEGIN
:new.tcol2 := sysdate;
END;