Trigger to count insert and updates from users - sql

I'm new with oracle sql. I am trying to make a trigger that counts when X user performs an update or an insert, but the TRANSACTIONCONTROL table shows it like this:
DATE--------- USER-----------INSERT----UPDATE
10/03/2022 UserParcial 1 0
10/03/2022 UserParcial 0 1
10/03/2022 UserParcial 1 0
But I want it to look like this:
DATE--------- USER-----------INSERT----UPDATE
10/03/2022 UserParcial 2 1
This is my trigger:
create or replace NONEDITIONABLE TRIGGER TRANSACTIONCONTROL_Trig
AFTER INSERT OR DELETE OR UPDATE on products
for each row
DECLARE
dataTran date;
userTran varchar(30);
InsertTran number:=0;
UpdateTran number:=0;
BEGIN
SELECT SYSDATE INTO dateTran FROM DUAL;
SELECT USER INTO userTran FROM DUAL;
IF INSERTING THEN
InsertTran := InsertTran +1;
INSERT INTO TransactionControl(date, user, insert, updates)
VALUES(dateTran, userTran, insertTran, updateTran);
END IF;
IF UPDATING THEN
updateTran:= updateTran+1;
INSERT INTO TransactionControl(date, user, insert, updates)
VALUES(dateTran, userTran, insertTran, updateTran);
END IF;
END;

If you don't need exact numbers, than mining ALL_TAB_MODIFICATIONS periodically could probably suffice. (I'm curious as to what business function having the count provides)
But if you really must use a trigger, then a compound trigger lets you keep counts at row level, but then summarise at statement level.
Some pseudo code below
create or replace trigger mytrig
for insert or update on mytable
compound trigger
ins_cnt int;
upd_cnt int;
before statement is
begin
ins_cnt := 0;
upd_cnt := 0;
end before statement;
after each row is
begin
if inserting then ins_cnt := ins_cnt + 1; end if;
if updating then upd_cnt := upd_cnt + 1; end if;
end after each row;
after statement is
begin
insert into txn_control ( ... ) values (ins_cnt, upd_cnt);
end after statement;
end;
/

Related

How to include a SUBSELECT in VALUES of INSERT to take values from different row?

