How to implement AFTER INSERT Trigger in Oracle PL/SQL? - sql

I am trying to implement after insert trigger in PLSQL. The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
I am struggling with implementing warning-free query, which causes error during insertion. How could I manage this?
Here is my implemented trigger which I guess needs some changes.
CREATE TRIGGER blatrigger
AFTER INSERT
ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT (exception_name, -20999);
BEGIN
if (select count(*) as counter from BLATABLE where CLIENTID = :NEW.CLIENTID and STATUS='PENDING').counter > 1
THEN
raise exception_name;
END IF;
END;
Here is the table itself:
create table BLATABLE
(
ID NUMBER(19) not null primary key,
CLIENTID NUMBER(10),
CREATED TIMESTAMP(6),
STATUS VARCHAR2(255 char)
);

The goal is to check if there are multiple (>1) rows having specific status for each client. If so I'd like to rise an exception and roll the insertion back.
No need for a trigger. It looks like a simple unique constraint should get the job done here:
create table blatable (
id number(19) not null primary key,
clientid number(10),
created timestamp(6),
status varchar2(255 char),
constraint blaconstraint unique (clientid, status)
);
The unique constraint prevents duplicates on (clientid, status) across the whole table. If a DML operation (insert, update) attempts to generate a duplicate, an error is raised and the operation is rolled back.
If, on the other end, you want to allow only one "PENDING" status per user, then you can use a unique index as follows:
create unique index bla_index
on blatable( (case when status = 'PENDING' then clientid end) );

Use a Statement Level Trigger, rather than a Row Level by removing FOR EACH ROW, and converting to your code as below :
CREATE OR REPLACE TRIGGER blatrigger
AFTER INSERT ON BLATABLE
REFERENCING NEW AS NEW OLD AS OLD
DECLARE
counter INT;
exception_name EXCEPTION;
PRAGMA EXCEPTION_INIT(exception_name, -20999);
BEGIN
SELECT MAX(COUNT(*))
INTO counter
FROM BLATABLE
WHERE STATUS = 'PENDING'
GROUP BY CLIENTID;
IF counter > 1 THEN
RAISE exception_name;
END IF;
END;
/
where
the SELECT statement need to be removed from IF .. THEN conditional
Most probably, the mutating table error would raise for Row Level Trigger case
Demo

Related

Trigger after insert to check and compare records between table

In an Oracle Database, I need to create some trigger or procedure to treat this case in the most performative way possible (is an extremely large amount of data).
I have a table called ORDER_A that every day receives a full load (its truncated, and all records are inserted again).
I have a table called ORDER_B which is a copy of ORDER_A, containing the same data and some additional control dates.
Each insertion on ORDER_A must trigger a process that looks for a record with the same identifier (primary key: order_id) in table B.
If a record exists with the same order_id, and any of the other columns have changed, an update must be performed on table B
If a record exists with the same order_id, and no values ​​in the other columns have been modified, nothing should be performed, the record must remain the same in table B.
If there is no record with the same order_id, it must be inserted in table B.
My tables are like this
CREATE TABLE ORDER_A
(
ORDER_ID NUMBER NOT NULL,
ORDER_CODE VARCHAR2(50),
ORDER_STATUS VARCHAR2(20),
ORDER_USER_ID NUMBER,
ORDER_DATE TIMESTAMP(6),
PRIMARY KEY (ORDER_ID)
);
CREATE TABLE ORDER_B
(
ORDER_ID NUMBER NOT NULL,
ORDER_CODE VARCHAR2(50),
ORDER_STATUS VARCHAR2(20),
ORDER_USER_ID NUMBER,
ORDER_DATE TIMESTAMP(6)
INSERT_AT TIMESTAMP(6),
UPDATED_AT TIMESTAMP(6),
PRIMARY KEY (ORDER_ID)
);
I have no idea how to do this and what is the best way (with a trigger, procedure, using merge, etc.)
Can someone give me a direction, please?
Here is some pseudo-code to show you a potential trigger based solution that does not fall back into slow row-by-row processing.
create or replace trigger mytrg
for insert or update delete on ordera
compound trigger
pklist sys.odcinumberlist;
before statement is
begin
pklist := sys.odcinumberlist();
end before statement ;
after each row is
begin
pklist.extend;
pklist(pklist.count) := :new.order_id;
end before each row;
after statement is
begin
merge into orderb b
using (
select a.*
from ordera a,
table(pklist) t
where a.order_id = t.column_value
) m
when matched then
update
set b.order_code = m.order_code,
b.order_status = m.order_status,
...
where decode(b.order_code,m.order_code,0,1)=1
or decode(b.order_status,m.order_status,0,1)=1
....
when not matched then
insert (b.order_id,b.order_code,....)
values (m.order_id,m.order_code,....);
end after statement ;
end;
We hold the impacted primary keys, and then build a single merge later, with an WHERE embed to minimise update activities.
If your application allows the update of primary keys, you'd need some additions, but this should get you started

