Oracle, trigger to control link between tables - oracle

I need to create a trigger to control a link between tables. I have the following table structure (image below). an item is linked to a budget and a structure, and this structure should be linked to the same budget.
Sometimes the item receive wrong structures, receive structures different from its own budget budget. I need that when you insert a budget item or edit a budget item to be validated if the id_budget are equal, the id_budget of dg_budget_item is equal to the id_budget dg_budget_structure to which it is being linked
I started building this trigger, but do not know how to continue
CREATE TRIGGER T_BUDGETO_STRUCTURE_ITEM_ID_STRUCTURE
BEFORE INSERT OR UPDATE OF id_budget_structure ON dg_budget_structure_item
select id_budget from dg_budget_structure where id_budget_structure = new.id_budget_structure
FOR EACH ROW
WHEN (new.id_budget <> ???(result of select?))
pl/sql_block

This should get you close to what you want
CREATE OR REPLACE TRIGGER T_BUDGET_STRUCTURE_ITEM_ID_STRUCTURE
BEFORE INSERT OR UPDATE OF id_budget_structure ON dg_budget_item
FOR EACH ROW
DECLARE
v_id_budget_structure NUMBER;
Budgets_do_not_match EXCEPTION;
v_code NUMBER;
v_errm VARCHAR2(64);
BEGIN
SELECT id_budget INTO v_id_budget_structure FROM dg_budget_structure WHERE id_budget_structure = :new.id_budget_structure
IF :new.id_budget <> v_id_budget_structure THEN
RAISE Budgets_do_not_match;
END IF;
EXCEPTION
WHEN Budgets_do_not_match THEN
Raise_application_error (-20001,
'Budget '||TO_CHAR(:new.id_budget)||' for structure '
|| :new.id_budget_structure || 'does not match the budget trying to be linked';
WHEN NO_DATA_FOUND THEN
Raise_application_error(-20002,
'Invalid budget structure ' ||:new.id_budget_structure);
WHEN OTHERS THEN
v_code := SQLCODE;
v_errm := SUBSTR(SQLERRM, 1 , 64);
Raise_application_error(-20000,
'Unexpected error ' || v_code ': ' || v_errm;
END;
You cannot ROLLBACK within a trigger so your application will need to deal with that.

Related

Exception Handling SQL

I created a user defined function that calculates the quantity in stock for a product
CREATE OR REPLACE FUNCTION function_quantityInStock(
oldProductQuantity IN INTEGER,
orderedQuan IN INTEGER)
RETURN INTEGER
IS
v_newQuantity INTEGER;
v_oldQuantity INTEGER;
v_orderedQuan INTEGER;
BEGIN
v_newquantity := oldProductQuantity - orderedQuan;
RETURN v_newquantity;
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line('Please check your data.');
END function_quantityInstock;
I then create a trigger that updates the table by calling the function
CREATE OR REPLACE TRIGGER TRIGGER_QUANTITY AFTER INSERT ON ordered_product
FOR EACH ROW
DECLARE
v_oldQuantity INTEGER;
BEGIN
SELECT PRODUCT_QUANTITYINSTOCK INTO v_oldQuantity
FROM product
WHERE product_id = :NEW.product_id;
UPDATE PRODUCT
SET product_quantityinstock =
function_quantityINSTOCK(v_oldQuantity, :NEW.ORDERED_PRODUCTQUANTITY)
WHERE product_id = :NEW.product_id;
END;
I want to display a message when the user enters invalid data but my exception block doesnt do that.
I use the following anonyomus block to test the function:
SET SERVEROUTPUT ON;
DECLARE
v_productID ordered_product.product_id%TYPE:= &ProductID;
v_orderID ordered_product.order_id%TYPE:=&OrderID;
v_orderedQuan ordered_product.ordered_productQuantity%TYPE := &OrderedProductQuantity;
v_totalCost ordered_product.ordered_productTotalCost%TYPE := '&TotalCost';
BEGIN
INSERT INTO ordered_product VALUES
(v_orderID, v_productID, v_orderedQuan, v_totalCost);
dbms_output.put_line('A new record has been inserted.');
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('Invalid data!');
WHEN VALUE_ERROR THEN
dbms_output.put_line('Error! Please check your values.'|| SQLERRM);
END;
You might be missing exec dbms_output.enable(10000) and therefore not be seeing the output.
You should throw the exception using
raise_application_error(-20000, 'Please check your data.');
Also it's bad practice to catch all exceptions using when others and then ignore them.
If I were you, I wouldn't rely on dbms_output.put_line to pass information around. Instead, rely on the standard exception handling, e.g. RAISE or RAISE_APPLICATION_ERROR. Also, given that you're trying to find the new quantity, I'd move the select on the product table into the function, something like:
CREATE OR REPLACE FUNCTION function_quantityinstock(p_product_id IN product.product_product_id p_orderedquan IN INTEGER)
RETURN INTEGER IS
v_newquantity INTEGER;
e_not_enough_stock EXCEPTION;
e_not_enough_stock_num INTEGER := -20001;
e_no_product_exists_num INTEGER := -20002;
PRAGMA EXCEPTION_INIT(e_not_enough_stock, -20001);
BEGIN
SELECT product_quantityinstock - orderedquan
INTO v_newquantity
FROM product
WHERE product_id = :new.product_id;
IF v_newquantity < 0
THEN
RAISE e_not_enough_stock;
END IF;
RETURN v_newquantity;
EXCEPTION
WHEN no_data_found THEN
raise_application_error(-e_no_product_exists_num,
'No product exists for product_id = ' || product_id);
-- no need to check for TOO_MANY_ROWS if product_id is the primary/unique key on the product table.
WHEN e_not_enough_stock THEN
raise_application_error(e_not_enough_stock_num,
'Not enough stock present to fulfil the order for product_id = ' ||
product_id);
WHEN OTHERS THEN
raise_application_error(SQLCODE,
'Unexpected error occurred whilst finding new quantity for product_id = ' ||
product_id || ': ' || SQLERRM);
END function_quantityinstock;
/
You'd need to change the function name though, to reflect the action that it's doing, since it's returning the new quantity in stock...

How can I modify my PL/SQL procedure to go to my exception handling?

I am using SQL developer to write a procedure.
The objective is to get the name of the drainage system from one table and get the count of how much the drainage system name code appears in another table.
My procedure works, but when I enter an incorrect value, it does not go to the exception section. For example, when I input ‘Mexico River’, this name does not exist in the table. So, I want my exception section to realize that this is an incorrect value being entered.
My question is how do I modify my code, so it can detect incorrect values and go to my exception section.
Below is a snippet of my code:
PROCEDURE number_of_rivers --second procedure with 1 parameter
(input_sysName IN TBLDrainage.DRAINAGE_SYS%TYPE)
is
-- Create a cursor
cursor c_river is
select code, drainage_sys
from TBLDRAINAGE
where DRAINAGE_SYS = input_sysName;
v_rivercount Number;
r_variable c_river %rowtype;
Begin
FOR r_variable in c_river
loop
select count (Drainage_sys) into v_rivercount
from TBLRIVER
where DRAINAGE_SYS = r_variable.code;
DBMS_OUTPUT.PUT_LINE (UPPER(input_sysName) || ' has ' || v_rivercount || ' river(s) in the drainage system.');
end loop;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE ('Error: Please enter a valid drainage system name');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ('Error in finding system');
END ;
The CURSOR..FOR loop has the property of executing zero or more times. It doesn't throw NO_DATA_FOUND.
There are a couple of solutions. One is to include a count inside the loop, and raise an exception afterwards.
l_count := 0;
FOR r_variable in c_river
loop
....
l_count := l_count + 1;
end loop;
if l_count = 0 then
raise NO_DATA_FOUND;
end if;
The other would be to validate the input parameter at the start of your program.
begin
open c_river;
fetch c_river into r_variable;
if c_river%notfound then
raise NO_DATA_FOUND;
else
select count (Drainage_sys)
into v_rivercount
from TBLRIVER
where DRAINAGE_SYS = r_variable.code;
DBMS_OUTPUT.PUT_LINE (UPPER(input_sysName) || ' has ' || v_rivercount || ' river(s) in the drainage system.');
end if;
close c_river;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE ('Error: Please enter a valid drainage system name');
close c_river;
END ;
In this solution I have removed the loop, because I would expect a look-up on drainage system should be unique and return one record. Please re-instate the loop if your data model isn't like that.
I have retained your names for the cursor and its row variables but you should re-name them. They are used for selecting drainage systems not rivers, and their names ought to reflect that. Discipline in naming things is a useful habit to acquire, as misleading variable names will cause confusion in larger chunks of code.
Also, swallowing exceptions like this is very bad:
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE ('Error in finding system');
Oracle has thousands of error messages: it better to do nothing with the error message than to throw it away.

Oracle trigger checks previous record before insert/update

I would like to create a trigger that prevents a student from enrolling into a new module if he has any outstanding bills.
studID studNRIC paymentStatus
-------------------------------------
200 F7654672F Non Payment
it would reject the following statement:
INSERT INTO student(studID, studNRIC, paymentStatus)
VALUES (201, 'F7654672F', 'Good');
I've came out with the following trigger but I'm still able to insert a new student.
set define off;
CREATE OR REPLACE TRIGGER reject_new_account
AFTER INSERT OR UPDATE ON Student
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
totover NUMBER(3);
BEGIN
SELECT COUNT (*)
INTO totover
FROM Student
WHERE :NEW.nric = student.nric
AND :NEW.paymentStatus = 'Non Payment';
IF totover > 0 THEN
RAISE_APPLICATION_ERROR ( -20002,
'Student' || :NEW.nric ||
' has outstanding bills' );
END IF;
END;
/
there's seems to be a problem with line 13 AND :NEW.paymentStatus = 'Non Payment';
so how do I go about doing this?
table structure
CREATE TABLE Student(
studID INTEGER NOT NULL,
firstName CHAR(25) NULL,
lastName CHAR(25) NULL,
NRIC CHAR(9) NOT NULL,
paymentStatus CHAR(25) Default 'Good',
CONSTRAINT stud_Pkey PRIMARY KEY (studID),
CONSTRAINT studPaymentStatus_type CHECK (PaymentStatus IN ('Late Payment', 'Non Payment', 'Good'))
);
There are a couple of issues with this trigger.
First if you want to prevent an INSERT you need to use a BEFORE trigger. An AFTER trigger will fire after the insert has successfully completed, by which point it is too late to stop the insert.
Secondly, I'm unsure about what you are trying to achieve with your SQL statement. Since the trigger is attached to the customer table you don't need to do a select on the customer table to access the record data. You can just do:
IF :NEW.badstatus = 'Non Payment'
THEN
RAISE_APPLICATION_ERROR ( -20002,
'Employee ' || :NEW.nric ||
' already has 5 readings' );
END IF;
You probably don't want to check if the new record has a bad status, you want to check if the existing customer records have a bad status. As James mentioned, a BEFORE trigger probably makes more sense, but that depends on what you are trying to do, whether the insert gets rollback or not by the caller, etc.
CREATE OR REPLACE TRIGGER reject_new_account
AFTER INSERT OR UPDATE ON Customer
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
totover NUMBER(3);
BEGIN
SELECT COUNT (*)
INTO totover
FROM Customer c
WHERE c.nric = :NEW.nric
AND c.badstatus = 'Non Payment';
IF totover > 0 THEN
RAISE_APPLICATION_ERROR ( -20002,
'Employee ' || :NEW.nric ||
' already has 5 readings' );
END IF;
END;
/
I've removed the PRAGMA AUTONOMOUS_TRANSACTION; for you and replace it with an exception to handle the issue, can you try if it works.
set define off;
CREATE OR REPLACE TRIGGER reject_new_account
BEFORE INSERT OR UPDATE ON CUSTOMER
FOR EACH ROW
DECLARE
totover CHAR(100);
BEGIN
SELECT distinct badStatus
INTO totover
FROM customer
WHERE :NEW.nric = CUSTOMER.nric;
IF totover = 'Non Payment' THEN
RAISE_APPLICATION_ERROR ( -20003,
'Customer ' || :NEW.firstName||''||:NEW.lastName ||
' cannot create new account due to bad payment' );
END IF;
EXCEPTION
WHEN NO_DATA_FOUND
THEN DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
END;
/
As I assume you have discovered, you cannot select from the same table that a row-level trigger is defined against; it causes a table mutating exception. You have attempted to get round this by adding the autonomous transaction pragma. Unfortunately, although this works, it is just covering up your mistake in methodology.
In order to properly create this validation using a trigger a procedure should be created to obtain user-specified locks so the validation can be correctly serialized in a multi-user environment.
PROCEDURE request_lock
(p_lockname IN VARCHAR2
,p_lockmode IN INTEGER DEFAULT dbms_lock.x_mode
,p_timeout IN INTEGER DEFAULT 60
,p_release_on_commit IN BOOLEAN DEFAULT TRUE
,p_expiration_secs IN INTEGER DEFAULT 600)
IS
-- dbms_lock.allocate_unique issues implicit commit, so place in its own
-- transaction so it does not affect the caller
PRAGMA AUTONOMOUS_TRANSACTION;
l_lockhandle VARCHAR2(128);
l_return NUMBER;
BEGIN
dbms_lock.allocate_unique
(lockname => p_lockname
,lockhandle => p_lockhandle
,expiration_secs => p_expiration_secs);
l_return := dbms_lock.request
(lockhandle => l_lockhandle
,lockmode => p_lockmode
,timeout => p_timeout
,release_on_commit => p_release_on_commit);
IF (l_return = 1) THEN
raise_application_error(-20001, 'dbms_lock.request Timeout');
ELSIF (l_return = 2) THEN
raise_application_error(-20001, 'dbms_lock.request Deadlock');
ELSIF (l_return = 3) THEN
raise_application_error(-20001, 'dbms_lock.request Parameter Error');
ELSIF (l_return = 5) THEN
raise_application_error(-20001, 'dbms_lock.request Illegal Lock Handle');
ELSIF (l_return not in (0,4)) THEN
raise_application_error(-20001, 'dbms_lock.request Unknown Return Value ' || l_return);
END IF;
-- Must COMMIT an autonomous transaction
COMMIT;
END request_lock;
This procedure can then be used in a compound trigger (assuming at least Oracle 11, this will need to be split into individual triggers in earlier versions)
CREATE OR REPLACE TRIGGER reject_new_account
FOR INSERT OR UPDATE ON student
COMPOUND TRIGGER
-- Table to hold identifiers of inserted/updated students
g_studIDs sys.odcinumberlist;
BEFORE STATEMENT
IS
BEGIN
-- Reset the internal student table
g_studIDs := g_studIDs();
END BEFORE STATEMENT;
AFTER EACH ROW
IS
BEGIN
-- Store the inserted/updated students; the payment status may be updated
-- without checking the constraint
IF ( INSERTING
OR ( UPDATING
AND ( :new.studID <> :old.studID
OR :new.NRIC <> :old.NRIC)))
THEN
g_studIDs.EXTEND;
g_studIDs(g_studIDs.LAST) := :new.studID;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT
IS
CURSOR csr_students
IS
SELECT sdt.studID
, sdt.NRIC
FROM TABLE(g_studIDs) sid
INNER JOIN student sdt
ON (sdt.studID = sid.column_value)
ORDER BY sdt.NRIC;
CURSOR csr_constraint_violations
(p_studID student.studID%TYPE
,p_NRIC student.studNRIC%TYPE)
IS
SELECT NULL
FROM student sdt
WHERE sdt.NRIC = p_NRIC
AND sdt.paymentStatus = 'Bad Payment'
AND sdt.studID <> p_studID;
r_constraint_violation csr_constraint_violations%ROWTYPE;
BEGIN
-- Check if for any inserted/updated student there exists another record for
-- the same NRIC with a Bad Payment status. Serialise the constraint for each
-- NRIC so concurrent transactions do not affect each other
FOR r_student IN csr_students LOOP
request_lock('REJECT_NEW_ACCOUNT_' || r_student.NRIC);
OPEN csr_constraint_violations(r_student.studID, r_student.NRIC);
FETCH csr_constraint_violations INTO r_constraint_violation;
IF csr_constraint_violations%FOUND THEN
CLOSE csr_constraint_violations;
raise_application_error(-20001, 'Student ' || r_student.NRIC || ' has Bad Payment status');
ELSE
CLOSE csr_constraint_violations;
END IF;
END LOOP;
END AFTER STATEMENT;
END;

Oracle Trigger creation - table is mutating; trigger may not read or modify it

As the table is mutating the following trigger does not work as I believe the SQL statement within the trigger cannot be executed against a mutating table, however as I am not on 11g I cannot create a compound trigger. I have tried including PRAGMA AUTONOMOUS TRANSACTION; in the declaration section, however this would not compile. Could anyone provide me with the best solution?
create or replace
trigger fb_pers_id_check2_tr
--after insert on ifsapp.person_info_tab
before insert on ifsapp.person_info_tab
for each row
begin
declare
-- pragma autonomous_transaction;
v_pid_ person_info_tab.person_id%type;
format_name_ person_info_tab.name%type;
begin
v_pid_ := :new.person_id;
select regexp_replace(upper(:new.name), '\W')
into format_name_
from ifsapp.person_info_tab
where person_id = v_pid_;
if length(v_pid_) < 3 and (length(format_name_) < 21 and v_pid_ <> format_name_) then
raise_application_error(-20001, 'Person ID: ' || v_pid_ || 'is not valid, please enter a valid Person ID, e.g. "' || format_name_ || '".');
end if;
end;
end fb_pers_id_check2_tr;
N.B. In plain English this trigger is intended to stop users setting a person id that is less than 3 characters long and does not equal variable 'format_name_' if it is less than 21 characters long.
Oracle doesn't allow a row trigger to read or modify the table on which the trigger is defined. However, if PERSON_ID is a PRIMARY or UNIQUE key on PERSON_INFO_TAB (which seems to be the case given that it's used in a singleton SELECT) you don't really need to read the table - just use the OLD or NEW values where appropriate:
create or replace trigger fb_pers_id_check2_tr
before insert on ifsapp.person_info_tab
for each row
declare
v_pid_ person_info_tab.person_id%type;
format_name_ person_info_tab.name%type;
begin
v_pid_ := :new.person_id;
format_name_ := REGEXP_REPLACE(UPPER(:new.name), '\W');
if length(v_pid_) < 3 and
length(format_name_) < 21 and
v_pid_ <> format_name_
then
raise_application_error(-20001, 'Person ID: ' || v_pid_ ||
' is not valid, please enter a valid' ||
' Person ID, e.g. "' || format_name_ || '".');
end if;
end fb_pers_id_check2_tr;
Here the code is checking the NEW value of NAME (which I think is right, given that it appears to be validating input), but if the intent was to check the OLD value it's simple to change :NEW to :OLD in the REGEXP_REPLACE call.
Share and enjoy.

first attempt at learning oracle triggers

I am just starting to learn triggers so please bear with me. If the row being inserted has a gift that is the same as any gift already in the table, print a message saying that the gift was already given to receiver from donor.
create or replace TRIGGER Same_Gift_Given
BEFORE INSERT ON GIVING
FOR EACH ROW
DECLARE
giftgiven varchar(255);
BEGIN
SELECT giftname INTO giftgiven from GIVING;
IF :new.giftname = giftgiven then
dbms_output.put_line(giftgiven || ' has already been gifted to ' || giving.receiver || ' by ' || giving.donor);
end if;
END;
This is a really awful homework problem. You would never, ever, ever us a trigger to do anything like this in a real system. It will break most INSERT operations and it will fail if there are ever multiple users. In reality, you would use a constraint. In reality, if for some reason you were forced at gunpoint to use a trigger, you would need a series of three triggers, a package, and a collection to do it properly.
What the professor is probably looking for
Just to emphasize, though, you would never, ever consider doing this in a real system
create or replace trigger same_gift_given
before insert on giving
for each row
declare
l_existing_row giving%rowtype;
begin
select *
into l_existing_row
from giving
where giftname = :new.giftname
and rownum = 1;
dbms_output.put_line( :new.giftname ||
' has already been gifted to ' ||
l_existing_row.receiver ||
' from ' ||
l_existing_row.donor );
exception
when no_data_found
then
null;
end;
This does not prevent you from inserting duplicate rows. It will throw a mutating trigger error if you try to do anything other than an INSERT ... VALUES on the giving table. It is inefficient. It does not handle multiple sessions. In short, it is absolutely atrocious code that should never be used in any real system.
What you would do in reality
In reality, you would create a constraint
ALTER TABLE giving
ADD CONSTRAINT unique_gift UNIQUE( giftname );
That will work in a multi-user environment. It will not throw a mutating trigger exception. It is much more efficient. It is much less code. It actually prevents duplicate rows from being inserted.
Let's try something a bit different:
CREATE OR REPLACE TRIGGER GIVING_COMPOUND_INSERT
FOR INSERT ON GIVING
COMPOUND TRIGGER
TYPE STRING_COL IS TABLE OF VARCHAR2(255) INDEX BY VARCHAR2(255);
colGiftnames STRING_COL;
aGiftname VARCHAR2(255);
nCount NUMBER;
-- Note that the way the associative array is used here is a bit of a cheat.
-- In the BEFORE EACH ROW block I'm putting the string of interest into the
-- collection as both the value *and* the index. Then, when iterating the
-- collection only the index is used - the value is never retrieved (but
-- since it's the same as the index, who cares?). I do this because I'd
-- rather not write code to call a constructor and maintain the collections
-- size - so I just use an associative array and let Oracle do the work for
-- me.
BEFORE EACH ROW IS
BEGIN
colGiftnames(:NEW.GIFTNAME) := :NEW.GIFTNAME;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
aGiftname := colGiftnames.FIRST;
WHILE aGiftname IS NOT NULL LOOP
SELECT COUNT(*)
INTO nCount
FROM GIVING
WHERE GIFTNAME = aGiftname;
IF nCount > 1 THEN
DBMS_OUTPUT.PUT_LINE('Found ' || nCount || ' instances of gift ''' ||
aGiftname || '''');
RAISE_APPLICATION_ERROR(-20001, 'Found ' || nCount ||
' instances of gift ''' ||
aGiftname || '''');
END IF;
aGiftname := colGiftnames.NEXT(aGiftname);
END LOOP;
END AFTER STATEMENT;
END GIVING_COMPOUND_INSERT;
Again, this is a LOUSY way to try to guarantee uniqueness. In practice the "right way" to do this is with a constraint (either UNIQUE or PRIMARY KEY). Just because you can do something doesn't mean you should.
Share and enjoy.

Resources