PL/SQL Insert check - oracle

Just having a problem with ensuring a constraint is not violated.
Goal: To insert a record that does not violate a constraint.
Constraint: An order should not be allowed if it is discontinued or out of stock.
Now I have a procedure that I was working on and have the necessary syntax working. Just need the finishing touches to get this insert procedure working.
/* Creating Procedure */
CREATE PROCEDURE INSERT_ORDER_DETAIL(
--PARAMETERS
p_product_name IN VARCHAR,
p_unit_price IN NUMBER,
p_quantity IN NUMBER,
p_discount IN NUMBER) IS
--VARIABLES AND DECLARATION FOR PROCEDURE
p_order_id NUMBER;
PRODUCT_LIST PRODUCT%ROWTYPE;
Fail EXCEPTION;
BEGIN
-- Check for product availability
BEGIN
SELECT *
INTO PRODUCT_LIST
FROM PRODUCT
WHERE DISCONTINUED = 'N'; -- select all orders available
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('ERROR ! PRODUCT IS UNAVAILABLE');
RAISE Fail;
END;
--generate new order ID
SELECT MAX(ORDER_ID)+1
INTO p_order_id
FROM ORDER_DETAIL;
-- INSERTING A NEW ORDER
INSERT INTO ORDER_DETAIL VALUES (p_order_id, p_product_name, p_unit_price, p_quantity, p_discount);
COMMIT;
DBMS_OUTPUT.PUT_LINE('DONE!');
EXCEPTION
WHEN Fail THEN
ROLLBACK;
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM);
ROLLBACK;
END INSERT_ORDER_DETAIL;
/

You have two simple approaches, implement either of them.
In your insert procedure, add an IF condition to check whether the product is available in the inventory against the input order, and put the INSERT statement inside the IF block. Else, raise an EXCEPTION product not available. Similarly check for quantity as well.
Or,
Create a FUNCTION based on above logic that returns TRUE if product exists. You could simply go for the count in the table for that matching product in the function.
Based on the function's return value TRUE, call the INSERT procedure. Else, if FALSE, raise an exception that product is not available.
Put all the functions, procedures in one package.

Related

How to create user defined exception in a procedure?

Using a procedure adds an employee record into the employee table, also validates the input values salary must be always greater than or equal to 500, raise a user-defined exception for any validation error.
You can create something like this. Add parameters for other columns as well
CREATE OR REPLACE PROCEDURE insert_emp (salary NUMBER)
AS
invalid_salary EXCEPTION;
BEGIN
IF salary < 500 THEN
RAISE invalid_salary;
ELSE
INSERT INTO employees(salary) VALUES (salary);
COMMIT;
END IF;
END;
You can also associate an Exception number with the exception
Exception number for user-defined exceptions must be in the range of -20000 to -20999
CREATE OR REPLACE PROCEDURE insert_emp (salary NUMBER)
AS
invalid_salary EXCEPTION;
PRAGMA EXCEPTION_INIT (invalid_salary, -20100);
BEGIN
IF salary < 500 THEN
RAISE invalid_salary;
ELSE
INSERT INTO employees(salary) VALUES (salary);
COMMIT;
END IF;
END;
This sounds like a homework problem so I'll let using a trigger go, except a possible gotta and th show a better way of handling it. You raise the user defined exception "invalid salary" however unless your code handles it Oracle will throw the error "ORA-06510 Unhandled user defined exception", indicating it does not know what with it. So what is a better way: Define the column and include a check constraint on it.
Create table employees (...
, salary number(8,2) not null
constraint salary_less_then_minimum check (salary >= 500)
, ...);

Statement level trigger to enforce a constraint

