I have one table with 3 columns, lets say Table A with columns c1,c2 and c3.
now I want to call different APIs when each of the column are updated trough DB Trigger.
Should I use if else conditions or go for multiple DB Triggers each for one column?
One trigger is fine, because you can use the UPDATING clause to determine which columns are being manipulated, eg
SQL> create table t ( x int, y int );
Table created.
SQL> insert into t values (1,1);
1 row created.
SQL>
SQL> create or replace
2 trigger TRG
3 before update on t
4 for each row
5 begin
6 if updating('X') then
7 dbms_output.put_line('I need to call API for X');
8 end if;
9
10 if updating('Y') then
11 dbms_output.put_line('I need to call API for Y');
12 end if;
13 end;
14 /
Trigger created.
SQL>
SQL> set serverout on
SQL> update t set x = x + 1;
I need to call API for X
1 row updated.
SQL> update t set x = x + 1;
I need to call API for X
1 row updated.
SQL> update t set x = x + 1, y = y+ 1;
I need to call API for X
I need to call API for Y
1 row updated.
Having said that, burying business logic in triggers can often be more trouble than its worth. I've discussed that at length here:
https://youtu.be/P1OFbNhgT1k
https://youtu.be/hdU92bsByOQ
so I've a similar stance here to Wernfried
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.
I am using before update trigger for each row on table, say emp_table to update one column modifid_date before loading into table. If I am going to update the table with same/existing values of a row, then is this trigger going to fire or not?
condition in trigger:
:new.modifid_dt := sysdate;
Table Values before update: john (name),4867 (id),20-04-2016 (modifid_dt)
Table values now going to update: john (name),4867 (id)
Your trigger will be fired, no matter the values you are using; for example:
SQL> create table testTrigger ( a number)
2 /
Table created.
SQL> CREATE OR REPLACE TRIGGER before_update_trigger
2 before update on testTrigger
3 for each row
4 begin
5 dbms_output.put_line('Trigger fired!');
6 end;
7 /
Trigger created.
SQL> insert into testTrigger values (10);
1 row created.
SQL>
SQL>
SQL> update testTrigger set a = 10;
Trigger fired!
1 row updated.
SQL> update testTrigger set a = 11;
Trigger fired!
1 row updated.
SQL>
If you want avoid "false" firing you should write trigger like this:
create or replace trigger trigger1
before update on tst
for each row
begin
IF :new.t_key != :old.t_key AND ... THEN
dbms_output.put_line('Trigger fired!');
END IF;
end;
But beware of NULL values, of course.
New or existing values - no matter, anyway you'll perform an update so trigger will fire.
I have a table trigger, which calls a procedure when the status change from 2 to 3. The procedure check if the whole group of data(group_id) is in status 3 and then perform some actions.
But now I'm facing the problem that when I set the whole group of data in status 3 at the same time, the procedure get called multiple times and perform this actions multiple times. How can I prevent his? For example with locks
Here is my procedure query:
SELECT COUNT(*)
INTO nResult
FROM ticket
WHERE group_id = nGroupId
AND statusid BETWEEN 0 AND 2;
/* If not all tickets of group in status 3, no action required */
IF nResult != 0 THEN
RETURN;
END IF;
And this is my trigger:
IF (:NEW.STATUSID = 3 AND :OLD.STATUSID = 2) THEN
myprocedure(:NEW.group_id);
END IF;
You probably have a row level trigger, that is fired every time a row is updated; for example:
SQL> create table trigger_table(status number);
Table created.
SQL> insert into trigger_table values (1);
1 row created.
SQL> insert into trigger_table values (2);
1 row created.
SQL> insert into trigger_table values (3);
1 row created.
SQL> create trigger update_trigger
2 after update on trigger_table
3 for each row /* ROW LEVEL */
4 begin
5 dbms_output.put_line('change');
6 end;
7 /
Trigger created.
SQL> set serveroutput on
SQL> update trigger_table set status = 1;
change
change
change
3 rows updated.
You need a table level trigger, fired after every update statement:
SQL> create or replace trigger update_trigger
2 after update on trigger_table
3 begin
4 dbms_output.put_line('change');
5 end;
6 /
Trigger created.
SQL> update trigger_table set status = 1;
change
3 rows updated.
Here you find something more.
As rightly observed by Nicholas Krasnov, in this kind of trigger, considering a set of rows and not a single one, you have not the :new or :old values.
A way to get your needs could be the following, but it's a tricky solution and I'd check it carefully before using in a production environment.
You could create a semaphore table to know if you have to fire the trigger or not, then use two triggers, one at row level, BEFORE update, and one at table level, AFTER update; the row level one checks the values and updates the semaphore table while the table level one, fired after the update, reads the semaphore, calls your procedure, if necessary, then resets the semaphore.
For example:
SQL> create table trigger_table(status number);
Table created.
SQL> insert into trigger_table values (1);
1 row created.
SQL> insert into trigger_table values (2);
1 row created.
SQL> insert into trigger_table values (3);
1 row created.
SQL> create table checkChange (fire varchar2(3));
Table created.
SQL> insert into checkChange values ('NO');
1 row created.
SQL> create or replace trigger before_update_trigger
2 before update on trigger_table
3 for each row /* ROW LEVEL */
4 begin
5 if :new.status = 3 and :old.status = 2 then
6 update checkChange set fire = 'YES';
7 end if;
8 end;
9 /
Trigger created.
SQL> create or replace trigger after_update_trigger
2 after update on trigger_table
3 declare
4 vFire varchar2(3);
5 begin
6 select fire
7 into vFire
8 from checkChange;
9 if vFire = 'YES' then
10 dbms_output.put_line('change');
11 update checkChange set fire = 'NO';
12 end if;
13 end;
14 /
Trigger created.
SQL> update trigger_table set status = 2;
3 rows updated.
SQL> update trigger_table set status = 3;
change
3 rows updated.
SQL>
I am facing an issue wherein thru JMeter if I try to insert same record from two different transactions and at the same time (even the same second) then duplicate records appear in a table temp_tab . Even though we have trigger deployed to to avoid duplicate records getting inserted into temp_tab table. Due to design limitation we cannot use constraints on this table.
Need your valuable suggestion on this issue.
Below is the trigger code
SELECT COUNT(1) INTO row_c
FROM temp_tab
WHERE offer_id = oiv_pkg.trig_tab(idx).offer_id
AND view_id != oiv_pkg.trig_tab(idx).view_id
AND offer_inst_id != oiv_pkg.trig_tab(idx).offer_inst_id
AND subscr_no = oiv_pkg.trig_tab(idx).subscr_no
AND subscr_no_resets = oiv_pkg.trig_tab(idx).subscr_no_resets
AND view_status IN (view_types.cPENDING, view_types.cCURRENT)
AND disconnect_reason IS NULL
AND ((oiv_pkg.trig_tab(idx).active_dt >= active_dt AND
(oiv_pkg.trig_tab(idx).active_dt < inactive_dt OR inactive_dt IS NULL)) OR
(oiv_pkg.trig_tab(idx).active_dt < active_dt AND
(oiv_pkg.trig_tab(idx).inactive_dt IS NULL OR
oiv_pkg.trig_tab(idx).inactive_dt > active_dt)));
IF row_c > 0 THEN
oiv_pkg.trig_tab.DELETE;
raise_application_error (-20001, '269901, TRIG: INSERT Failed: OID: ' || oiv_pkg.trig_tab(idx).offer_inst_id ');
END IF;
If you really want to prevent duplicates without using the proper solution, a constraint, you'd need to implement some sort of locking mechanism. In this example, I'll create a table foo with a single column col1 and create a couple of triggers that ensure that the data in col1 is unique. In order to do this, I'm introducing a new table that exists just to have its single row locked to provide a serialization mechanism. Note that I'm only handling insert operations, I'm ignoring updates that create duplicates. I'm also simplifying the problem by not bothering to track which rows are inserted in row-level triggers in order to make the final check more efficient. Of course, serializing insert operations on your table will absolutely crush you application's scalability.
SQL> create table foo( col1 number );
Table created.
SQL> create table make_application_slow(
2 dummy varchar2(1)
3 );
Table created.
SQL> insert into make_application_slow values( 'A' );
1 row created.
SQL> ed
Wrote file afiedt.buf
1 create or replace trigger trg_foo_before_stmt
2 before insert on foo
3 declare
4 l_dummy varchar2(1);
5 begin
6 -- Ensure that only one session can ever be inserting data
7 -- at any time. This is a great way to turn a beefy multi-core
8 -- server into a highly overloaded server with one effective
9 -- core.
10 select dummy
11 into l_dummy
12 from make_application_slow
13 for update;
14* end;
SQL> /
Trigger created.
SQL> create or replace trigger trg_foo_after_stmt
2 after insert on foo
3 declare
4 l_cnt pls_integer;
5 begin
6 select count(*)
7 into l_cnt
8 from( select col1, count(*)
9 from foo
10 group by col1
11 having count(*) > 1 );
12
13 if( l_cnt > 0 )
14 then
15 raise_application_error( -20001, 'Duplicate data in foo is not allowed.' );
16 end if;
17 end;
18 /
Now, if you try to insert data with the same col1 value in two different sessions, the second session will block indefinitely waiting for the first session to commit (or rollback). That prevents duplicates but it is generally hideously inefficient. And if there is any possibility that a user would be able to walk away from an active transaction, your DBA will curse you for building an application that forces them to constantly kill sessions when someone locks up the entire application because they went to lunch without committing their work.