Creating trigger which throws an exception on insert - sql

Hello fellow programmers and happy new year to you all!
I have few university tasks for winter break and one of them is to create trigger on table:
PERSON(ID, Name, Surname, Age);
Trigger is supposed to inform user when they have inserted row with invalid ID. Vadility criteria is that ID is 11 digits long.
I tried to write solution like this:
CREATE OR REPLACE TRIGGER person_id_trigg
AFTER INSERT
ON person
DECLARE
idNew VARCHAR(50);
lengthException EXCEPTION;
BEGIN
SELECT id INTO idNew FROM INSERTED;
IF LENGTH(idNew) <> 11 THEN
RAISE lengthException;
END IF;
EXCEPTION
WHEN lengthException THEN
dbms_output.put_line('ID for new person is INVALID. It must be 11 digits long!');
END;
Then I realized that INSERTED exists only in sqlserver and not in oracle.
What would you suggest I could do to fix that?
Thanks in advance!

Do you want to raise an exception (which would prevent the insert from succeeding)? Or do you want to allow the insert to succeed and write a string to the dbms_output buffer that may or may not exist and may or may not be shown to a human running the insert?
In either case, you'll want this to be a row-level trigger, not a statement-level trigger, so you'll need to add the for each row clause.
CREATE OR REPLACE TRIGGER person_id_trigg
AFTER INSERT
ON person
FOR EACH ROW
If you want to raise an exception
BEGIN
IF( length( :new.id ) <> 11 )
THEN
RAISE_APPLICATION_ERROR( -20001,
'The new ID value must have a length of 11' );
END IF;
END;
If you want to potentially print output but allow the insert to succeed
BEGIN
IF( length( :new.id ) <> 11 )
THEN
dbms_output.put_line( 'The new ID value must have a length of 11' );
END IF;
END;
Of course, in reality, you would never use a trigger for this sort of thing. In the real world, you would use a constraint.

Related

Oracle trigger: SELECT INTO, no data found

I have the following table that describes which chemical elements each planet is composed of using percentage.
CREATE TABLE elem_in_planet
(
id_planet INTEGER,
element_symbol CHAR(2),
percent_representation NUMBER CONSTRAINT NN_elem_in_planet NOT NULL,
CONSTRAINT PK_elem_in_planet PRIMARY KEY (id_planet, element_symbol),
CONSTRAINT FK_planet_has_elem FOREIGN KEY (id_planet) REFERENCES planet (id_planet),
CONSTRAINT FK_elem_in_planet FOREIGN KEY (element_symbol) REFERENCES chemical_element (element_symbol)
);
I'm trying to make a trigger that warns users when they add a new element to a planet and the sum of elements in that planet exceeds 100%. I came up with this.
CREATE OR REPLACE TRIGGER elem_in_planet_check
AFTER INSERT OR UPDATE ON elem_in_planet
FOR EACH ROW
DECLARE
sum_var NUMBER;
PRAGMA autonomous_transaction;
BEGIN
SELECT SUM(percent_representation)
INTO sum_var
FROM elem_in_planet
WHERE id_planet = :NEW.id_planet
GROUP BY id_planet;
EXCEPTION
WHEN NO_DATA_FOUND THEN
sum_var := 0;
IF sum_var > 100 THEN
DBMS_OUTPUT.put_line('WARNING: Blah blah.');
END IF;
END;
/
This code seems to throw the NO_DATA_FOUND exception every single time, even though I have inserted test data and when I run the SQL query alone, it works as expected.
I'm new to this and don't understand what I'm doing wrong.
Thank you for any advice.
You have NOT inserted the row into the table, 2 reasons.
The trigger runs as part of the insert statement which has not
completed. So the row does not exist.
You specified "PRAGMA autonomous_transaction" (AKA the create
untraceable bug here statement), did you previously get a mutating
table exception. So you cannot see any data inserted/updated/deleted
by the current transaction.Further if an error did occur the row would still be inserted as you did not raise an error or re-raise the existing error. I suggest you familiarize yourself with the PLSQL block structure. For now you may want to try:
You could use an after statement trigger or after statement section of compound trigger to make this test, do raise_application_error if sum > 100;
BTW as it stands your "if on sum_var > 100" runs only when an error occurs. Anything after the "EXCEPTION" and before END for that block runs only when a error occurs.
create or replace trigger elem_in_planet_check
after insert or update on elem_in_planet
declare
error_detected boolean := False;
begin
for planet in
(
select id_planet, sum_var
from (select id_planet, sum(percent_representation) sum_var
from elem_in_planet
group by id_planet
)
where sum_var > 100
)
loop
dbms_output.put_line('Planet ' || planet.id_planet || ' at '|| planet.sum_var || '% resources exceed 100%');
error_detected:= True;
end loop;
if error_detected then
Raise_application_error('-20001', 'Planet resources cannot exceed 100%');
end if;
end elem_in_planet_check;

