ORACLE PL/SQL Update another table value on insert - sql

I am trying to create a trigger on any insert or update on a table which holds Diseases with animalName and diseaseName.
I have another table, Animals, which holds informations like the animalName (which is a primary key; don't comment on the design as it is not mine), and the amountOfDisease he had.
I want that, upon insert, update or delete in the Diseases table, the amount of Diseases is automatically updated.
I have a hard time understanding how I can obtain the current animalName so that I can update his amountOfDisease.
So far, I have this :
CREATE OR REPLACE TRIGGER update_animal_diseases
AFTER INSERT OR UPDATE OR DELETE ON Diseases
FOR EACH ROW
BEGIN
UPDATE Animals SET amountOfDisease = amountOfDisease + 1
WHERE animalName = :NEW.animalName;
END;
/
Which compile but doesn't work, as the values in Animals never get updated on inserting something into Diseases. I also tried this :
CREATE OR REPLACE TRIGGER update_animal_diseases
AFTER INSERT OR UPDATE ON Diseases
FOR EACH ROW
DECLARE
DiseasesCount INTEGER;
BEGIN
SELECT COUNT(*) INTO DiseasesCount
FROM Diseases
WHERE animalName = :OLD.animalName;
UPDATE Animals SET amountOfDisease = DiseasesCount WHERE animalName = :OLD.animalName;
END;
/
As you can see I also don't really understand what the :NEW and :OLD are. How can I solve my problem, which is to update an animal amountOfDisease on any modification of the Diseases table ?
To be clear, what I get on INSERT-ing into Diseases is just nothing. Nothing happens as far as I can tell.

I see two possible causes.
In the first version you are always adding +1, even on delete.
Also if amountofdisease field is null on a record after adding +1 it will null anyway.
Maybe something like this should work for you.
Tables:
CREATE TABLE animals
( animalname VARCHAR2 (10),
amountofdisease NUMBER);
CREATE TABLE diseases
( animalname VARCHAR2 (10),
diseasename VARCHAR2 (20));
Trigger:
CREATE OR REPLACE TRIGGER apps.diseases_aiud1
BEFORE DELETE OR INSERT OR UPDATE
ON diseases
REFERENCING NEW AS new OLD AS old
FOR EACH ROW
DECLARE
BEGIN
IF INSERTING
THEN
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) + 1
WHERE animalname = :new.animalname;
ELSIF DELETING
THEN
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) - 1
WHERE animalname = :old.animalname;
ELSIF UPDATING
THEN
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) + 1
WHERE animalname = :new.animalname;
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) - 1
WHERE animalname = :old.animalname;
END IF;
END diseases_aiud1;
/
Note de use of :new and :old depending on the event.
Loading sample animals:
insert into animals values ('jaguar',0);
insert into animals values ('capibara',0);
insert into animals values ('fox',0);
commit;
Test 1 Insert
insert into diseases values
('jaguar','pneumonia');
insert into diseases values
('jaguar','epistaxis');
commit;
select *
from animals;
Result 1:
1 row created.
1 row created.
Commit complete.
ANIMALNAME AMOUNTOFDISEASE
---------- ---------------
jaguar 2
capibara 0
fox 0
3 rows selected.
Test 2 delete:
delete from diseases
where animalname = 'jaguar' and diseasename = 'pneumonia'
;
insert into diseases values
('fox','hydrophobia');
Result 2:
ANIMALNAME AMOUNTOFDISEASE
---------- ---------------
jaguar 1
capibara 0
fox 1
Test 3 Update:
update diseases
set animalname = 'capibara'
where animalname = 'fox';
Result 3:
ANIMALNAME AMOUNTOFDISEASE
---------- ---------------
jaguar 1
capibara 1
fox 0
As a side note it should be recommended to write a package in order to handle this logic. Triggers are tricky, hardest to maintain and can lead to unexpected results in some scenarios.
Regards,

If I had to speculate, you don't have matching rows in Animals for all animals in Diseases. You can find these by doing:
select d.*
from diseases d
where not exists (select 1 from animals a where a.animal = d.animal);
If this is the case, then you should structure the database to have an explicit foreign key relationship from diseases to animals. This will ensure that only valid animals are in the table.

