Oracle Trigger to insert /update to another table - oracle

Basically, i wanted to create a oracle trigger, which will track the Insert/Updates in a table and i wanted to insert these changed records alone into a new table on a daily basis.This new table data will be using for my daily data refresh (more like a CDC approach)
I was using below code. My expectation was when my CUSTOMER table got INSERT/UPDATED,that record should be available in CUSTOMER_D table.But i am missing something in below code
CREATE OR REPLACE TRIGGER CUST_TRIG AFTER INSERT OR UPDATE ON CUSTOMERS
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW WHEN (OLD.ID <> NEW.ID)
begin
IF INSERTING THEN
begin
INSERT INTO CUSTOMERS_D
(ID, NAME, AGE, ADDRESS, SALARY) values
(:new.ID, :new.NAME, :new.AGE, :new.ADDRESS, :new.SALARY);
-- assuming, there is an unique key on id
exception
when dup_val_on_index then
null;
end;
ELSIF UPDATING THEN
IF :NEW.ID = :OLD.ID THEN
UPDATE CUSTOMERS_D DWT
SET DWT.ID = :NEW.ID,
DWT.NAME = :NEW.NAME,
DWT.AGE = :NEW.AGE,
DWT.ADDRESS = :NEW.ADDRESS,
DWT.SALARY = :NEW.SALARY;
END IF;
MERGE INTO CUSTOMERS_D D
USING DUAL
ON (D.ID = :NEW.ID)
WHEN MATCHED THEN
UPDATE SET D.NAME = :NEW.NAME,
D.AGE = :NEW.AGE,
D.ADDRESS = :NEW.ADDRESS,
D.SALARY = :NEW.SALARY
WHEN NOT MATCHED THEN
INSERT
(D.ID, D.NAME, D.AGE, D.ADDRESS, D.SALARY) VALUES
(:NEW.ID, :NEW.NAME, :NEW.AGE, :NEW.ADDRESS, :NEW.SALARY);
END IF;
end test;

Your trigger in its present form has too many unnecessary constructs which you should get rid of. All you need ( as you confirmed in the comments ) is a single insert statement into customers_d whenever a record gets added or inserted from customers table.
I would recommend you to add another column indicating the time of transaction -MODIFIED_TIME
CREATE OR REPLACE TRIGGER CUST_TRIG
AFTER INSERT OR UPDATE ON CUSTOMERS
FOR EACH ROW
begin
INSERT INTO CUSTOMERS_D
(ID, NAME, AGE, ADDRESS, SALARY,modified_time )
values (:new.ID,
:new.NAME,
:new.AGE,
:new.ADDRESS,
:new.SALARY,
systimestamp);
end ;
/
DEMO