ORA-04091: table name is mutating - when trigger from child table wants to update parent table

I have 2 simple tables:
CREATE TABLE ORDERS
( ORDER_KEY number(10) NOT NULL,
ORDER_NR varchar2(50) NOT NULL,
LAST_UPDATE DATE,
CONSTRAINT ORDERS_PK PRIMARY KEY (ORDER_KEY)
);
CREATE TABLE ORDER_POSITIONS
( ORDER_POS_KEY number(10) NOT NULL,
ORDER_POS_NR number(10),
ORDER_POS_DESCRIPTION varchar2(50),
ORDER_KEY NUMBER(10) NOT NULL,
CONSTRAINT ORDER_POSITIONS_PK PRIMARY KEY (ORDER_POS_KEY),
CONSTRAINT ORDERS_FK
FOREIGN KEY (ORDER_KEY)
REFERENCES ORDERS(ORDER_KEY)
ON DELETE CASCADE
);
On the table ORDER_POSITIONS I created a trigger which should update the column LAST_UPDATE whenever a position is deleted.
CREATE OR REPLACE TRIGGER TGAUDIT_ORDER_POS
AFTER DELETE
ON ORDER_POSITIONS
FOR EACH ROW
DECLARE
BEGIN
UPDATE ORDERS O SET O.LAST_UPDATE = SYSDATE WHERE O.ORDER_KEY = :OLD.ORDER_KEY;
END;
If I delete a position from the table ORDER_POSITION, everything is working fine (the column LAST_UPDATE is updated).
However, if I want to delete an order, all its positions are gonna be deleted, too (via CASCADE DELETE). At this moment also the trigger on the table ORDER_POSITIONS is being raised and it wants to update the column of the table which is currently being deleted - ORDERS. Obviously I get here the error : ORA-04091 Table ORDERS is mutating.
Is there a way to get it solved?
I solved it finally using a compound trigger:
CREATE OR REPLACE TRIGGER TGAUDIT_ORDER_POS
FOR DELETE ON ORDER_POSITIONS
COMPOUND TRIGGER
TYPE parent_key_type IS TABLE OF ORDERS.ORDER_KEY%TYPE;
parent_keys parent_key_type := parent_key_type();
AFTER EACH ROW IS BEGIN
IF DELETING THEN
BEGIN
parent_keys.extend;
parent_keys(parent_keys.last) := :old.ORDER_KEY;
END;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT IS BEGIN
FOR i IN 1..parent_keys.count LOOP
UPDATE DEVART_TEST.ORDERS O SET O.LAST_UPDATE = SYSDATE WHERE O.ORDER_KEY = parent_keys(i);
END LOOP;
END AFTER STATEMENT;
END;
UPDATE: Another option would be to catch this specific exception within the regular trigger.
CREATE OR REPLACE TRIGGER TGAUDIT_ORDER_POS
AFTER DELETE
ON ORDER_POSITIONS
FOR EACH ROW
DECLARE
TABLE_MUTATING EXCEPTION;
PRAGMA EXCEPTION_INIT(TABLE_MUTATING, -4091 );
BEGIN
UPDATE ORDERS O SET O.LAST_UPDATE = SYSDATE WHERE O.ORDER_KEY = :OLD.ORDER_KEY;
EXCEPTION
WHEN TABLE_MUTATING THEN
NULL; -- suppress
END;
I would recreate the foreign-key constraint without the ON DELETE CASCADE clause and delete all order positions before deleting an order. That way you avoid the mutating table error by not having two tables mutating at the same time.

How to create a conditional trigger

