ORACLE "before update" trigger does not fire when column is changed within another trigger - oracle

I am using ORACLE 12c.
On a table I have 2 triggers, both "before update".
One of the triggers fires while updating a column and within this trigger another column gets a new value. The second trigger should fire while updating this second column. But he did not.
create table TRIGGER_TEST
(
col1 varchar2(64),
col2 varchar2(64),
col3 varchar2(64)
);
create or replace trigger TR_TRIGGER_TEST_1
before update of COL1 on TRIGGER_TEST
for each row
begin
dbms_output.put_line('here we are in TR_TRIGGER_TEST_1');
:new.col2 := 'only testing';
end;
/
create or replace trigger TR_TRIGGER_TEST_2
before update of COL2 on TRIGGER_TEST
for each row
begin
dbms_output.put_line('here we are in TR_TRIGGER_TEST_2');
:new.col3 := 'trigger_test_2 has fired';
end;
/
insert into TRIGGER_TEST values ('1_col1','1_col2','1_col3');
select * from TRIGGER_TEST;
COL1 COL2 COL3
----------------------------------------------------------------
1_col1 1_col2 1_col3
After I have inserted the row I perform an UPDATE. And I expect COL1=
"now we will see", COL2="only testing" and COL3 = "trigger_test_2 has fired".
update TRIGGER_TEST set COL1 = 'now we will see';
But what I get is this:
select * from TRIGGER_TEST;
COL1 COL2 COL3
----------------------------------------------------------------
now we will see only testing 1_col3
Can anybody explain this to me? I am really sure, that with former ORACLE versions this szenario has worked. But now it does not.