Updating the record of same table when new record is inserted or updated in oracle

I am new to learning Oracle. I have a task in which I need to update value of any previous record if new record contains its reference.
Table structure is as below :
Review_Table
(review_id number pk,
review_name varchar2,
previous_review number null,
followup_review number null
)
Here previous_review and followup_review columns are objects of same table i.e Review_table.
Now consider we have two records in Review_table A and B, A does not have any previous or followup review. When user creates/updates the record B and he selects record A as previous record, then we want to automatically update (via trigger) the value of A record's followup review with B's Review ID.
I have tried writing following trigger
create or replace trigger "REVIEW_T1"
AFTER insert or update on "REVIEW_TABLE"
for each row
begin
update REVIEW_TABLE
set review_follow_up_review = :new.REVIEW_ID
where REVIEW_ID = :new.REVIEW_PREVIOUS_REVIEW;
end;
But I am getting error as : REVIEW_TABLE is mutating, trigger/function may not see it ORA-06512
I have tried searching everything but was unable to find any solution for it
TL;DR: No trigger, no mutating. Do not use trigger to change another row in the same table.
I absolutely agree with #StevenFeuerstein's comment:
I also suggest not using a trigger at all. Instead, create a package that contains two procedures, one to insert into table, one to update. And within these procedures, implement the above logic. Then make sure that the only way developers and apps can modify the table is through this package (don't grant privs on the table, only execute on the package).
Take a look at the following example.
Prepare the schema:
create table reviews (
id number primary key,
name varchar2 (32),
previous number,
followup number
);
create or replace procedure createNextReview (name varchar2, lastId number := null) is
lastReview reviews%rowtype;
nextReview reviews%rowtype;
function getLastReview (lastId number) return reviews%rowtype is
begin
for ret in (
select * from reviews where id = lastId
for update
) loop return ret; end loop;
raise_application_error (-20000, 'last review does not exist');
end;
procedure insertReview (nextReview reviews%rowtype) is
begin
insert into reviews values nextReview;
exception when others then
raise_application_error (-20000, 'cannot insert next review');
end;
procedure setFollowUp (nextId number, lastId number) is
begin
update reviews set
followup = nextId
where id = lastId
;
exception when others then
raise_application_error (-20000, 'cannot update last review');
end;
begin
if lastId is not null then
lastReview := getLastReview (lastId);
end if;
nextReview.id := coalesce (lastReview.id, 0)+1;
nextReview.name := name;
nextReview.previous := lastId;
insertReview (nextReview);
if lastReview.Id is not null then
setFollowUp (nextReview.id, lastReview.Id);
end if;
exception when others then
dbms_output.put_line (
'createNextReview: '||sqlerrm||chr(10)||dbms_utility.format_error_backtrace ()
);
end;
/
Execute:
exec createNextReview ('first review')
exec createNextReview ('next review', 1)
See the outcome of work done:
select * from reviews;
ID NAME PREVIOUS FOLLOWUP
---------- ---------------- ---------- ----------
1 first review 2
2 next review 1
First you need to read about triggers, mutating table error and compound triggers: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/triggers.htm#LNPLS2005
Your trigger is AFTER UPDATE OR INSERT. Means if you run UPDATE OR INSERT statements on this table, the trigger will fire. But you are trying to update the same table again inside your trigger, which is compl. wrong.
I think you can fix this by rewriting this as a before trigger, rather than an after trigger.

Oracle Trigger - Condition before insert

I don't know what my problem when created new trigger.
Is my syntax correct? Thanks! Console Logging pane
p/s: This my console display when I try to insert values
CREATE OR REPLACE TRIGGER EX03_3
BEFORE INSERT ON HR.CHITIETDATHANG
FOR EACH ROW
DECLARE
TONGHANG NUMBER; -- Total Items
HANGHIENCO NUMBER; -- Items present
HANGDABAN NUMBER; -- Items was sales.
BEGIN
-- Get total Items
SELECT SUM(MH.SOLUONG) INTO TONGHANG
FROM HR.MATHANG MH;
-- Get total Items was sales
SELECT SUM(CTDH.SOLUONG) INTO HANGDABAN
FROM HR.CHITIETDATHANG CTDH;
-- Items present
HANGHIENCO := TONGHANG - HANGDABAN;
IF(HANGHIENCO >= HANGDABAN) THEN
HANGHIENCO := HANGHIENCO-1;
INSERT INTO HR.CHITIETDATHANG VALUES(:NEW.SOHOADON,:NEW.MAHANG,
:NEW.GIABAN,:NEW.SOLUONG,:NEW.MUCGIAMGIA);
ROLLBACK;
END IF;
NULL;
END;
Seem two critical mistakes
1) trigger tries to insert into HR.CHITIETDATHANG in the body of insert trigger of HR.CHITIETDATHANG.
2) to use rollback after an insert statement is useless.
Note : I can see nothing relevant to make raise no_data_found in those select statements. EX03_3 and EX04_4 are confused, as Kaushik Nayak says.

