How to have trigger fire after a (conditional) insert in Oracle? - oracle

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>

Related

How do i Solve Oracle Mutation Error from TRIGGER

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.

PLSQL trigger not working 0 rows inserted

Whatever I do to the trigger always returns "0 rows inserted".
It is like it can't find the new values after inserting them.
After adding the exception it return no_data_found and i don't know why.
before insert or update of rent_date, return_date on rent
for each row
declare
pragma AUTONOMOUS_TRANSACTION;
v_rentDate date;
v_returnDate date;
begin
select rent_date
into v_rentDate
from rent
where rent_date = :new.rent_date;
select return_date
into v_returnDate
from rent
where return_date = :new.return_date;
if v_returnDate < v_rentDate then
raise_application_error(-20158, 'Return date must be after the rent date');
else
dbms_output.put_line('TEST');
end if;
exception when no_data_found then raise_application_error(-20157, 'No data found');
end;
/
insert into rent values (82,sysdate-5,101,sysdate,sysdate+5,100);
--0 rows inserted
It appears that you're doing it the wrong way. Here's why:
you are trying to select values from a table you're currently inserting into (or updating existing values)
Oracle complains that it can't do that because the table is mutating
in order to "fix" it, you used pragma autonomous_transaction which isolates trigger code from the main transaction
You shouldn't use that pragma for such a purpose. Lucky you, trigger can be rewritten in a simpler manner, the one that doesn't cause the mutating table error. As you want to compare rent_date and return_date, do it directly. Here's an example (see line #5):
SQL> create table rent
2 (id number,
3 rent_date date,
4 return_date date
5 );
Table created.
SQL> create or replace trigger trg_biu_rent
2 before insert or update on rent
3 for each row
4 begin
5 if :new.return_date < :new.rent_date then
6 raise_application_error (-20158, 'Return date must be after the rent date');
7 end if;
8 end;
9 /
Trigger created.
Testing:
SQL> -- This will fail
SQL> insert into rent (id, rent_date, return_date) values
2 (1, date '2019-05-25', date '2019-04-10');
insert into rent (id, rent_date, return_date) values
*
ERROR at line 1:
ORA-20158: Return date must be after the rent date
ORA-06512: at "SCOTT.TRG_BIU_RENT", line 3
ORA-04088: error during execution of trigger 'SCOTT.TRG_BIU_RENT'
SQL> -- This is OK
SQL> insert into rent (id, rent_date, return_date) values
2 (1, date '2019-03-28', date '2019-10-20');
1 row created.
SQL>

How to update another table's value using insertion trigger of another table

I am working on a database project on national election system . While casting vote for a certain candidate I want to automatically update total_votes +1 in the results table . The total_votes of all candidates is initiated at zero. I wrote a trigger but it isn't working
/* I tried this trigger*/
create or replace Trigger tvc
after insert
on cast_vote
Declare
for each row
begin
update results r
set total_vote = total_vote+1
where :old.can_id=r.can_id;
end tvc ;
/* This are the table's used */
create table candidate(
can_id number(8),
name varchar(256),
age number(3) check (age>=18),
gender varchar(7),
aff_party varchar(256),
seat_no number(5),
seat_name varchar(256),
net_income number(8),
primary key(can_id)
);
create table cast_vote(
vote_no number(15) not null,
voter_id number(8) not null unique,
can_id number(8),
primary key(vote_no),
foreign key (voter_id) references voter(voter_id),
foreign key(can_id) references candidate(can_id)
);
create table results (
can_id number(10) primary key,
total_vote number(10),
foreign key (can_id) references candidate(can_id)
);
/I want the trigger to work/
Try this , It should work
CREATE OR REPLACE TRIGGER tvc
AFTER INSERT ON cast_vote
REFERENCING NEW AS newRow OLD AS oldRow
FOR EACH ROW
BEGIN
UPDATE results SET total_vote =(select max(total_vote) from results where
can_id=:newRow.can_id)+1 where
can_id=:newRow.can_id;
END tvc;
EDIT :
Also , This will handle if there are no rows in the RESULTS table initially
CREATE OR REPLACE TRIGGER tvc
AFTER INSERT ON cast_vote
REFERENCING NEW AS newRow OLD AS oldRow
FOR EACH ROW
DECLARE
initialcnt number;
BEGIN
SELECT COUNT(*) INTO initialcnt FROM results;
IF(initialcnt=0) THEN
INSERT INTO results VALUES(:newRow.can_id,1);
ELSE
UPDATE results SET total_vote =(select max(total_vote) from
results where can_id=:newRow.can_id)+1 where can_id=:newRow.can_id;
END tvc;
Initially, there are no rows in the RESULTS table so there's nothing to update; insert a row first.
SQL> create table cast_vote(
2 vote_no number(15) not null,
3 voter_id number(8) not null unique,
4 can_id number(8),
5 primary key(vote_no)
6 );
Table created.
SQL> create table results (
2 can_id number(10) primary key,
3 total_vote number(10)
4 );
Table created.
SQL> create or replace Trigger tvc
2 before insert on cast_vote
3 for each row
4 begin
5 update results r set
6 r.total_vote = r.total_vote + 1
7 where r.can_id = :new.can_id;
8
9 if sql%rowcount = 0 then
10 -- the first row for CAN_ID - insert
11 insert into results (can_id, total_vote)
12 values (:new.can_id, 1);
13 end if;
14 end tvc ;
15 /
Trigger created.
Testing:
SQL> insert into cast_vote (vote_no, voter_id, can_id) values (1000, 1, 200);
1 row created.
SQL> insert into cast_vote (vote_no, voter_id, can_id) values (1001, 2, 200);
1 row created.
SQL> select * From cast_Vote;
VOTE_NO VOTER_ID CAN_ID
---------- ---------- ----------
1000 1 200
1001 2 200
SQL> select * From results;
CAN_ID TOTAL_VOTE
---------- ----------
200 2
SQL>
If "total_votes of all candidates is initiated at zero' means that there is a row for each candidate with a 0 vote tally initially defined then the following works (almost what you had): 1. Remove the declare, it unnecessary, or reverse it and "for each row".
2. change :old.can_id=r.can_id; to :new.can_id=r.can_id; In an insert trigger :old doesn't exist (you can reference it but all columns are null).
If there is the possibility that the total votes is not initialized then use Merge.
create or replace Trigger tvc
before insert
on cast_vote
for each row
begin
merge into results r
using (select :new.can_id newid from dual)
on (newid = r.id)
when matched then
update set total_vote = total_vote+1
when not matched then
insert (id,total_vote) values(newid,1);
end tvc;

Prevent parallel exection of procedure

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>

Oracle database on trigger fail rollback

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 ).

Resources