As far as trigger,
:NEW means when New record in inserted to your primary table(primary table here means a table where you sets triggers on insert) it takes always New I'd to insert record related records as per trigger in relative table (relative table means in trigger you defined to insert / update record when primary table effected. )
:OLD means in relative table it takes last inserted I'd as new id in relative table. It can be use as foriegn key in second table.
I hope it's meaning full for your question.

Related

How to create a trigger on table A that inserts/updates entity in table B?

I have tables TableAAA, TableBBB, TableCCC and TableCollected. The AAA/BBB/CCC tables have a DateTime column:
table_aaa: DateTime modified
table_bbb: DateTime modified
table_ccc: DateTime modified
I'd like to have a 1-1 trigger on each table that runs after inserting/updating. It should save the biggest DateTime from given table into the TableCollected table. So if TableAAA table contains 3 rows with dates 1980-01-01, 1990-01-01 and 2010-01-01 then in TableCollected I'd have a row that contains the name of the table (or some identifier, it's okay if I have to set it) and the biggest date table_collected: name (table_aaa), modified (2010-01-01).
If TableBBB has rows with 1999-01-01, 2012-04-01, TableCollected should have 2 rows:
name datetime
table_aaa 2010-01-01
table_bbb 2012-04-01
So TableCollected would hold the biggest DateTime of other tables. It should contain each table's DateTime value just once. The name is unique and if it inserted once, it should update every time a new row is inserted in table AAA/BBB/CCC and modified value is bigger than the old value.
You need to create 3 triggers for every table, one for insert, one for update and one for delete (if the most recently modified record is deleted):
CREATE TRIGGER table_aaa_insert AFTER INSERT ON table_aaa
BEGIN
INSERT OR REPLACE INTO TableCollected (name, modified)
SELECT 'table_aaa', max(modified) FROM table_aaa;
END;
CREATE TRIGGER table_aaa_update AFTER UPDATE ON table_aaa
BEGIN
INSERT OR REPLACE INTO TableCollected (name, modified)
SELECT 'table_aaa', max(modified) FROM table_aaa;
END;
CREATE TRIGGER table_aaa_delete AFTER DELETE ON table_aaa
BEGIN
INSERT OR REPLACE INTO TableCollected (name, modified)
SELECT 'table_aaa', max(modified) FROM table_aaa;
END;
Note that name must be a primary key or a unique field of TableCollected to take advantage of INSERT OR REPLACE
However, depending on the rate of insert/update/delete operations on those tables, it could be more efficient to replace your TableCollected table with a view which dynamically returns the desired values:
CREATE VIEW TableCollected (name, modified) as
SELECT 'table_aaa', max(modified) from table_aaa
UNION ALL
SELECT 'table_bbb', max(modified) from table_bbb
UNION ALL
SELECT 'table_ccc', max(modified) from table_ccc;
In both solutions, an index on modified field in tables aaa, bbb, and ccc would be beneficial to performance.
EDIT:
On second thought, a more efficient way which doesn't require to calculate max(modified) every time a record is inserted or updated is this:
CREATE TRIGGER table_aaa_insert AFTER INSERT ON table_aaa
BEGIN
UPDATE TableCollected SET modified=NEW.modified WHERE name='table_aaa' AND modified<NEW.modified;
END;
CREATE TRIGGER table_aaa_update AFTER UPDATE ON table_aaa
BEGIN
UPDATE TableCollected SET modified=NEW.modified WHERE name='table_aaa' AND modified<NEW.modified;
END;
DELETE trigger doesn't change because in that case you need to search for max(modified) anyway.
Note that this solution requires to pre-populate TableCollected with a row for each table. Also note that in the trigger modified refers to the TableCollected field, and NEW.modified refers to the table_aaa field which was just inserted or updated.
The answer to your question is Yes. Here is an example from the official docs:
CREATE TRIGGER update_customer_address UPDATE OF address ON customers
BEGIN
UPDATE orders SET address = new.address WHERE customer_name = old.name;
END;

How do I create this trigger?

I have two tables: table1 and table2. I have a trigger in table1 that inserts the current row into table2 based on some conditions. If the row gets inserted into table2, I want to delete that row from table1. Now in Oracle, it seems we cannot delete the current row from the trigger in table1 itself.
A row level trigger on a table can manipulate the data of the updated rows. It cannot perform additional dml on the table itself (select, insert, delete).
A possible solution is to create a view on table1 with an INSTEAD OF trigger that deletes from table1 and perform all insert/update/delete statements on the view instead of the table
Example: when test_table.name is updated to 'KOEN', then row itself will be deleted. This example shows just an ON UPDATE trigger but it can be done for INSERT/UPDATE/DELETE:
CREATE TABLE test_table (
id NUMBER
GENERATED BY DEFAULT ON NULL AS IDENTITY
CONSTRAINT test_table_id_pk PRIMARY KEY,
name VARCHAR2(100 CHAR)
);
CREATE VIEW test_table_v AS
SELECT
id,
name
FROM
test_table;
CREATE OR REPLACE TRIGGER test_table_v_bu
INSTEAD OF UPDATE ON test_table_v
FOR EACH ROW
DECLARE
BEGIN
-- do your stuff on the other table.
IF :NEW.NAME = 'KOEN' THEN
DELETE FROM test_table WHERE id = :NEW.ID;
END IF;
END;
/
koen>INSERT INTO test_table ( name ) VALUES ( 'JIM' );
1 row inserted.
koen>select * from test_table;
ID NAME
_____ _______
3 JIM
koen>update test_table_v set name= 'KOEN';
1 row updated.
stapp_dev--SAMPLEAPPS>select * from test_table;
no rows selected
koen>

Incrementing a column value in a SQL database if there is a duplicate row

Suppose I have a table like this:
Number Product Type
1 Meat Cow
1 Milk A
If I insert a new row:
INSERT INTO t1 (Product, Type) VALUES (Meat, Cow)
I don't want a new row. I want the column "Number" where Product = Meat and Type = Cow be incremented by 1:
Number Product Type
2 Meat Cow
1 Milk A
Is there a SQL command to do this?
You may try the following by using ON CONFLICT that starts from version 9.5 :
create table t1( Number int,
Product varchar(15),
Type varchar(15),
primary key (Product, Type)
);
insert into t1(Number,Product,Type) values(1,'Meat','Cow');
insert into t1(Number,Product,Type) values(1,'Meat','Cow')
on conflict (Product,Type) do update set Number = t1.Number + 1;
select * from t1;
number product type
------ ------- ----
2 Meat Cow
Rextester Demo
where composite unique(primary) key is a must for Product and Type columns, if not exists than 42P10: there is no unique or exclusion constraint matching the ON CONFLICT specification error raises.
You can use triggers to achieve what you want. Format :
CREATE OR REPLACE TRIGGER trigger_name
INSTEAD OF INSERT ON table_name
WHEN (your_condition_here) --for example, row exist
BEGIN
UPDATE ... --your update query
END;
Create Unique Index on Product and Type, then
INSERT INTO t1 (Product, Type) VALUES (Meat, Cow)
ON CONFLICT ON CONSTRAINT <your_key>
UPDATE set Number = Number + 1
Should work.

SQL - How to create a trigger for two joined tables which is used for inserting

Ok , so I know that inserting information in a view based on two joined tables is impossible.
In order to do so , I need to create a trigger to insert the information in both tables , when an insert is made in that view.
For example :
CREATE VIEW myJoinedView AS
SELECT name,g.value from students
JOIN grades g on g.id=students.id;
The trigger is not working :
CREATE TRIGGER myTrigger
INSTEAD OF INSERT ON myJoinedView
BEGIN
INSERT INTO students
(name,value)
SELECT i.myJoinedView
FROM inserted i
INNER JOIN grades
ON i.id = grades.id
END myTrigger;
Then I'm trying to insert :
INSERT INTO myJoinedView VALUES ('Alex',10);
I don't know if the syntax is correct , I did not find any helpful documentation on this specific type of trigger.
I'm getting this error:
Error(10,46): PLS-00103: Encountered the symbol "end-of-file" when
expecting one of the following: ( begin case declare end exception
exit for goto if loop mod null pragma raise return select update
while with
<< continue close current delete fetch lock
insert open rollback savepoint set sql execute commit forall merge
pipe purge
Any help will be well received.
Thank you!
You need to either perform the inserts separately with separate single table insert or merge statements or by using a multi table insert (insert all) statement. Assuming you have a sequence to generate the id you are joining on for example this code will work in a very rudimentary way, but has some significant issues:
create table students ( id number primary key
, name varchar2(60));
create table grades( id number not null
, value number
, constraint grades_fk1 foreign key (id) references students(id));
create sequence student_id_seq;
create or replace view studentgrades as
select name, value from students s join grades g on s.id = g.id;
create or replace trigger studentgrades_ii_trg
instead of insert on studentgrades
begin
insert all into students(id, name) values (student_id_seq.nextval, name)
into grades(id, value) values (student_id_seq.nextval, value)
select :new.name name, :new.value value from dual;
end;
/
insert into studentgrades values ('Alex',10);
insert into studentgrades values ('Alex',8);
The BIG issue with the above trigger is that every time a grade is inserted for 'Alex' a new student record for 'Alex' is also created instead of reusing the previous student record for 'Alex'. That's probably not the desired behavior. Instead it should probably just insert a new grade record for Alex. One way to acheive this is for the studentgrades view to include the id column from the students table so you can uniquely identify which student to add the grade to, updating the trigger as needed:
create or replace view studentgrades as
select s.id, name, value from students s join grades g on s.id = g.id;
create or replace trigger studentgrades_ii_trg
instead of insert on studentgrades
declare
newid students.id%type;
begin
if :new.id is null then
newid := student_id_seq.nextval;
else
newid := :new.id;
end if;
insert all when :new.id is null
then into students(id, name) values (id, name)
else into grades(id, value) values (id, value)
select newid id, :new.name name, :new.value value from dual;
end;
/
insert into studentgrades values (null, 'Paul',10);
insert into studentgrades values (student_id_seq.currval, 'Paul',8);
However, now what happens if you try this:
insert into studentgrades values (student_id_seq.currval, 'Mary',10);
In this case the name is effectively ignored and Paul gets a new grade so again this isn't quite right. The question is should Paul's name be updated to Mary, or should a new student record for Mary be created, or should an exception be raised?

In a trigger in Oracle, how to get the information out of the `where` from an update?

Say we have a simple table:
ID | Name
1 | "A"
2 | "B"
An update can be
update set Name = "C" where ID = 1
So when creating a trigger, I know how to get the new row's info by :NEW., but the ID won't be in there as I tried.
How can I get the ID information out of the newly updated row?
What exactly did you try? The following works for me:
set serveroutput on;
drop table test;
create table test (id number, name varchar2(10));
insert into test values (1, 'A');
insert into test values (2, 'B');
commit;
create or replace trigger test_trg
before update on test
for each row
begin
dbms_output.put_line('id of row being updated is: '||:old.id||' and it''s new value is: '||:new.id);
end;
/
update test
set name = 'C'
where id = 1;
commit;
and the output I got was:
Table dropped.
Table created.
1 row created.
1 row created.
Commit complete.
Trigger created.
id of row being updated is: 1 and it's new value is: 1
1 row updated.
Commit complete.
Bear in mind that if you ever do batch operations on the table in question that is covered by the scope of a for-each-row-trigger, you're going to turn it from a set operation into a row-by-row operation, so you will see performance degredation. I'd also question what it is you're using the trigger to do; in my experience, you can do most things without needing to resort to a trigger!
Simply refer it as ":OLD.column-name".
In a Row-level UPDATE trigger, :OLD represents the existing value in the column, prior to the update being applied.
In your case, you could do -
old_id := :old.ROWID