Check if VARCHAR2 contains only alphabets using trigger

I need to write such a trigger that will check name of the person and will print out his/her id if those people have any digits in theirs names.
What I have by now:
set SERVEROUTPUT ON
create or replace trigger BeforeUpdate
Before insert on customer
for each row
declare
n varchar2(10);
counter number;
nextR number:=0;
begin
select count(id_customer) into counter from customer;
LOOP
nextR:= nextR +1;
select cname into n from customer where id_customer = nextR;
if n not like '%[0-9]%' then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if;
exit when nextR = counter;
end loop;
end;
/
It compiles and when I am trying to fire this trigger it do nothing.
I will be grateful for any help.
There are a couple of problems in your code:
using dbms_output in a trigger doesn't really make sense; usually, the INSERT will be performed by client code that doesn't handle the console output.
The sensible thing is to raise an exception instead.
You don't need to perform a SELECT in your trigger code. In fact, doing so will usually either be superfluous or raise a mutating table error. Instead, use :new and :old to refer to the values of the row that was inserted
(minor) naming a before insert trigger BeforeUpdate is somewhat confusing
use a regular expression for testing this business rule (seriously; regexes rule for this kind of thing)
Altogether, here's the fixed version (untested, I don't have an Oracle instance available for testing right now):
create or replace trigger TR_BI_CUSTOMER
Before insert on customer
for each row
begin
if regexp_like(:new.name, '.*[0-9].*') then
raise_application_error(-20001, 'Incorrect name: ' || :new.name);
end if;
end;
Use regular expression to get your result.
In your case, if you get a digit in n, your if clause should be executed.
So,
if regexp_replace(n,'[^[:digit:]]') IS NOT NULL then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if
It seems you are also attempting to use a regular expression for digit. However, what your code is searching is for a string that has [0-9] in it. Like Bat[0-9]Man, which is not your desired result.
In my code, whatever expression is not digit in the given name is being replaced. If the name does not contain any digits, the regular expression would return null. If there is any digit at any place,the expression would return those digits.
You could analyse the following query for better grasping of what is happening here:
select regexp_replace(cname,'[^[:digit:]]') OUTP, cname from customer;
EDIT :
This is not how you write a trigger !
The trigger will be fired each time an insert is going to take place. You don't need the counter. You need to use :NEW reference
set SERVEROUTPUT ON
create or replace trigger update or
Insert on customer
for each row
begin
if regexp_replace(:NEW.cname,'[^[:digit:]]') IS NOT NULL then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if;
end;
/
This is a job for REGEXP_LIKE()! The regex of '\d' matches a number.
SQL> with tbl(id, name) as (
select 1, 'Batman' from dual union
select 2, 'Robin1' from dual union
select 3, 'Supe4rman' from dual union
select 4, '3Joker' from dual
)
select id, name bad_name
from tbl
where regexp_like(name, '\d');
ID BAD_NAME
---------- ---------
2 Robin1
3 Supe4rman
4 3Joker
SQL>
If your goal is to strip out the digits on the way in (but be careful, a company really could have a number in the name like Level3 Communications or 3Com, if it's a person its less likely but these days who knows!) This is untested:
CREATE OR REPLACE TRIGGER customer_bu
BEFORE INSERT OR UPDATE
ON customer
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
-- If the new name contains a digit, strip it.
if regexp_like(:new.name, '\d') then
:new.name := regexp_replace(:new.name, '\d', NULL);
end if;
END customer_bu;
/

Oracle SQL, avoiding errror "table is mutating" (trigger)

i need trigger which will check if updated worker can be moved to other team
CREATE TABLE WORKERS
(
ID_WORKER NUMBER(4,0), --FK
ID_TEAM NUMBER(2,0) --FK
);
My trigger looks like:
CREATE OR REPLACE TRIGGER TEAM_LIMIT
BEFORE INSERT OR UPDATE OF ID_TEAM ON WORKERS
FOR EACH ROW
DECLARE
V_num NUMBER;
BEGIN
SELECT Count(*) INTO V_num FROM Worker WHERE ID_TEAM=:new.ID_TEAM;
IF V_num >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END;
this generate error : "table %s.%s is mutating, trigger/function may not see it" when row is updated. How to write this statements properly to not generate this kind of error?
You can use a compound trigger, it looks like this (not tested):
CREATE OR REPLACE TRIGGER TEAM_LIMIT
FOR INSERT OR UPDATE OF ID_TEAM ON WORKERS
COMPOUND TRIGGER
V_num NUMBER;
TYPE Row_TableType IS TABLE OF WORKERS.ID_TEAM%TYPE;
AffectedTeams Row_TableType;
BEFORE STATEMENT IS
BEGIN
AffectedTeams := Row_TableType(); -- init the table variable
END BEFORE STATEMENT;
-------------------
BEFORE EACH ROW IS
BEGIN
AffectedTeams.EXTEND;
AffectedTeams(AffectedTeams.LAST) := :NEW.ID_TEAM;
END BEFORE EACH ROW;
--------------------
AFTER STATEMENT IS
BEGIN
FOR i IN AffectedTeams.FIRST..AffectedTeams.LAST LOOP
SELECT Count(*) INTO V_num FROM Worker WHERE ID_TEAM=AffectedTeams(i);
IF V_num >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END LOOP;
END AFTER STATEMENT;
END TEAM_LIMIT;
/
You can use a statement trigger. Statement triggers are only fired once for each statement executed rather than once for each row affected. Statement triggers are useful because they don't have the restriction that they can't query the table on which the trigger is declared. They don't have access to the :OLD and :NEW row values but with a little thought you can still accomplish what you're trying to do:
CREATE OR REPLACE TRIGGER TEAM_LIMIT
BEFORE INSERT OR UPDATE OF ID_TEAM ON WORKERS
-- Note: no FOR EACH ROW - therefore, this is a statement trigger
DECLARE
nMax_team_count NUMBER;
BEGIN
SELECT MAX(TEAM_COUNT)
INTO nMax_team_count
FROM (SELECT ID_TEAM, COUNT(*) AS TEAM_COUNT
FROM WORKERS
GROUP BY ID_TEAM));
IF nMax_team_count >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END TEAM_LIMIT;
Instead of looking at the team of the particular work which has been updated we find the counts of workers on each team, then extract the largest count, and if it's more than four we raise the appropriate exception.
Share and enjoy.