I am really sure, that with former ORACLE versions this szenario has worked.
It has not. I ran your code in 11gR2 and got the same result:
set serveroutput on
update TRIGGER_TEST set COL1 = 'now we will see';
here we are in TR_TRIGGER_TEST_1
1 row updated.
select * from TRIGGER_TEST;
COL1 COL2 COL3
------------------------------ ------------------------------ ------------------------------
now we will see only testing 1_col3
The before update of COL2 on TRIGGER_TEST is a DML event clause. You are creating simple DML triggers:
A DML trigger is created on either a table or view, and its triggering event is composed of the DML statements DELETE, INSERT, and UPDATE. ...
When you issue your update that DML causes the first trigger to fire. But when you assign a new value inside that trigger:
:new.col2 := 'only testing';
.. that is not a DML statement - it is not a separate update.
If assigning a value in that way did cause a trigger to fire, then if you instead did:
:new.col1 := 'something';
... then that first trigger would fire again, recursively, until you hit the error ORA-00036: maximum number of recursive SQL levels (50) exceeded. That would obviously be bad.
You will have to repeat the assignment of col3 in the first trigger if that is what you need to happen. For more complicated side-effects that you want to happen whether you hit either trigger, you could have a procedure that does any necessary actions (that don't affect this table), and then call that from both triggers. Although then you'd need a mechanism to make sure the procedure isn't called twice if a DML update touches both columns - which would cause both triggers to fire.

"update of COL2" in a trigger would mean update using a SQL statement such as UPDATE or MERGE, and not otherwise. Why dont you code the second trigger in the first ?

Related

Trying to delete a row based upon condition defined in my trigger (SQL)

I am trying to create a row level trigger to delete a row if a value in the row is being made NULL. My business parameters state that if a value is being made null, then the row must be deleted. Also, I cannot use a global variable.
BEGIN
IF :NEW.EXHIBIT_ID IS NULL THEN
DELETE SHOWING
WHERE EXHIBIT_ID = :OLD.EXHIBIT_ID;
END IF;
I get the following errors:
ORA-04091: table ISA722.SHOWING is mutating, trigger/function may not see it
ORA-06512: at "ISA722.TRG_EXPAINT", line 7
ORA-04088: error during execution of trigger 'ISA722.TRG_EXPAINT'
When executing this query:
UPDATE SHOWING
SET EXHIBIT_ID = NULL
WHERE PAINT_ID = 5104
As already indicated this is a terrible idea/design. Triggers are very poor methods for enforcing business rules. These should be enforced in the application or better (IMO) by a stored procedure called by the application. In this case not only is it a bad idea, but it cannot be implemented as desired. Within a trigger Oracle does not permit accessing the table the trigger fired was fired on. That is what mutating indicates. Think of trying to debug this or resolve a problem a week later. Nevertheless this non-sense can be accomplished by creating view and processing against it instead of the table.
-- setup
create table showing (exhibit_id integer, exhibit_name varchar2(50));
create view show as select * from showing;
-- trigger on VIEW
create or replace trigger show_iiur
instead of insert or update on show
for each row
begin
merge into showing
using (select :new.exhibit_id new_eid
, :old.exhibit_id old_eid
, :new.exhibit_name new_ename
from dual
) on (exhibit_id = old_eid)
when matched then
update set exhibit_name = new_ename
delete where new_eid is null
when not matched then
insert (exhibit_id, exhibit_name)
values (:new.exhibit_id, :new.exhibit_name);
end ;
-- test data
insert into show(exhibit_id, exhibit_name)
select 1,'abc' from dual union all
select 2,'def' from dual union all
select 3,'ghi' from dual;
-- 3 rows inserted
select * from show;
--- test
update show
set exhibit_name = 'XyZ'
where exhibit_id = 3;
-- 1 row updated
-- Now for the requested action. Turn the UPDATE into a DELETE
update show
set exhibit_id = null
where exhibit_name = 'def';
-- 1 row updated
select * from show;
-- table and view are the same (expect o rows)
select * from show MINUS select * from showing
UNION ALL
select * from showing MINUS select * from show;
Again this is a bad option yet you can do. But just because you can doesn't mean you should. Or that you'll be happy with the result. Good Luck.
You have written a trigger that fires after or before a row change. This is in the middle of an execution. You cannot delete a row from the same table in that moment.
So you must write an after statement trigger instead that only fires when the whole statement has run.
create or replace trigger mytrigger
after update of exhibit_id on showing
begin
delete from showing where exhibit_id is null;
end mytrigger;
Demo: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=dd5ade700d49daf14f4cdc71aed48e17
What you can do is create an extra column like is_to_be_deleted in the same table, and do this:
UPDATE SHOWING
SET EXHIBIT_ID = NULL, is_to_be_deleted = 'Y'
WHERE PAINT_ID = 5104;
You can use this parameter to implement your business logic of not showing the null details.
And later you can schedule a batch delete on that table to clean up these rows (or maybe archive it).
Benefit: you can avoid an extra unnecessary trigger on that table.
Nobody, will suggest you to use trigger to do this type of delete as it is expensive.

Trigger to update week of the year

I want to write a trigger so that when decom_date is inserted or updated the week of the year is updated to the corresponding value.
This is what I have so far, but after inserting a date the week is still null.
create or replace trigger test_trigger
before insert on check_decom
for each row
begin
if inserting then
update check_decom set decom_week= (select to_char(to_date(decom_date,'DD-
MON-YY'),'WW') as week from check_decom) ;
end if;
end;
/
SQL> select * from check_decom;
DECOM_DATE DECOM_WEEK
------------------------------ ----------
23-JUN-17
What am I doing wrong?
Example for Week of a year
SQL> select to_char(to_date(sysdate,'DD-MON-YY'),'WW') as week from dual;
WE
--
28
You're doing a couple of things wrong, starting with date handling. Your decom_date column should be defined as a DATE column - it looks like it might be a string in your sample output. But your handling with sysdate is also wrong, as you're implicitly converting to a string in order to convert it back to a date, which is both pointless and prone to error as this might happen in a session which has different NLS settings. If your column is actually a DATE then you should not be calling to_date() against that either; and if it is a string then that conversion is valid but it should be a DATE.
Then your trigger is querying and trying to update the table that the trigger is against. With no data that doesn't error but doesn't do anything as there is no existing row to update - the one you are inserting doesn't exist yet. If there was data you would get a mutating table error, if you didn't get a too-many-rows exception from the select part.
Row-level triggers can access NEW and OLD pseudorecords to see and manipulate the affected row; you don't need to (and generally can't) use DML queries to access the data in the row you're manipulating.
If your table was defined with a date column and a number column:
create table check_decom(decom_date date, decom_week number);
then your trigger might look something like:
create or replace trigger test_trigger
before insert on check_decom
for each row
begin
if inserting then
:new.decom_week := to_number(to_char(:new.decom_date, 'WW'));
end if;
end;
/
although the if inserting check is a bit pointless as the trigger will only fire on insert anyway. Which in itself might be an issue; you perhaps want it to be set on update as well, but the logic the same, so would be:
create or replace trigger test_trigger
before insert or update on check_decom
for each row
begin
:new.decom_week := to_number(to_char(:new.decom_date, 'WW'));
end;
/
which does what you want:
insert into check_decom (decom_date) values (date '2017-06-23');
1 row inserted.
select * from check_decom;
DECOM_DAT DECOM_WEEK
--------- ----------
23-JUN-17 25
But I wouldn't do this with a trigger at all. From Oracle 11g you can use a virtual column instead:
create table check_decom (
decom_date date,
decom_week generated always as (to_number(to_char(decom_date, 'WW')))
);
Table CHECK_DECOM created.
insert into check_decom (decom_date) values (date '2017-06-23');
1 row inserted.
select * from check_decom;
DECOM_DAT DECOM_WEEK
--------- ----------
23-JUN-17 25

How to create a trigger which fires when any insert or update is done in a table?

I have created the below trigger, but it is not getting fired after i am doing new insert/update in to the mentioned table:
CREATE OR REPLACE TRIGGER ref_upd_user_phi_details
AFTER
INSERT OR UPDATE --of emp_email_address, ssn_nb
ON ref_adp_employees
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
BEGIN
UPDATE ref_adp_employees
SET emp_email_address = 'QA_' ||emp_email_address,
ssn_nb = nvl2(ssn_nb, NULL, '123-45-6789')
WHERE upper(emp_email_address) NOT LIKE 'QA_%'
AND upper(emp_email_address) LIKE '%#KEENAN.COM';
exception
WHEN others
THEN
NULL;
END;
Can someone please suggest me what i am missing?
As #phonetic_man pointed out, you are hiding any error you get by catching when others and taking no action. Without the exception block you would see that you are causing a mutating table error (ORA-04091), because you are referring to the same table the trigger is against.
If you took out the for each row part to turn it into a statement-level trigger then you would avoid that issue, but now you would have an infinite loop (ORA-00036) - when you try to update the table from within the trigger, that update itself causes the same trigger to fire again; which tries to update the same table yet again, which causes the trigger to fire yet again; etc. until Oracle notices and kills the process.
It would make more sense to use a before-insert row-level trigger to make sure the new values for the row match whatever pattern you are trying to enforce. Maybe something like:
CREATE OR REPLACE TRIGGER ref_upd_user_phi_details
BEFORE INSERT OR UPDATE --of emp_email_address, ssn_nb
ON ref_adp_employees
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
BEGIN
IF upper(:NEW.emp_email_address) NOT LIKE 'QA_%'
AND upper(:NEW.emp_email_address) LIKE '%#KEENAN.COM'
THEN
:NEW.emp_email_address := 'QA_' || :NEW.emp_email_address;
:NEW.ssn_nb := CASE WHEN :NEW.ssn_nb IS NULL THEN '123-45-6789' END;
END IF;
END;
/
And to see what it does:
insert into ref_adp_employees (emp_id, emp_email_address, ssn_nb) values (1, 'TEST_1', '123-45-6789');
insert into ref_adp_employees (emp_id, emp_email_address, ssn_nb) values (2, 'TEST_1#KEENAN.COM', '123-45-9876');
insert into ref_adp_employees (emp_id, emp_email_address, ssn_nb) values (3, 'QA_TEST_1', null);
select emp_id, emp_email_address, ssn_nb from ref_adp_employees;
EMP_ID EMP_EMAIL_ADDRESS SSN_NB
---------- ------------------------------ -----------
1 TEST_1 123-45-6789
2 QA_TEST_1#KEENAN.COM
3 QA_TEST_1
Not sure if you really intended to replace set SSNs with null, and turn nulls into the fixed value; I suspect you are really trying to replace set values with the fixed string and leaves nulls alone, in which case it would be:
:NEW.ssn_nb := CASE WHEN :NEW.ssn_nb IS NOT NULL THEN '123-45-6789' END;
You might also want to move that outside the IF block, so it's done regardless of the email address; I've replicated what your original code was trying to do but that might not be right.
If you have existing data that you want to modify to match these changes, do a one-off update of the whole table - don't try to do that inside a trigger.
Have you heard of Google? There's a myriad of answers and examples out there for triggers, but some things I see at this time:
1) Change trigger to BEFORE insert or update. Use AFTER when making changes to another table, or to run some subsequent process on the table.
2) Take out the comment to the individual fields being changed or added. That was good AFAIK.
3) In your body of the trigger use WHEN INSERTING and WHEN UPDATING. Or if you are just updating the table, change DDL to BEFORE UPDATE only.
4) In the update, reference with set :new.emp_email_address = 'QA_' || :old.emp_email_address ... and so on and so forth. That's where that old as old and new as new becomes important.
Kindly check if the trigger is valid or not by firing the below query..
SELECT *
FROM ALL_OBJECTS
WHERE OBJECT_NAME = trigger_name
AND OBJECT_TYPE = 'TRIGGER'
AND STATUS <> 'VALID'
The trigger is firing after update..
try before update
CREATE OR REPLACE TRIGGER ref_upd_user_phi_details
Before
INSERT OR UPDATE