I want to make a trigger that will insert a value from a connected row. For example I have a table with 3 rows as below:
I create a trigger that will work once row 3 and 4 are deleted (in this case will be deleted at the same time). And I want to record invnr and extinvnr from row 1 based on idparent=id. I cannot seem to make it work though.
CREATE OR REPLACE TRIGGER LOG_DELETEDPAYMENTS
BEFORE DELETE ON payments
FOR EACH ROW
BEGIN
IF :old.invnr IS NULL THEN
INSERT INTO TABLE_LOG_DELETEDPAYMENTS (table_name, invnr, extinvnr, invdate, transactionid, info, createdby, deleted_by, date_of_delete)
values ('payments', :old.invnr, :old.extinvnr, :old.invdate, :old:transactionid, :old.info, :old.createdby, sys_context('userenv','OS_USER'), SYSDATE);
END IF;
END;
How can I incorporate this into the trigger above?
Try it this way:
create or replace TRIGGER LOG_DELETEDPAYMENTS
BEFORE DELETE ON payments
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
Declare
my_invnr PAYMENTS.INVNR%TYPE;
my_extinvnr PAYMENTS.EXTINVNR%TYPE;
Begin
IF :old.INVNR IS NULL THEN
Select INVNR, EXTINVNR
Into my_invnr, my_extinvnr
From PAYMENTS
Where ID = :old.IDPARENT;
--
INSERT INTO TABLE_LOG_DELETEDPAYMENTS (table_name, invnr, extinvnr, invdate, transactionid, info, createdby, deleted_by, date_of_delete)
values ('payments', my_invnr, my_extinvnr, :old.invdate, :old:transactionid, :old.info, :old.createdby, sys_context('userenv','OS_USER'), SYSDATE);
END IF;
End;
END;
You should select the values of INVNR and EXTINVNR based on ID - IDPARENT relationship and store it in the variables (my_invnr and my_extinvnr).
Those variables are used in INSERT into the log statement.
Because of the Select ... Into statement that is reading the affected table - trigger would fail with table PAYMENTS is mutating error.
To avoid that (to separate transaction from the table) you should Declare the PRAGMA AUTONOMOUS_TRANSACTION.
There will be two rows inserted into LOG as the trigger runs FOR EACH (deleted) ROW.
Regards...
This assumes your are on release 12c or greater of Oracle database.
CREATE OR REPLACE PACKAGE LOG_DELETEDPAYMENTS_PKG
AS
-- need a locally defined type for use in trigger
TYPE t_payments_tbl IS TABLE OF payments%ROWTYPE INDEX BY PLS_INTEGER;
END LOG_DELETEDPAYMENTS_PKG;
CREATE OR REPLACE PACKAGE BODY LOG_DELETEDPAYMENTS_PKG
AS
BEGIN
-- could also put the trigger code here and pass the type as a parameter to a procedure
NULL;
END LOG_DELETEDPAYMENTS_PKG;
CREATE OR REPLACE TRIGGER LOG_DELETEDPAYMENTS_CT
FOR DELETE ON payments
COMPOUND TRIGGER
l_tab LOG_DELETEDPAYMENTS_PKG.t_payments_tbl;
l_count PLS_INTEGER:= 0;
BEFORE EACH ROW IS
BEGIN
-- capture the deletes in local type
l_count := l_count + 1;
l_tab(l_count).invnr := :old.invnr;
l_tab(l_count).extinvnr := :old.extinvnr;
l_tab(l_count).invdate := :old.invdate;
l_tab(l_count).transactionid := :old.transactionid;
l_tab(l_count).info := :old.info;
l_tab(l_count).createdby := :old.createdby;
l_tab(l_count).idparent := :old.idparent;
l_tab(l_count).id := :old.id;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN l_tab.first .. l_tab.COUNT LOOP
IF(l_tab(i).invnr IS NULL) THEN
-- if the invoice number is NULL, then get info from parent
SELECT p.invnr
,p.extinvnr
INTO l_tab(i).invnr
,l_tab(i).extinvnr
FROM TABLE(l_tab) p
WHERE p.id = l_tab(i).idparent;
END IF;
END LOOP;
-- log all deletes
FORALL i IN 1 .. l_tab.COUNT
INSERT INTO LOG_DELETEDPAYMENTS
(table_name, invnr, extinvnr, invdate, transactionid, info, createdby, deleted_by, date_of_delete)
VALUES
('payments', l_tab(i).invnr, l_tab(i).extinvnr, l_tab(i).invdate, l_tab(i).transactionid, l_tab(i).info, l_tab(i).createdby, sys_context('userenv','OS_USER'), SYSDATE);
l_tab.delete;
END AFTER STATEMENT;
END LOG_DELETEDPAYMENTS_CT;

How to avoid deletion of more than 1 row per request with trigger?

I want to write a trigger which denies deletion of more than 1 row per request
Is it possible to write such trigger? If yes, can you help with an idea or code?
I am using oracle database 11g and writing in sqldeveloper
Orders Table
id
detail
orderdate
I'm not sure why you would want to do this (multi-row deletes are more efficient than single-row deletes), but you could use a compound trigger for this purpose, e.g.:
create table mytable (id number);
create or replace trigger one_row_per_delete
for delete on mytable
compound trigger
row_count integer;
before statement is
begin
row_count := 0;
end before statement;
before each row is
begin
row_count := row_count + 1;
end before each row;
after statement is
begin
if row_count > 1 then
raise_application_error(-20000, 'Cannot delete more than one row per statement');
end if;
end after statement;
end one_row_per_delete;
Tested:
insert into mytable values (1);
1 row inserted.
insert into mytable values (2);
1 row inserted.
insert into mytable values (3);
1 row inserted.
delete mytable where id = 1;
1 row deleted.
delete mytable;
ORA-20000: Cannot delete more than one row per statement
ORA-06512: at "SCOTT.ONE_ROW_PER_DELETE", line 18
ORA-04088: error during execution of trigger 'SCOTT.ONE_ROW_PER_DELETE'
You can perform it with a trigger, for example
CREATE OR REPLACE TRIGGER denies_delete_trg
BEFORE DELETE
ON table_name
DECLARE
v_count_elements NUMBER := 0;
BEGIN
SELECT COUNT(1) INTO v_count_elements
FROM table_name
WHERE contitions_of_delete;
IF v_count_elements > 0 THEN
RAISE_APPLICATION_ERROR (-20000,'Too rows to delete.');
END IF;
END;

Creating specific value sequence in oracle

