Update statement in a trigger or a scheduled job in Oracle? - oracle

I want to update certain rows if the status of a device is changed. Here is the plain update statement:
update REMINDER set FLAG_LIST = 0 where ID in
(select r.id from REMINDER r
join DEVICE d
on d.id = (regexp_replace(r.origin_values, '[^0-9]', ''))
and d.status <> 0
and r.context = 'DEVICE'
and r.flag_list <> 0);
I wrote a trigger like below but I get an ORA-04091 (table DEVICE is mutating, trigger/function may not see ist) when i change the status of a device:
create or replace TRIGGER DEACTIVATE_REMINDER
AFTER UPDATE OF STATUS ON DEVICE
for each row
BEGIN
if updating then
if (:old.STATUS = 0 and :new.STATUS != 0) then
update REMINDER set FLAG_LIST = 0 where ID in
(select r.ID from REMINDER r
join DEVICE d
on d.id = (regexp_replace(r.ORIGIN_VALUES, '[^0-9]', ''))
and d.STATUS <> 0
and d.ID = :new.ID
and r.CONTEXT = 'DEVICE'
and r.FLAG_LIST <> 0);
end if;
end if;
END;
Maybe i can not use a trigger here, because the same object whose modification triggered the trigger must not be modified or read in a row-level trigger? Is it better to use a scheduled job somehow?
What I also want to do is to update the FLAG_LIST back to 1, if the STATUS of a device is changed back to 0. The only way i can imagine to do this is also in the trigger, but as i wrote I`m not sure if this is basically possible.
Thank you in advance!

A row-level trigger on device can't query the device table. My wager, though, is that you don't need to and can simply use the :new pseudorecord (note that the :new.status predicate in the subquery is redundant here given the if statement but doesn't hurt anything)
create or replace TRIGGER DEACTIVATE_REMINDER
AFTER UPDATE OF STATUS ON DEVICE
for each row
BEGIN
if updating then
if (:old.STATUS = 0 and :new.STATUS != 0) then
update REMINDER set FLAG_LIST = 0 where ID in
(select r.ID
from REMINDER r
where :new.id = (regexp_replace(r.ORIGIN_VALUES, '[^0-9]', ''))
and :new.STATUS <> 0
and r.CONTEXT = 'DEVICE'
and r.FLAG_LIST <> 0);
end if;
end if;
END;
If that's not what you want, it would be helpful to provide a full test case that we can run to see the behavior you want. You could create a compound trigger with a row-level component that stores the modified id values in a collection and then a statement-level component that updates the table using that collection. But that seems like overkill in this situation.
Architecturally, however, I am always really hesitant about designs that put this sort of logic in triggers. It will virtually always make more sense to have a stored procedure that updates the device table and then runs whatever update you want on the reminder table. You won't need to be concerned about a mutating table exception, all the logic will be in one place rather than partially in an application and partially in a trigger, you won't encounter problems if two months from now you want to create a trigger on the reminder table that involves queries against device, etc.

Related

Can I make an update trigger using data from one table to update another?

I'm trying to get the suspension value in my studentstaff table to change from 'no' to 'yes' when the fine (a separate table) amount reaches >=10 for a specific person. I've also tried using IF but nothings seems to be working as I keep getting this error: ORA-04079: invalid trigger specification. "amount NUMBER(8);" is in the code as it was asking me to declare amount. I am using Oracle SQL. Thanks in advance.
CREATE TRIGGER new_suspension
AFTER UPDATE ON fine
FOR EACH ROW
amount NUMBER(8);
BEGIN
CASE
WHEN Amount >= 10 THEN
UPDATE studentstaff
SET suspensions = 'yes'
WHERE library_card_no = '419746';
END CASE;
END;
/
The best option for this type of trigger is to use the when condition on trigger as follows:
CREATE OR REPLACE TRIGGER new_suspension
AFTER UPDATE ON fine
FOR EACH ROW
WHEN (NEW.AMOUNY >= 10)
BEGIN
UPDATE studentstaff
SET suspensions = 'yes'
WHERE library_card_no = :new.LIBRARY_CARD_HOLDER;
END;
/
This trigger will be executed when the condition written in the WHEN clause is satisfied.
Without seeing your table definitions its hard, but here's a guess at what you might need
CREATE OR REPLACE
TRIGGER new_suspension
AFTER UPDATE ON fine
FOR EACH ROW
BEGIN
if :new.Amount >= 10 THEN -- ie, the incoming AMOUNT on the FINE table
UPDATE studentstaff
SET suspensions = 'yes'
WHERE library_card_no = :new.LIBRARY_CARD_HOLDER -- ie, the STUDENT being fined
end if;
END;
/
Hopefully that makes sense and is close to what you're aiming to achieve here. In this example, I'm assuming that your FINE table, contains the amount and the library card holder (ie, the student etc)

Update after calculate for each record ORACLE

SELECT CIF_ID,
SUM (IN_VERIFIED_DEBT + IN_FAC_WITH_OTHER + IN_FAC_WITH_BANK)
from LOS_CIF_INDV
WHERE STATUS= 'ACTIVE'
GROUP By CIF_ID;
I want to update the total column again after the user manipulates the client as update, insert but it gives an error
ORA-04098: trigger 'RLOS138.UPDATE_IN_TOTAL_COMMIT' is invalid and failed re-validation
CREATE OR REPLACE TRIGGER UPDATE_IN_TOTAL_COMMIT
AFTER UPDATE ON
LOS_CIF_INDV
FOR EACH ROW
DECLARE
inactive_id number;
BEGIN
inactive_id:=
:new.IN_VERIFIED_DEBT + :new.IN_FAC_WITH_OTHER + :new.IN_FAC_WITH_BANK;
UPDAte LOS_CIF_INDV
SET IN_TOTAL_COMMIT = inactive_id
WHERE CIF_ID = :NEW.CIF_ID;
END ;
/
I have tried this again
CREATE OR REPLACE TRIGGER RLOS138.UPDATE_IN_TOTAL_COMMIT
AFTER UPDATE ON RLOS138.LOS_CIF_INDV
FOR EACH ROW
DECLARE
inactive_id number;
BEGIN
SELECT SUM (IN_VERIFIED_DEBT+IN_FAC_WITH_OTHER+IN_FAC_WITH_BANK)
into inactive_id
from LOS_CIF_INDV
WHERE STATUS= 'ACTIVE'
and CIF_ID=:NEw.CIF_ID;
update LOS_CIF_INDV
set IN_TOTAL_COMMIT = inactive_id
where CIF_ID = :NEW.CIF_ID;
END ;
/
yes [CIF_ID] is primary key
In which case this trigger has the logic you need:
CREATE OR REPLACE TRIGGER RLOS138.UPDATE_IN_TOTAL_COMMIT
BEFORE UPDATE ON RLOS138.LOS_CIF_INDV
FOR EACH ROW
BEGIN
if :new.status = 'ACTIVE'
then
:new.IN_TOTAL_COMMIT := :new.IN_VERIFIED_DEBT + :new.IN_FAC_WITH_OTHER + :new.IN_FAC_WITH_BANK;
end if;
END ;
/
I have included the check on status because you used it in your aggregation queries, even though you omitted from the first version of the trigger. I haven't included an ELSE branch, but you may wish to add one. Also, I have assumed that the three columns in the addition are guaranteed to be not null; if that's not the case you'll need to handle that.
I have put a working demo on db<>fiddle. This includes a version of the trigger which fires on inserts as well as updates, and handles null values too....
CREATE OR REPLACE TRIGGER UPDATE_IN_TOTAL_COMMIT
-- handle INSERT as well as UPDATE
BEFORE INSERT OR UPDATE ON LOS_CIF_INDV
FOR EACH ROW
BEGIN
if :new.status = 'ACTIVE'
then
-- handle any of these columns being null
:new.IN_TOTAL_COMMIT := nvl(:new.IN_VERIFIED_DEBT,0)
+ nvl(:new.IN_FAC_WITH_OTHER,0)
+ nvl(:new.IN_FAC_WITH_BANK,0);
end if;
END ;
/
Why not after you could explain it to me
Because Oracle have written triggers that way: the AFTER EACH ROW trigger uses the finalised version of the record, the state which will be written to the database. Consequently, if we want to change any values we need to use a BEFORE EACH ROW trigger. Oracle enforces this with the error you got, ORA-04084: cannot change NEW values for this trigger type.
Just a reminder: ORA-04098 is telling you there are compilation errors in your trigger code. If you're not using an IDE which tells you what these errors are you can find them with this query:
select * from all_errors
where owner = 'RLOS138'
and name = 'UPDATE_IN_TOTAL_COMMIT' ;
(Not sure if you're connecting as RLOS138 - if you are, query USER_ERRORS instead.)
If I understood correctly, You want to update all the records having CIF_ID as an updated record with the same value in the IN_TOTAL_COMMIT column.
This is not a good idea. If you have some derived column then you should use the views instead of updating its value for every insert/update using the trigger.
If you really want to update the column then you must use the combination of Row level trigger, Statement trigger, and package variables. (Search for mutating table error in the SO)
But according to me, the best solution is to use the view, something like follows:
CREATE OR REPLACE VIEW LOS_CIF_INDV_VW AS
SELECT L.*,
COALESCE(
SUM(
CASE
WHEN STATUS = 'ACTIVE' THEN
IN_VERIFIED_DEBT + IN_FAC_WITH_OTHER + IN_FAC_WITH_BANK
END
) OVER(
PARTITION BY L.CIF_ID
),
0
) AS IN_TOTAL_COMMIT
FROM LOS_CIF_INDV L;

How to solve this exercise about Oracle Triggers

I have to solve this exercise about triggers:
Consider the following relational database schema used to represent
project information:
Person (ID, Surname, Name, Nationality)
Project (Name, Manager,
StartingYear, NumPeopleInvolved, International)
Personnel (Project, PersonID)
Specify the triggers required in Oracle to maintain the following
integrity constraints:
a) The number of people involved in a project (attribute
NumPeopleInvolved) must be consistent with the number of tuples
entered in Personnel for that project
b) If the project is international (the International attribute
assumes only two values) then the project must involve at least two
people of different nationalities
I have a problem with the b) part.
I don't know how to handle the case in which a given Project has no people involved. If I try to insert the first people, I can not have two people of different nationalities since I have only one people.
How should this situation be handled?
Should I use a statement level trigger? I have not experience with triggers so I still haven't understood well what I can / I can't do with one kind of trigger.
I tried this way but it's clearly not working as it should:
CREATE TRIGGER InsertPersonnelInternational
AFTER INSERT ON Personnel
FOR EACH ROW
BEGIN
SELECT ProjectName
FROM Personnel INNER JOIN Project
WHERE PersonID = :new.ID Project = Name
SELECT International
FROM Personnel INNER JOIN Project
ON Project = Name
SELECT COUNT(*) AS NumPersonnel
FROM Personnel
WHERE Project = :new.Project
IF NumPersonnel >= 1 THEN
BEGIN
SELECT COUNT(*) AS NumNationalities
FROM Personnel INNER JOIN Person
ON Project = ProjectName
GROUP BY Nationality
IF International THEN
IF NumNationalities = 1 Then
BEGIN
raise_application_error(-1)
END
ELSE
IF NumNationalities <> 1 THEN
BEGIN
raise_application_error(-1)
END
END
END
END
When you have a row-level trigger on table Personnel then you cannot run any SELECT on table Personnel within the trigger - you will get an ORA-04091: table PERSONEL is mutating ... error.
I think your teacher is expecting something like this:
CREATE TRIGGER ProjectConsistency
BEFORE INSERT OR UPDATE ON PROJECT
FOR EACH ROW
p_count INTEGER;
n_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO p_count
FROM Personnel
WHERE PROJECT = :new.NAME;
IF :new.NumPeopleInvolved <> p_count THEN
RAISE_APPLICATION_ERROR(-20010, 'The number of people involved in a project must be consistent with the number of tuples entered in Personnel for that project');
END IF;
IF :new.International = 'YES' THEN
SELECT COUNT(DISTINCT Nationality)
INTO n_count
FROM Personnel
WHERE PROJECT = :new.NAME;
IF n_count < 2 THEN
RAISE_APPLICATION_ERROR(-20010, 'The project must involve at least two people of different nationalities')
END IF;
END IF;
END;
In reality you would not implement such requirement with a trigger, you would use a PL/SQL procedure.
Attribute NumPeopleInvolved is useless, i.e. redundant. Typically you would solve it by
UPDATE PROJECT proj
SET NumPeopleInvolved =
(SELECT COUNT(*)
FROM Personnel p
WHERE PROJECT = :new.NAME)
WHERE NAME = :new.NAME;
Such an update could be done by a trigger, for example.
Actually you would need similar triggers also on table Personnel and Person, because the personel/persons may change and project would become inconsistent. I don't know whether this should be considered by the exercise.
Imagine, on person gets released, i.e. deleted from table Person:
would the application raise an error - person cannot be released (what happens if the person dies by Corona :-))?
would be project be invalid?
would be project be automatically updated?
Then, you should never raise errors like raise_application_error(-1) - always let the user know what went wrong!
The best way to do this is with a compound trigger. With a compound trigger we avoid the problem of mutating tables which we would get from a row level trigger on PERSONNEL.
We keep track of each project which is referenced by each affected row in a DML statement (insert, update, delete) in an array. At the end of the statement we query those projects to find whether the project is international, and if it is to check the nationalities of its assigned personnel.
It might look like this:
CREATE OR REPLACE TRIGGER international_project_trg
FOR insert or update or delete ON personnel
COMPOUND TRIGGER
-- Global declaration
type project_t is table of number index by personnel.project%type;
g_project project_t;
BEFORE EACH ROW IS
BEGIN
CASE
-- we don't care about the value here, we just what a set of distinct projects
WHEN INSERTING THEN
g_project(:new.project) := 1;
WHEN UPDATING THEN
g_project(:new.project) := 1;
WHEN DELETING THEN
g_project(:old.project) := 1;
END CASE;
END BEFORE EACH ROW;
AFTER STATEMENT IS
l_project personnel.project%type;
l_country_cnt pls_integer;
l_people_cnt pls_integer;
BEGIN
l_project := g_project.first();
while l_project is not null loop
select count(distinct ppl.nationality)
,count(*)
into l_country_cnt
,l_people_cnt
from personnel per
join project prj on per.project = prj.name
join person ppl on per.personid = ppl.id
where per.project = l_project
and prj.international = 'Y';
if l_people_cnt <= 1 then
-- either not international project or only one assigned person
-- so we don't care
null;
elsif l_country_cnt <= 1 then
raise_application_error(-20999, l_project ||' must have multi-national team membership');
end if;
l_project := g_project.next(l_project);
end loop;
END AFTER STATEMENT;
END international_project_trg;
Here is a working demo on db<>fiddle. You can see that although the trigger allows for an international project to have only one assigned person it throws an error when we add a second person of the same nationality. We can solve this by inserting rows in a special order, or better by inserting a set of rows. This is a problem with apply such business rules.
You can use the same approach (in the same trigger) to check that whether the number of assigned personnel meets the Project.NumPeopleInvolved rule.
Note: compound triggers arrived in Oracle 11gR1.
I think the following should work with insertions, deletions and updates on table Personnel. It simply check and update the international consistency for each project whether the table Personnel is altered.
CREATE TRIGGER UpdateInternationalProject
AFTER INSERT OR UPDATE OR DELETE ON Personnel
BEGIN
SELECT name, international
FROM Project
AS ProjectInternational;
FOR projectInfo IN ProjectInternational
LOOP
SELECT COUNT(DISTINCT nationality)
AS numNationalities
FROM Personnel INNER JOIN Person
ON personId = id
WHERE project = projectInfo.name;
IF numNationalities = 1 THEN
IF projectInfo.international THEN
UPDATE Project
SET international = 0
WHERE name = projectInfo.name;
END IF;
ELIF numNationalities > 1 THEN
IF NOT projectInfo.international THEN
UPDATE Project
SET international = 1
WHERE name = projectInfo.name;
END IF;
END IF;
END LOOP;
END;

Oracle Trigger Performance is to slow

Given the following trigger:
CREATE OR REPLACE TRIGGER TR_MY_TRG_NAME
AFTER UPDATE OF COL_A, COL_B, COL_C ON T_MY_TABLE_Y
FOR EACH ROW
BEGIN
UPDATE T_MY_TABLE_X X
SET
X.COL_A = :NEW.COL_A,
X.COL_B = :NEW.COL_B,
X.COL_C = :NEW.COL_C
WHERE
X.ID = :NEW.ID;
END;
... and given 2 million existing records in T_MY_TABLE_Y.
Problem:
if my app is changing all of the 2 mio records (e.g. COL_A), then without the trigger it runs 2-3 minutes, but with the trigger it took 40min.
Question:
are there some alternative approaches that I could try?
An alternative is to update T_MY_TABLE_X in a single statement, without forcing a trigger to fire for each of 2 million rows and (probably) perform context switching.
So: as you update T_MY_TABLE_Y, reuse the same UPDATE for T_MY_TABLE_X (with some modifications, if necessary).
I think you might benefit to decrease the load for DML by splitting into three parts so as to update for each individual columns :
CREATE OR REPLACE TRIGGER TR_MY_TRG_NAME
AFTER UPDATE OF COL_A, COL_B, COL_C ON T_MY_TABLE_Y
FOR EACH ROW
BEGIN
IF :NEW.COL_A != :OLD.COL_A THEN
UPDATE T_MY_TABLE_X SET COL_A = :NEW.COL_A WHERE ID = :NEW.ID;
END IF;
IF :NEW.COL_B != :OLD.COL_B THEN
UPDATE T_MY_TABLE_X SET COL_B = :NEW.COL_B WHERE ID = :NEW.ID;
END IF;
IF :NEW.COL_C != :OLD.COL_C THEN
UPDATE T_MY_TABLE_X SET COL_C = :NEW.COL_C WHERE ID = :NEW.ID;
END IF;
END;
This way, Update might not incur every column for each occurence.
Moreover, be sure T_MY_TABLE_X.ID has index on it, preferably Unique Index.
I don't have the time to write the code out, but an outline approach I can suggest trying is to create a package with a ARRAY of a RECORD of ID, COL_A, COL_B and COL_C.
The BEFORE statement trigger should instantiate and initialise the package and the array within, e.g.:
the_package.pr_init
The ROW LEVEL trigger should simply write the :NEW.ID, :NEW.COL_A, :NEW.COL_B, :NEW.COL_C to the array within the package, e.g:
the_package.pr_save ( :NEW.ID, :NEW.COL_A, :NEW.COL_B, :NEW.COL_C )
The AFTER statement trigger should then issue a BULK UPDATE on the driving off the array within the package, and clear out the ARRAY, eg:
the_package.pr_do_update.
The benefit of this approach is you only execute ONE additional UPDATE statement regardless of how many rows.
The solution is also contained with in a single package, albeit splayed across 3 triggers, though the trigger code itself will be much simplified.

PL/SQL Update Trigger Updates All Rows

New to working with PL/SQL and trying to create a statement level trigger that will change the 'Reorder' value to 'Yes' when the product quantity (p_qoh) is either less than 10 or less than two times the product minimum (p_min). And if that's not the case, then to change the 'Reorder' value to 'No'. My problem is that when I perform an update for a specific product, it changes the reorder value of all rows instead of the one I'm specifying. Can't seem to figure out where I'm going wrong, think I've been staring at it too long, any help is greatly appreciated.
CREATE OR REPLACE TRIGGER TRG_AlterProd
AFTER INSERT OR UPDATE OF p_qoh, p_min ON product
DECLARE
v_p_min product.p_min%type;
v_p_qoh product.p_qoh%type;
CURSOR v_cursor IS SELECT p_min, p_qoh FROM product;
BEGIN
OPEN v_cursor;
LOOP
FETCH v_cursor INTO v_p_min, v_p_qoh;
EXIT WHEN v_cursor%NOTFOUND;
IF v_p_qoh < (v_p_min * 2) OR v_p_qoh < 10 THEN
UPDATE product SET p_reorder = 'Yes';
ELSE
UPDATE product SET p_reorder = 'No';
END IF;
END LOOP;
END;
/
The update command :
UPDATE product SET p_reorder = 'Yes';
updates all of your rows because you are not specifying a WHERE clause.
What you can do is to retrieve the product's id (product_id) using your cursor and save it so that you would use it this way:
UPDATE product SET p_reorder = 'Yes' WHERE id = product_id;
Whoaa, this is not how you do triggers.
1 - Read the Oracle Trigger Documentation
2 - (almost) Never do a commit in a trigger. That is the domain of the calling application.
3 - There is no need to select anything related to product. You already have the product record at hand with the :new and :old pseudo records. Just update the column value in :new as needed. Example below (not checked for syntax errors, etc.);
CREATE OR REPLACE TRIGGER TRG_AlterProd
BEFORE INSERT OR UPDATE OF p_qoh, p_min ON product
FOR EACH ROW
BEGIN
IF :new.p_qoh < (:new.p_min * 2) OR :new.p_qoh < 10 THEN
:new.p_reorder = 'Yes';
ELSE
:new p_reorder = 'No';
END IF;
END;
#StevieP, If you need to commit inside a trigger, you may want to consider doing it as Autonomous Transaction.
Also, sorry if my understanding of your problem statement is wrong, but your it sounded to me like a row level trigger - are you only updating the current row or are you scanning the entire table to change status on several rows? If it's on current row, #OldProgrammer's solution seems right.
And I am just curious, if you do an UPDATE statement inside the trigger on the same table, wouldn't it generate (recursive) trigger(s)? I haven't done statement triggers like this, so sorry if this is not the expected trigger behavior.
To me a statement trigger would make more sense, if the trigger was on say, sales table, when a product is sold (inserted into sales table), it will trigger the corresponding product id records to be updated (to REORDER) in Product table. That will prevent recursion danger also.

Resources