Use of ORA_ROWSCN to find current and immediate previous transaction

I have table TEST_TABLE having 3 columns COL1(Number), COL2(Varchar), COL3(Date).
I want to find the result of the below query in an anonymous block for two different transactions. One for the current transaction and the other for the immediately previous transaction. How to do it using SCN?
BEGIN
Select col3 into v_curr_date from test_table where col1=123; --Current
uncommitted Transaction
Select col3 into v_prev_date from test_table where col1=123; --How to
modify this query to find for immediately Previous committed Transaction.
END ;
Below one will gives you the latest committed date happened on a table. SCN_TO_TIMESTAMP takes as an argument a number that evaluates to a system change number (SCN), and returns the approximate timestamp associated with that SCN. So modify your query accordingly.
DECLARE
v_prev_date date;
BEGIN
Select SCN_TO_TIMESTAMP(ORA_ROWSCN) into v_prev_date
from test_table where col1=123;
dbms_output.put_line(v_prev_date );
end;

Update same table after Insert trigger

I am working on a product in which I have to send SMS to concerned person when someone waits for more than 15 minutes for being served.
For that I have written a procedure that watches a table and stores CUST_ID, CUST_CATEGORY, DURATION in a separate table when the Duration exceeds 15. The table structure of this table is:
Some_Table
CUST_ID CUST_CATEGORY DURATION SMS_STATUS
I wrote a trigger as:
Trigger
create or replace trigger kiosk_sms_trg
after insert on Some_Table
referencing new as new old as old
for each row
BEGIN
SMS_Proc#My_Server; --Procudure that generates SMS
update Some_Table set status = 'Y' where id = (select max(id) id from Some_Table where status = 'N'); --Update Table that SMS has been sent
select 'Y' into :new.status from dual;
END;
But it creates Mutation Problem. How do I resolve it? Any help would be highly appreciated. I'm using Oracle 11G.
I don't think that UPDATE is allowed on SOME_TABLE as it is currently mutating.
Why not place it right after the INSERT statement which fired the trigger in the first place?.
INSERT INTO SOME_TABLE ...
update Some_Table set status = 'Y' where id = (select max(id) id from Some_Table where status = 'N'); --Update Table that SMS has been sent
I guess this would be the right approach considering you aren't doing anything row specific in that UPDATE.
As I mentioned in the comment, Is there any particular use for this last statement in the AFTER INSERT trigger? It does have meaning in the BEFORE INSERT trigger.
select 'Y' into :new.status from dual;
You cannot update the same table in Row-Level AFTER Trigger.
Change your Row-Level AFTER INSERT trigger to row-level BFEORE INSERT trigger.
But you UPDATE stmt inside the trigger will not effect the new record being inserted.
Wonder how it can be done, this is tricky.

Resources