I need to create an oracle sequence with specific values
FOUR0001, FOUR0002, FOUR0003.....
the increment must be in order.
First create a simple sequence
create sequence my_seq ; --start with 1 increment by 1
In your application code / table where you use the sequence to store the data, use something like this
INSERT INTO yourtab (col1) VALUES( 'FOUR'||lpad(my_seq.nextval,4,'0'));
You could create a sequence:
create sequence SEQ_NAME ...;
and then to create a trigger to feed the field automatically:
CREATE OR REPLACE TRIGGER INS_TABLENAME
before insert on TABLENAME
for each row
BEGIN
if :new.FIELD_NAME is null then
:new.FIELD_NAME := 'FOUR'||lpad(SEQ_NAME.nextval,4,'0');
end if;
END;
I've created a sequence starting with 210 ( because i already ahve 209 records) then created the trigger bellow
CREATE OR REPLACE trigger BIU_FRS
before insert or update on FOURNISSEUR
for each row
begin
if :NEW.FRS_NUM is null then
select ('FOUR'||lpad(four_seq.nextval,4,'0')) into :NEW.FRS_NUM from dual;
end if;
if inserting then
:new.created := localtimestamp;
:new.created_by := nvl(wwv_flow.g_user,user);
end if;
:new.updated := localtimestamp;
:new.updated_by := nvl(wwv_flow.g_user,user);
end;
thank you #Kaushik Nayak and #Diego Souza

How to avoid "table mutating" errors

I have an trigger that needs to read from a table after deleting a row. Essentially, I need to count up the remaining rows that are similar to the current row, and if that count is zero, update a field elsewhere.
After two days of hammering around, I haven't been able to figure out how to restructure my thought process to allow me to do this. Here is an example:
CREATE OR REPLACE TRIGGER Di_PatMustBeWell
AFTER DELETE
ON Diagnosis
FOR EACH ROW
Declare
--PRAGMA AUTONOMOUS_TRANSACTION;
NumDiseases Number;
BEGIN
SELECT NUMDISEASES INTO Numdiseases
FROM DiagnosisCount
where Di_Patient = :OLD.Di_Patient;
IF( NumDiseases != 1 ) THEN
UPDATE Patient SET Pat_Sick = 0 WHERE Pat_Person = :OLD.Di_Patient;
END IF;
END;
/
Short answer - no trigger, no mutating.
Yow can use the trigger with pragma autonomous_transaction for counting of remaining diagnoses for certain patient, but it's is not recommended way to do this.
Better you create new function or procedure to implement your logic on deleted diagnosis. Something like this:
create table Diagnosis as select 456 idDiseases, 123 di_patient from dual;
/
create table diagnosisCount as select 1 numDiseases, 123 di_patient from dual;
/
create table Patient as select 123 Pat_Person, 1 Pat_Sick from dual;
/
drop trigger di_patmustbewell;
create or replace function deleteDiagnosis(idDiseases number) return number is
rows_ number;
di_patient number;
Numdiseases number;
begin
<<del>> begin
delete Diagnosis where IdDiseases = deleteDiagnosis.IdDiseases
returning Diagnosis.di_patient into deleteDiagnosis.di_patient
;
rows_ := sql%rowcount;
if rows_ != 1 then raise too_many_rows; end if;
end del;
select count(1) into deleteDiagnosis.numDiseases from Diagnosis where Di_Patient = deleteDiagnosis.di_patient;
if deleteDiagnosis.numdiseases = 0 then <<upd>> begin
update Patient set Pat_Sick = 0 where Pat_Person = deleteDiagnosis.di_patient;
exception when others then
dbms_output.put_line('Cannot update Patient di_patient='||di_patient);
raise;
end upd; end if;
return rows_;
end;
/
show errors
declare rows_ number := deleteDiagnosis(456);
begin dbms_output.put_line('deleted '||rows_||' rows'); end;
/
deleted 1 rows
select * from Patient;
PAT_PERSON PAT_SICK
---------- ----------
123 0
An alternative solution, if you prefer (or must) to use a trigger in your application - declare internal function returning count of patient's diagnoses in the trigger body:
create or replace trigger di_patmustbewell
after delete on diagnosis for each row
declare
numdiseases number;
function getNumDiagnosis (di_patient number) return number is
ret number;
pragma autonomous_transaction;
begin
select count(1) into ret from diagnosis where di_patient = getNumDiagnosis.di_patient;
return ret;
end getNumDiagnosis;
begin
numDiseases := getNumDiagnosis(:old.di_patient);
if(numdiseases = 0) then
update patient set pat_sick = 0 where pat_person = :old.di_patient;
end if;
end;
/
show errors;
Trigger DI_PATMUSTBEWELL compiled
Hope it helps you a bit.
You can create a COMPOUND trigger for such cases:
create or replace TRIGGER Di_PatMustBeWell
FOR DELETE ON Diagnosis
COMPOUND TRIGGER
TYPE Di_Patient_Table_type IS TABLE OF DiagnosisCount.Di_Patient%TYPE;
Di_Patient_Table Di_Patient_Table_type;
BEFORE STATEMENT IS
BEGIN
Di_Patient_Table := Di_Patient_Table_type();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
Di_Patient_Table.EXTEND;
Di_Patient_Table(Di_Patient_Table.LAST) := :OLD.Di_Patient;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN Di_Patient_Table.FIRST..Di_Patient_Table.LAST LOOP
SELECT NUMDISEASES INTO Numdiseases
FROM DiagnosisCount
where Di_Patient = Di_Patient_Table(i);
IF NumDiseases != 1 THEN
UPDATE Patient SET Pat_Sick = 0 WHERE Pat_Person = Di_Patient_Table(i);
END IF;
END LOOP;
Di_Patient_Table.DELETE;
END AFTER STATEMENT;
END;
/