I have a table with an id as auto incremented primary key and another id.
CREATE TABLE tester (
"id" integer PRIMARY KEY AUTOINCREMENT,
"refId" integer DEFAULT 0
);
refId should be able to either be 0 (the default) or reference id if refId > 0 (i.e. act as foreign key).
Now I need two constraints:
A row should only be deletable if its id is not used (referenced?) by any other row's refId
A row should only be deletable if its refId is 0.
From what I have understood, I need to create a trigger that checks for these constraints before a DELETE event happens. And depending on refId's value either abort the delete action or allow it.
However, I have a hard time understanding the syntax for this and how to do a conditional check. But what I have so far (in mind!) is concerning 1.):
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE "refId" = OLD."id") IS NOT NULL;
END;
And concerning 2.)
CREATE TRIGGER no_delete_if_ref
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
IF OLD."refId" > 0 THEN RAISE(ABORT, "cannot delete tester because it refers to an existing tester");
END;
Does this make sense and is valid?
I am totally not sure, to me it does but well, I am all noob.
Also as a last question, can I alternatively combine this into a single trigger? For example would this be a valid query:
CREATE TRIGGER no_delete_if_inuse
BEFORE DELETE ON tester
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'cannot delete because of foreign key violation')
WHERE (SELECT "refId" FROM tester WHERE ("refId" = OLD."id" OR "refId" > 0) ) IS NOT NULL;
END;
You can define a foreign key referring to the same table. Use null instead of 0 for rows without a reference:
create table tester(
id int primary key,
refid int references tester,
check (id <> refid)
);
insert into tester values
(1, null),
(2, null),
(3, 1),
(4, 3);
You need a trigger to ensure that a row which references another one cannot be deleted.
create or replace function before_delete_on_tester()
returns trigger language plpgsql as $$
begin
if old.refid is not null then
raise exception
'Cannot delete: (id)=(%) references (id)=(%)', old.id, old.refid;
end if;
return old;
end $$;
create trigger before_delete_on_tester
before delete on tester
for row execute procedure before_delete_on_tester();
Test:
delete from tester where id = 1;
ERROR: update or delete on table "tester" violates foreign key constraint "tester_refid_fkey" on table "tester"
DETAIL: Key (id)=(1) is still referenced from table "tester".
delete from tester where id = 4;
ERROR: Cannot delete from tester. (id)=(4) references (id)=(3)
CONTEXT: PL/pgSQL function before_delete_on_tester() line 4 at RAISE
In Postgres you have to define a trigger function. Read more:
Overview of Trigger Behavior
Trigger Procedures
Create Trigger

Storing date & time and updater/user of row data in Oracle Apex

Looking for an ideal way to store (1) date & time of update (2) updater/user per row of table data using Oracle Apex. I am thinking of adding 2 extra columns to store the info and trying to come up with a good as to how changes per row can be tracked.
If you want create logs of insert, update , delete on your table, adding 2 columns not enough. Each new update will erase previous and delete couldn't be logged. So you need to store log table separately from data table, and fill it on before and after triggers created on your data table. If you want sample I can provide some.
Here simplified example, of course in real life data will be more complex and I guess a trigger should be more smarter, but this is a simple start point to create your own. After executing codes below, try to insert, update, delete delete records in table TEST_DATA, see what happens in TEST_LOG
Create data table
create table TEST_DATA (
UNID number,
COL_B varchar2(50)
);
-- Create/Recreate primary, unique and foreign key constraints
alter table TEST_DATA
add constraint PK_TEST_DATA_UNID primary key (UNID);
Create log table for it
create table TEST_LOG (
UNID number,
OPERATION varchar2(1),
COL_OLD varchar2(50),
COL_NEW varchar2(50),
CHNGUSER varchar2(50),
CHNGDATE date
);
and finally create trigger which tracks changes
create or replace trigger TR_LOG_TEST_DATA
after update or insert or delete on TEST_DATA
referencing new as new old as old
for each row
begin
if Inserting then
insert into TEST_LOG
(UNID, OPERATION, COL_OLD, COL_NEW, CHNGUSER, CHNGDATE)
values
(:new.unid, 'I', null, :new.col_b, user, sysdate);
end if;
if Updating then
insert into TEST_LOG
(UNID, OPERATION, COL_OLD, COL_NEW, CHNGUSER, CHNGDATE)
values
(:new.unid, 'U', :old.col_b, :new.col_b, user, sysdate);
end if;
if Deleting then
insert into TEST_LOG
(UNID, OPERATION, COL_OLD, COL_NEW, CHNGUSER, CHNGDATE)
values
(:old.unid, 'D', :old.col_b, null, user, sysdate);
end if;
end;

oracle trigger synthetic key

I'm trying to create a synthetic key with a sequence (attribute_1). Im not sure my code is working right as it's the first time i've done anything like this. I'm just looking for general pointers or tips about what im doing.
CREATE TABLE entity
(
attribute_1 INT NOT NULL PRIMARY KEY,
attribute_2 VARCHAR2(5),
attribute_3 NOT NULL VARCHAR2(5)
);
CREATE SEQUENCE attribute_1_seq
START WITH 1
INCREMENT BY 1
NOCACHE;
CREATE TRIGGER attribute_1_trig
BEFORE INSERT ON entity
FOR EACH ROW BEGIN
SELECT attribute_1_seq.NEXTVAL INTO :new.attribute_1 FROM dual;
END;
/
Your trigger will work but what if you want to supply an ID number? Some apps will insert into a parent and use the id as a foreign key to child tables. In this case you might want to call the sequence directly so you can reuse it.
This trigger will allow you to either insert null as the primary key or supply one
CREATE OR REPLACE TRIGGER entity_Id_TRG BEFORE INSERT OR UPDATE ON entity
FOR EACH ROW
BEGIN
if inserting and :new.attribute_1 is NULL then
SELECT attribute_1_SEQ.nextval into :new.attribute_1 FROM DUAL;
end if;
END;