I am trying to implement a statement level trigger to enforce the following "An applicant cannot apply for more than two positions in one day".
I am able to enforce it using a row level trigger (as shown below) but I have no clue how to do so using a statement level trigger when I can't use :NEW or :OLD.
I know there are alternatives to using a trigger but I am revising for my exam that would have a similar question so I would appreciate any help.
CREATE TABLE APPLIES(
anumber NUMBER(6) NOT NULL, /* applicant number */
pnumber NUMBER(8) NOT NULL, /* position number */
appDate DATE NOT NULL, /* application date*/
CONSTRAINT APPLIES_pkey PRIMARY KEY(anumber, pnumber)
);
CREATE OR REPLACE TRIGGER app_trigger
BEFORE INSERT ON APPLIES
FOR EACH ROW
DECLARE
counter NUMBER;
BEGIN
SELECT COUNT(*) INTO counter
FROM APPLIES
WHERE anumber = :NEW.anumber
AND to_char(appDate, 'DD-MON-YYYY') = to_char(:NEW.appDate, 'DD-MON-YYYY');
IF counter = 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'error msg');
END IF;
END;
You're correct that you don't have :OLD and :NEW values - so you need to check the entire table to see if the condition (let's not call it a "constraint", as that term has specific meaning in the sense of a relational database) has been violated:
CREATE OR REPLACE TRIGGER APPLIES_AIU
AFTER INSERT OR UPDATE ON APPLIES
BEGIN
FOR aRow IN (SELECT ANUMBER,
TRUNC(APPDATE) AS APPDATE,
COUNT(*) AS APPLICATION_COUNT
FROM APPLIES
GROUP BY ANUMBER, TRUNC(APPDATE)
HAVING COUNT(*) > 2)
LOOP
-- If we get to here it means we have at least one user who has applied
-- for more than two jobs in a single day.
RAISE_APPLICATION_ERROR(-20002, 'Applicant ' || aRow.ANUMBER ||
' applied for ' || aRow.APPLICATION_COUNT ||
' jobs on ' ||
TO_CHAR(aRow.APPDATE, 'DD-MON-YYYY'));
END LOOP;
END APPLIES_AIU;
It's a good idea to add an index to support this query so it will run efficiently:
CREATE INDEX APPLIES_BIU_INDEX
ON APPLIES(ANUMBER, TRUNC(APPDATE));
dbfiddle here
Best of luck.
Your rule involves more than one row at the same time. So you cannot use a FOR ROW LEVEL trigger: querying on APPLIES as you propose would hurl ORA-04091: table is mutating exception.
So, AFTER statement it is.
CREATE OR REPLACE TRIGGER app_trigger
AFTER INSERT OR UPDATE ON APPLIES
DECLARE
cursor c_cnt is
SELECT 1 INTO counter
FROM APPLIES
group by anumber, trunc(appDate) having count(*) > 2;
dummy number;
BEGIN
open c_cnt;
fetch c_cnt in dummy;
if c_cnt%found then
close c_cnt;
RAISE_APPLICATION_ERROR(-20001, 'error msg');
end if;
close c_cnt;
END;
Obviously, querying the whole table will be inefficient at scale. (One of the reasons why triggers are not recommended for this sort of thing). So this is a situation in which we might want to use a compound trigger (assuming we're on 11g or later).

sql command not ended properly after executing the function

for example I have created a table with table name 'aaa' with four columns act_num, clear_balance, available_balance, total_balance and I have inserted some values.
The function
deb_amount withdraws money from a bank account. It accepts an account
number and an amount of money as parameters. It uses the account number to
retrieve the account balance from the database, then computes the new balance. If this
new balance is less than zero then the function jumps to an error routine; otherwise,
it updates the bank account.
create or replace function deb_amount(p_act_num VARCHAR2, p_amount number )
return number as
declare
v_old_amount number;
v_new_amount number;
e_over_drawn exception;
begin
select clear_balance into v_old_amount from aaa where act_num=p_act_num;
v_new_amount:=v_old_amount-p_amount;
if v_old_amount<p_amount then
raise e_over_drawn;
else
update aaa set clear_balance=v_new_amount,available_balance=v_new_amount,total_balance=v_new_amount where act_num=p_act_num;
end if;
commit;
return clear_balance;
exception
when e_over_drawn then
rollback;
end;
it will compile, but with warnings.
If I want to execute the 'select * from deb_amount(1,100)' it show error.
sql command not ended properly.
Thank you.
You need to call function using dual. Ex:
select deb_amount(1,100) from dual;
or using a variable in plsql block
declare
l_return number;
begin
l_return:=deb_amount(1,100);
end;
It looks like you might be running several commands as a scipt, but haven't ended the function properly. The / after the function creation has to be on a line on its own, and at the start of the line:
create or replace function deb_amount(p_act_num VARCHAR2,
p_amount number)
return number as
declare
v_old_amount number;
v_new_amount number;
e_over_drawn exception;
begin
select clear_balance into v_old_amount
from aaa where act_num=p_act_num;
v_new_amount:=v_old_amount-p_amount;
if v_old_amount<p_amount then
raise e_over_drawn;
else
update aaa set clear_balance=v_new_amount,
available_balance=v_new_amount,
total_balance=v_new_amount
where act_num=p_act_num;
end if;
commit;
return clear_balance;
exception
when e_over_drawn then
rollback;
end;
/
show errors
select deb_account('1', 1) from dual;
The show errors will tell what actual compilation errors you got. It looks like it will complain about the return as you don't have a local clear_balance variable, but you can use v_new_amount instead here. You need to return something after the rollback too, or raise an exception which might be more useful.
As Manjunatha said, your query then needs to call the function properly, with the from clause referencing a table, rather than the function itself.
You have a bigger problem with the concept though; you can't call a function that does DML (insert, update, delete) from SQL, only from a PL/SQL block. Generally DML should be done from a procedure rather than a function, if it has to be done in PL/SQL at all.

PLSQL: BEFORE INSERT TRIGGER (check value in column from other table before allowing insert)

I've made a simple DVD store database. The DVD table has a column "status" which can be either 'FOR_RENT','FOR_SALE','RENTED',or 'SOLD'. I want to write a trigger to block any insertions into my RENTALS table if the status column in the DVD table is not set to 'FOR_RENT'.
Much of the documents I've looked at generally don't show example using values from two different tables so I'm a bit flummaxed.
This is what I believe has been my best attempt so far:
CREATE OR REPLACE TRIGGER RENTAL_UNAVAILABLE
BEFORE INSERT ON RENTAL;
FOR EACH ROW
WHEN (DVD.STATUS != 'FOR_RENT')
DECLARE
dvd_rented EXCEPTION;
PRAGMA EXCEPTION_INIT( dvd_rented, -20001 );
BEGIN
RAISE dvd_rented;
EXCEPTION
WHEN dvd_rented THEN
RAISE_APPLICATION_ERROR(-20001,'DVD has been rented');
END;
/
I'm getting this error:
ORA-00911: invalid character
Try this - I have not complied the code, but should be good. In case you see any compilation issues let me know and post schema on sqlfiddle.com
CREATE OR REPLACE TRIGGER rental_unavailable
BEFORE INSERT
ON rental
FOR EACH ROW
DECLARE
dvd_rented EXCEPTION;
PRAGMA EXCEPTION_INIT (dvd_rented, -20001);
n_count NUMBER (1);
BEGIN
SELECT COUNT (*)
INTO n_count
FROM dvd
WHERE dvd_id = :NEW.dvd_id AND dvd.status = 'FOR_RENT' AND ROWNUM < 2;
IF n_count > 0
THEN
RAISE dvd_rented;
END IF;
EXCEPTION
WHEN dvd_rented
THEN
raise_application_error (-20001, 'DVD has been rented');
END;

Customize PL/SQL exceptions in Oracle

Frequently I found myself doing some functions to insert/delete/update in one or more tables and I've seen some expected exceptions been taken care of, like no_data_found, dupl_val_on_index, etc. For an insert like this:
create or replace FUNCTION "INSERT_PRODUCTS" (
a_supplier_id IN FORNECEDOR.ID_FORNECEDOR%TYPE,
a_prodArray IN OUT PRODTABLE
)
RETURN NUMBER IS
v_error_code NUMBER;
v_error_message VARCHAR2(255);
v_result NUMBER:= 0;
v_prod_id PRODUTO.ID_PROD%TYPE;
v_supplier FORNECEDOR%ROWTYPE;
v_prodInserted PROD_OBJ;
newList prodtable := prodtable();
BEGIN
SELECT FORNEC_OBJ(ID_FORNECEDOR,NOME_FORNECEDOR,MORADA,ARMAZEM,EMAIL,TLF,TLM,FAX) into v_supplier from fornecedor where id_fornecedor = a_supplier_id;
FOR i IN a_prodArray.FIRST .. a_prodArray.LAST LOOP
INSERT INTO PRODUTO (PRODUTO.ID_PROD,PRODUTO.NOME_PROD,PRODUTO.PREC_COMPRA_PROD,PRODUTO.IVA_PROD,PRODUTO.PREC_VENDA_PROD,PRODUTO.QTD_STOCK_PROD,PRODUTO.QTD_STOCK_MIN_PROD)
VALUES (S_PRODUTO.nextval,a_prodArray(i).NOME_PROD,a_prodArray(i).PREC_COMPRA_PROD,a_prodArray(i).IVA_PROD,NULL,NULL,NULL);
/* If the above insert didn't failed, we can insert in weak entity PROD_FORNECIDO. */
SELECT ID_PROD into v_prod_id from PRODUTO where NOME_PROD = a_prodArray(i).NOME_PROD;
INSERT INTO PROD_FORNECIDO VALUES (a_supplier_id, v_prod_id,a_prodArray(i).PREC_COMPRA_PROD);
SELECT PROD_OBJ(ID_PROD,NOME_PROD,PREC_COMPRA_PROD,PREC_VENDA_PROD,QTD_STOCK_PROD,QTD_STOCK_MIN_PROD,IVA_PROD) into v_prodInserted from PRODUTO where ID_PROD= v_prod_id;
a_prodarray(i).ID_PROD := v_prod_id;
END LOOP;
INSERT INTO FORNECPRODS VALUES (a_supplier_id,v_supplier, a_prodarray);
v_result:= 1;
RETURN v_result;
COMMIT;
Exception
When no_data_found then
v_error_code := 0;
v_error_message:= 'Insert Products: One of selects returned nothing';
Insert Into errors Values (v_error_code,v_error_message, systimestamp);
RETURN v_result;
When others Then
ROLLBACK;
v_error_code := SQLCODE;
v_error_message:=substr(SQLERRM,1,50);
Insert Into errors Values (v_error_code,'Error inserting products list',systimestamp);
RETURN v_result;
END;
I would like to customize more of my exceptions or do an exception block for each select/insert. Is that possible or correct?
If so, could please show me some code with important exceptions being throwed by this function?
If you just want to substitute your own error message, there is RAISE_APPLICATION_ERROR...
When no_data_found then
RAISE_APPLICATION_ERROR(-20000
, 'Insert Products: One of selects returned nothing';
, true);
The third parameter returns the original error as well as your custom one.
Oracle also gives us the option to define our exceptions. This can be useful if we want to pass the exception to a calling program...
Declare
no_product_found exception;
Begin
....
When no_data_found then
raise no_product_found;
This would be most effective if we defined the NO_PRODUCT_FOUND exception in a package specification where it could be referenced by external program units.
In addition, Oracle provides the INIT_EXCEPTION pragma which allows us to associate Oracle error numbers with our custom exceptions. Unfortunately we cannot overload error numbers which Oracle has already defined (for instance, we cannot create our own exceptions for ORA-1403 which is already covered by the NO_DATA_FOUND exception). Find out more.
In the exception section; you can raise application error or return 0 with error code explanation. It is about your choice.
If you want to log your errors in the exception section (or in main section), write your own logging procedure with AUTONOMOUS TRANSACTION. so, your logging mechanism is not affected by your main transaction's COMMIT or ROLLBACK. (see: http://www.dba-oracle.com/t_autonomous_transaction.htm)
Another logging mechanism (DML Error Log) in Oracle 10gR2 (and above) is LOG ERRORS clause (see: http://www.oracle-base.com/articles/10g/DmlErrorLogging_10gR2.php).

Resources