problem with trigger in oracle

the problem is this :
I implemented a trigger on the table called CLAN_AFFILIATI that increases (if inseriemento) and decreases (in case of cancellation) an attribute (NUMAFFILIATI) of another table called CLAN. what I would do is block the update NUMAFFILIATI of Clan by the user and had thought to r another trigge on CLAN that did this:
trigger on CLAN_AFFILIATI(CLAN VARCHAR,AFFILIATO VARCHAR,RUOLO VARCHAR)
CREATE OR REPLACE TRIGGER "AggiornamentoNumAffiliati"
AFTER INSERT OR DELETE ON CLAN_AFFILIATI
FOR EACH ROW
DECLARE
CLAN_APPARTENENZA VARCHAR(20);
BEGIN
IF INSERTING THEN
SELECT NOME INTO CLAN_APPARTENENZA
FROM CLAN
WHERE NOME=:new.CLAN;
UPDATE CLAN
SET NUMAFFILIATI=NUMAFFILIATI+1
WHERE CLAN_APPARTENENZA=NOME;
ELSE
SELECT NOME INTO CLAN_APPARTENENZA
FROM CLAN
WHERE NOME=:old.CLAN;
UPDATE CLAN
SET NUMAFFILIATI=NUMAFFILIATI-1
WHERE CLAN_APPARTENENZA=NOME;
END IF;
END;
trigger on CLAN (NAME VARCHAR ,NUMAFFILIATI INTEGER)
CREATE OR REPLACE TRIGGER "ModificaNumAffiliati"
BEFORE INSERT OR UPDATE OF NUMAFFILIATI ON CLAN
FOR EACH ROW
DECLARE
CONT NUMBER:=0;
BEGIN
IF INSERTING THEN
IF :new.NUMAFFILIATI <> 0 THEN
RAISE_APPLICATION_ERROR(-20016,'NUMERO ERRATO');
END IF;
ELSE
SELECT COUNT(*) INTO CONT
FROM CLAN_AFFILIATI
WHERE :old.NOME=CLAN;
IF CONT <> :new.NUMAFFILIATI THEN
RAISE_APPLICATION_ERROR(-20017,'NUMERO ERRATO');
END IF;
END IF;
END;
but so I'm doing is reporting an error:
error ORA-04091: Table ANTONIO.CLAN_AFFILIATI is being modified, the trigger / function can not read
ORA-06512: at "ANTONIO.ModificaNumAffiliati", line 10
ORA-04088: error during execution of trigger 'ANTONIO.ModificaNumAffiliati'
ORA-06512: at "ANTONIO.AggiornamentoNumAffiliati", line 12
ORA-04088: error during execution of trigger 'ANTONIO.AggiornamentoNumAffiliati
how can I solve this problem ....
This is propably solution:
I tested it with this sample tables:
CREATE TABLE CLAN_AFFILIATI(CLAN VARCHAR2(100),AFFILIATO VARCHAR2(100),RUOLO VARCHAR2(100));
CREATE TABLE CLAN (NOME VARCHAR2(100) ,NUMAFFILIATI NUMBER(10));
You need this helper package.
CREATE OR REPLACE PACKAGE STORE_NOMES
AS
TYPE record_nomes IS RECORD (
nome VARCHAR2(100),
operation VARCHAR2(100) -- insert or delete
);
TYPE array_type_nomes IS TABLE OF record_nomes INDEX BY BINARY_INTEGER;
g_array_nomes array_type_nomes;
END STORE_NOMES;
/
Trigger on CLAN table:
CREATE OR REPLACE TRIGGER MODIFICANUMAFFILIATI
BEFORE INSERT OR UPDATE OF NUMAFFILIATI ON CLAN
FOR EACH ROW
DECLARE
l_CONT NUMBER:=0;
BEGIN
IF INSERTING THEN
-- prevent inserting <> 0
IF :new.NUMAFFILIATI <> 0 THEN
RAISE_APPLICATION_ERROR(-20016,'NUMERO ERRATO');
END IF;
ELSE
SELECT COUNT(*) INTO l_CONT
FROM CLAN_AFFILIATI
WHERE CLAN = :old.NOME;
IF l_CONT <> :new.NUMAFFILIATI THEN
RAISE_APPLICATION_ERROR(-20017,'NUMERO ERRATO');
END IF;
END IF;
END;
/
Before statement trigger on CLAN_AFFILIATI table:
CREATE OR REPLACE TRIGGER TRG_CLAN_AFFILIATI_BEFORE_STMT
BEFORE INSERT OR DELETE
ON CLAN_AFFILIATI
DECLARE
BEGIN
STORE_NOMES.g_array_nomes.DELETE;
END;
/
After statement trigger on CLAN_AFFILIATI table:
CREATE OR REPLACE TRIGGER TRG_CLAN_AFFILIATI_AFTER_STMT
AFTER INSERT OR DELETE
ON CLAN_AFFILIATI
DECLARE
BEGIN
FOR i IN STORE_NOMES.g_array_nomes.FIRST..STORE_NOMES.g_array_nomes.LAST LOOP
IF(STORE_NOMES.g_array_nomes(i).operation = 'INSERTING') THEN
UPDATE CLAN
SET NUMAFFILIATI=NUMAFFILIATI+1
WHERE NOME = STORE_NOMES.g_array_nomes(i).NOME;
ELSIF(STORE_NOMES.g_array_nomes(i).operation = 'DELETING') THEN
UPDATE CLAN
SET NUMAFFILIATI=NUMAFFILIATI-1
WHERE NOME = STORE_NOMES.g_array_nomes(i).NOME;
END IF;
END LOOP;
END;
/
Row Insert/Delete trigger on CLAN_AFFILIATI table:
CREATE OR REPLACE TRIGGER AGGIORNAMENTONUMAFFILIATI
BEFORE INSERT OR DELETE ON CLAN_AFFILIATI
FOR EACH ROW
DECLARE
l_CLAN_APPARTENENZA VARCHAR(20);
BEGIN
IF INSERTING THEN
SELECT NOME INTO l_CLAN_APPARTENENZA
FROM CLAN
WHERE NOME = :new.CLAN;
STORE_NOMES.g_array_nomes(STORE_NOMES.g_array_nomes.COUNT).nome := :new.CLAN;
STORE_NOMES.g_array_nomes(STORE_NOMES.g_array_nomes.LAST).operation := 'INSERTING';
ELSE
SELECT NOME INTO l_CLAN_APPARTENENZA
FROM CLAN
WHERE NOME = :old.CLAN;
STORE_NOMES.g_array_nomes(STORE_NOMES.g_array_nomes.COUNT).nome := :old.CLAN;
STORE_NOMES.g_array_nomes(STORE_NOMES.g_array_nomes.LAST).operation := 'DELETING';
END IF;
END;
/
Now working this (without ORACLE-EXCEPTION):
INSERT INTO CLAN(NOME, NUMAFFILIATI) VALUES('Antonio', 0);
INSERT INTO CLAN_AFFILIATI(CLAN,AFFILIATO,RUOLO) values('Antonio','Affiliato1','Ruolo1');
INSERT INTO CLAN_AFFILIATI(CLAN,AFFILIATO,RUOLO) values('Antonio','Affiliato2','Ruolo2');
Change the first trigger "AggiornamentoNumAffiliati" so that it doesn't immediately try to update clan, but stores the name (NOME) in a PL/SQL-Table within a Package; then, you make an AFTER INSERT OR DELETE (but without the FOR EACH ROW clause) trigger that reads the PL/SQL table from the package and updates the CLANs accordingly.
I don't have my developer tools with me, but it looks to me as though your getting yourself into a cyclic dependency issue of sorts. When your CLAN_AFFILIATI trigger is raised, in it you do an update of CLAN which calls the second trigger, which has a select from the CLAN_AFFILIATI table in the ELSE block.
Maybe the before insert (first query), and after insert(second query) have an affect also.
ORA-04091 is also known as a "mutating table" error - basically, row triggers cannot query or alter the table on which the trigger operates.
#Martin's answer is the classic description of how to work around this issue, but it you're on Oracle 11+ you can use a compound trigger to do the same thing.
Share and enjoy.