At first glance I can see the one issue in this trigger when new record is inserted the OLD.ID will be NULL so WHEN ((OLD.ID <> NEW.ID) will be FALSE and the trigger wont be invoked on INSERT into CUST_TRIG. Just add following condition:
FOR EACH ROW WHEN ((OLD.ID <> NEW.ID) OR (OLD.ID IS NULL))

Related

Getting error PLS-003036 wrong number or types of argument in call to =

I had written the below trigger
create or replace trigger my_trigger
Before insert or update on table1
referencing new as new old as old
for each row
declare id number;
cursor id_cnt is
select count(*) from table2 where my_id=:new.my_id;
begin
if :new.my_id is null
then RAISE_APPLICATION_ERROR(-001,"MY_ID should nit be null");
elsif id_cnt=0 then
RAISE_APPLICATION_ERROR(-002,"not a valid id ");
else
select new_id from table2 where my_id=:new.my_id;
if lenght(new_id) <5
then
RAISE_APPLICATION_ERROR(-003,"length is very small ");
END IF;
END IF;
END my_trigger;
At if :new.my_id is null i am getting the below error
error PLS-003036 wrong number or types of argument in call to =
There are 2 conditions needs to be checked first condition i need to check my_id is null or not and second condition need to check the length of new_id before that i am checking if that my_id is already existed in table 2 before inserting into table1
You:
want to SELECT ... INTO rather than using a CURSOR
misspelt LENGTH
need to use ' for string literals and not "; and
need to use -20000 to -20999 for user-defined error numbers.
Like this:
create or replace trigger my_trigger
Before insert or update on table1
referencing new as new old as old
for each row
declare
id number;
id_cnt PLS_INTEGER;
v_new_id table2.new_id%TYPE;
begin
IF :new.my_id is null THEN
RAISE_APPLICATION_ERROR(-20001,'MY_ID should nit be null');
END IF;
select count(*)
INTO id_cnt
from table2
where my_id=:new.my_id;
if id_cnt=0 then
RAISE_APPLICATION_ERROR(-20002,'not a valid id');
else
select new_id
INTO v_new_id
from table2
where my_id=:new.my_id;
if length(v_new_id) < 5 then
RAISE_APPLICATION_ERROR(-20003,'length is very small');
END IF;
END IF;
END my_trigger;
/
db<>fiddle here

How to prevent trigger from updating the timestamp on a column in Oracle?

I have a table which has a column on which I have set a trigger to update the timestamp when there is an update on the row
CREATE TABLE mytable (
id NUMBER NOT NULL,
sid VARCHAR(10) NOT NULL,
stext VARCHAR(10),
tid VARCHAR(10),
ttext VARCHAR(10),
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique PRIMARY KEY (id) ENABLE
);
CREATE OR REPLACE trigger last_updated_trigger
BEFORE UPDATE ON mytable
FOR EACH ROW
BEGIN
select systimestamp into :new.last_updated from dual;
END;
/
However, when certain SQLs are executed on the table, I do NOT want the 'last_updated' column to be updated. For instance, when the below SQL updates this table, I do NOT want the trigger to kick in. I need the column to retain the last_updated value so that my other queries do not get messed up. However, when other SQLs do updates on the data, I want the column to be updated.
Any ideas on how this could be done?
MERGE INTO mytable
USING dual
ON (id = ?)
WHEN MATCHED THEN
UPDATE SET
stext = case when sid = ? then ? else stext end,
ttext = case when tid = ? then ? else ttext end
You can easily keep a supplied value and only insert the current time if it is not supplied:
CREATE OR REPLACE trigger last_updated_trigger
BEFORE UPDATE ON mytable
FOR EACH ROW
BEGIN
:new.last_updated := coalesce(:new.last_updated, systimestamp);
END;
If you do not want to fire trigger when you update STEXT or TTEXT columns please try this:
CREATE OR REPLACE trigger last_updated_trigger
BEFORE UPDATE ON mytable
FOR EACH ROW
BEGIN
if (not updating('stext') and not updating('ttext')) then
:new.last_updated := systimestamp;
end if;
END;
Thanks
You can put that specific SQL Statement into a PROCEDURE in which the module info is updated for the concerned application(always call this statement from this procedure only) such as
CREATE OR REPLACE PROCEDURE Merge_Text AS
BEGIN
DBMS_APPLICATION_INFO.SET_MODULE($$plsql_unit,null);
MERGE INTO mytable
USING dual
ON (id = ?)
WHEN MATCHED THEN
UPDATE SET
stext = case when sid = ? then ? else stext end,
ttext = case when tid = ? then ? else ttext end;
END;
/
and then distinguish this caller module within the trigger such as
CREATE OR REPLACE trigger last_updated_trigger
BEFORE UPDATE ON mytable
FOR EACH ROW
v_module VARCHAR2(80);
v_action VARCHAR2(80);
BEGIN
DBMS_APPLICATION_INFO.READ_MODULE(v_module, v_action);
IF v_module != 'MERGE_TEXT' THEN
:new.last_updated := systimestamp;
END IF;
END;
/

Creating a trigger to insert and remove rows

I have been given an assignment to create a trigger that works when tables are inserted or updated or deleted in a table. If it is a delete or update then the table must store the older values before the action on another table. If it is insert then the new row should be added to the new table. Also it should include the number of rows that are affected by each action. So far this is what I have done:
CREATE OR REPLACE TRIGGER archive_update
BEFORE INSERT OR UPDATE ON EMPLOYEE
FOR EACH ROW
BEGIN
INSERT INTO archive_emp(EMP_ID, FIRST_NAME, LAST_NAME, BIRTH_DAY,
SEX, SALARY, SUPER_ID, BRANCH_ID)
VALUES(:new.EMP_ID, :new.FIRST_NAME, :new.LAST_NAME,
:new.BIRTH_DAY, :new.SEX, :new.SALARY, :new.SUPER_ID, :new.BRANCH_ID);
END;
To check the dml operation you may use an IF condition with appropriate dml predicate
CREATE OR REPLACE TRIGGER archive_update
BEFORE
INSERT OR UPDATE ON EMPLOYEE
FOR EACH ROW
BEGIN
IF UPDATING OR DELETING THEN --You may add this
INSERT INTO archive_emp( emp_id, first_name, last_name,
birth_day, sex,salary,super_id,branch_id
) VALUES (:old.emp_id, :old.first_name,:old.last_name,:old.birth_day,
:old.sex,:old.salary,:old.super_id,:old.branch_id
);
ELSIF INSERTING THEN --and this
INSERT INTO archive_emp( emp_id, first_name, last_name,
birth_day, sex,salary,super_id,branch_id
) VALUES (:new.emp_id, :new.first_name,:new.last_name,:new.birth_day,
:new.sex,:new.salary,:new.super_id,:new.branch_id
);
END IF;
END;
/

ORACLE trigger instead of insert on view with a nested table

I have to write a trigger. It compiles everything, but if I want to insert something in my view, i get an error message. Maybe you can help me.
SET DEFINE off;
CREATE OR REPLACE TRIGGER LieferantOV_trig
INSTEAD OF INSERT
ON LIEFERANT_OV
FOR EACH ROW
BEGIN
IF INSERTING THEN
INSERT INTO Lieferant (LiefNr, Name, Adresse)
VALUES(:new.LiefNr, :new.Name, ntTAdresse());
INSERT INTO TABLE (SELECT Adresse FROM Lieferant ) VALUES
(TAdresse(:new.Straße, :new.PLZ, :new.Ort));
END IF;
END;
INSERT INTO Lieferant_OV
VALUES(752443, 'Laepple Teublitz', 'Maxstr. 12', '93158', 'Teublitz');
For the nested Table
CREATE OR REPLACE TYPE TAdresse AS OBJECT(
Straße VARCHAR2(50),
PLZ VARCHAR2(5),
Ort VARCHAR2(50)
);
CREATE TABLE Lieferant(
LiefNr number(6) PRIMARY KEY,
Name varchar2(20) NOT NULL
);
1.
CREATE OR REPLACE TYPE ntTAdresse AS TABLE OF TAdresse;
2.
ALTER TABLE Lieferant ADD Adresse ntTAdresse NESTED TABLE Adresse STORE AS TAdresseNT;
CREATE OR REPLACE VIEW Lieferant_OV (LiefNr, Name, Straße, PLZ, ORT)
AS SELECT k.LiefNr, k.Name, l.Straße, l.PLZ, l.Ort
FROM Lieferant k, table(k.Adresse) l;
The syntax should be like this:
CREATE OR REPLACE TRIGGER LieferantOV_trig
INSTEAD OF INSERT
ON LIEFERANT_OV
FOR EACH ROW
BEGIN
INSERT INTO Lieferant (LiefNr, Name, Adresse)
VALUES(:new.LiefNr, :new.Name, ntTAdresse(TAdresse(:new.Straße,:new.PLZ,:new.ORT));
END;
You can skip IF INSERTING THEN because your trigger fires only on INSERT
Note, by this each record have at maximum only one address, thus a nested table does not make much sense.
In order to add an address to existing Lieferant you can do this one:
CREATE OR REPLACE TRIGGER LieferantOV_trig
INSTEAD OF INSERT
ON LIEFERANT_OV
FOR EACH ROW
DECLARE
lieferantCount INTEGER;
BEGIN
select count(*)
into lieferantCount
from Lieferant
where LiefNr = :new.LiefNr
and Name = :new.Name;
if lieferantCount = 0 then
INSERT INTO Lieferant (LiefNr, Name, Adresse)
VALUES(:new.LiefNr, :new.Name, ntTAdresse(TAdresse(:new.Straße,:new.PLZ,:new.ORT)));
else
UPDATE Lieferant
SET Adresse = Adresse MULTISET UNION ntTAdresse(TAdresse(:new.Straße,:new.PLZ,:new.ORT))
WHERE LiefNr = :new.LiefNr
and Name = :new.Name;
end if;
END;
I think you want do this if you want to keep one row per insertion.
CREATE OR REPLACE TRIGGER LieferantOV_trig
INSTEAD OF INSERT
ON LIEFERANT_OV
FOR EACH ROW
BEGIN
INSERT INTO Lieferant (LiefNr, Name, Adresse)
VALUES(:new.LiefNr, :new.Name, ntTAdresse(TAdresse(:new.Straße, :new.PLZ, :new.Ort)));
END;
/
This works if the Adresse column contain only one Adress. But that is not purpose of a nested table. So you probably want to check if there is an existing row in the table with given LiefNr. If yes, only insert into the nested table.

Oracle Compound Trigger Mutation Table

I'm trying to create a compound trigger to avoid the mutation problem.
I've a table and a python's procedure that perfoms a transaction insert. The table has n fields.
What I´m trying to do is when a value of one of those fields is negative, then do not perform the operation , and insert the value from the previous record of the field (prior to insert) of the table. Another concern is that one of the fields is and id, to distinguish between sites.
For no, this is the code I've, Considering only one field (KWHGEN):
CREATE OR REPLACE TRIGGER "CIRCU3".D_measures_TP_test
--FOR INSERT OR UPDATE ON T_MEASURES_TP_NEW
FOR INSERT ON T_MEASURES_TP_NEW
COMPOUND TRIGGER
VAL_KWHGEN NUMBER(21,2);
VAL_autoin NUMBER (19,0);
AFTER EACH ROW IS
BEGIN
SELECT autoin, KWHGEN INTO VAL_ID_MED, VAL_KWHGEN FROM
(SELECT *
FROM T_measures_TP_NEW WHERE ID_site = :NEW.ID_site
ORDER BY TIMESTAMP DESC)
WHERE ROWNUM = 1;
IF :NEW.KWHGEN <0
THEN UPDATE T_MEASURES_TP_NEW SET KWHGEN = VAL_KWHGEN WHERE autoin = VAL_autoin;
END IF;
END AFTER EACH ROW;
END D_MEASURES_TP_test;
But the mutation error is following me ;-)
You have created trigger on T_MEASURES_TP_NEW and then updating same table T_MEASURES_TP_NEW within trigger. This will again call your trigger.
If the first select in trigger again returns negative value in VAL_KWHGEN then mutating error will follow you.
You defined only an AFTER EACH block, nothing else. This is the same as creating a row-level trigger (i.e. using FOR EACH ROW)
It must be like this (not tested):
CREATE OR REPLACE TRIGGER "CIRCU3".D_measures_TP_test
FOR INSERT ON T_MEASURES_TP_NEW
COMPOUND TRIGGER
VAL_KWHGEN NUMBER(21,2);
VAL_autoin NUMBER (19,0);
TYPE RowIdTableType IS TABLE OF ROWID;
TYPE KWHGENTableType IS TABLE OF T_MEASURES_TP_NEW.KWHGEN%TYPE;
RowIdTable RowIdTableType;
KWHGENTable KWHGENTableType;
BEFORE STATEMENT IS
BEGIN
RowIdTable := RowIdTable();
KWHGENTable := KWHGENTableType();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
RowIdTable.EXTEND;
RowIdTable(RowIdTable.LAST) := :NEW.ROWID;
KWHGENTable.EXTEND;
KWHGENTable(RowIdTable.LAST) := :NEW.KWHGEN;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN RowIdTable.FIRST..RowIdTable.LAST LOOP
SELECT
DISTINCT MIN(autoin) OVER (ORDER BY TIMESTAMP DESC),
DISTINCT MIN(KWHGEN) OVER (ORDER BY TIMESTAMP DESC)
INTO VAL_ID_MED, VAL_KWHGEN
FROM T_measures_TP_NEW
WHERE ROWID = RowIdTable(i);
IF KWHGENTable(i) < 0
THEN UPDATE T_MEASURES_TP_NEW
SET KWHGEN = VAL_KWHGEN
WHERE autoin = VAL_autoin;
END IF;
END LOOP;
END AFTER STATEMENT;
END;
/
OK, I do have a solution:
1.- Create a package where record the new insert data (BEFORE)
create or replace PACKAGE PCK_MEDIDAS_TP AS
TYPE DATOS_MEDIDAS_TP IS RECORD(
v_id_sede NUMBER (10,0),
v_id_med NUMBER (10,0),
v_kwhGEN NUMBER (21,2),
v_timestamp TIMESTAMP
);
type T_MEDTP is table of DATOS_MEDIDAS_TP index by binary_integer;
tabla_medidas_tp T_MEDTP;
END PCK_MEDIDAS_TP;
2.- Create a procedure each row (BEFORE) to read the new insert data and then record them into de package's table.
create or replace TRIGGER "CIRCU3".D_MEDIDAS_TP_test
BEFORE INSERT ON T_MEDIDAS_TP_NEW
FOR EACH ROW
DECLARE
Indice binary_integer;
BEGIN
--AUTOINCREMENTAL DEL CAMPO ID_MEDIDAS
SELECT T_MEDIDAS_TP_NEW_SEQ.NEXTVAL INTO :NEW.id_MEDIDAS_OLD FROM DUAL;
Indice:= PCK_MEDIDAS_TP.tabla_medidas_tp.COUNT+1;
PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_id_sede := :NEW.ID_SEDE;
PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_id_med := :NEW.ID_MEDIDAS;
PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_kwhGEN := :NEW.KWHGEN;
PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_timestamp := :NEW.TIMESTAMP;
IF :NEW.KWHGEN <0 THEN
DBMS_OUTPUT.put_line('first trigger:' ||:NEW.ID_MEDIDAS||','||:NEW.ID_SEDE||','||:NEW.TIMESTAMP);
-- INSERT INTO TEST_TRIGGER VALUES ('100', :NEW.KWHGEN, SYSDATE);
--ELSE DBMS_OUTPUT.PUT_LINE('¿?');
END IF;
END;
3.- Create a statement procedure (AFTER) where you can check your condition, in my case if kwhgen <0. If is true, I'll read the previous record in the original tbale and update the insert record with taht value.
create or replace TRIGGER D_MEDIDAS_TP_TEST_STATEMENT
AFTER INSERT ON T_MEDIDAS_TP_NEW
DECLARE
Indice binary_integer;
s_id_sede NUMBER (10,0);
s_id_med NUMBER (10,0);
s_kwhGEN NUMBER (21,2);
s_timestamp TIMESTAMP;
BEGIN
FOR Indice in 1..PCK_MEDIDAS_TP.tabla_medidas_tp.count LOOP
DBMS_OUTPUT.put_line('second trigger: kwhgen: '||PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_kwhGEN||', id_sede: '||PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_id_sede);
IF PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_kwhGEN <0 THEN
DBMS_OUTPUT.put_line('second trigger: v_kwhGEN is negative');
SELECT prev_KWHGEN INTO s_kwhgen
from(
SELECT LEAD (KWHGEN,1) over (ORDER BY id_medidas desc) as prev_KWHGEN
FROM T_MEDIDAS_TP_NEW WHERE ID_SEDE = PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_id_sede
ORDER BY id_medidas DESC) where rownum =1;
INSERT INTO TEST_TRIGGER VALUES ('100', '5555', SYSDATE);
DBMS_OUTPUT.put_line('second trigger. KWHGEN: '||s_kwhGEN);
DBMS_OUTPUT.put_line('UPDATE');
UPDATE T_MEDIDAS_TP_NEW SET KWHGEN = S_KWHGEN WHERE ID_MEDIDAS = PCK_MEDIDAS_TP.tabla_medidas_tp(Indice).v_id_med;
else DBMS_OUTPUT.put_line('¿?');
END IF;
END LOOP;
PCK_MEDIDAS_TP.tabla_medidas_tp.delete; -- vaciamos la tabla
END;

Resources