Best way to manage exception in package - oracle

I have a package with a several procedures and functions, the procedures are called from an external program and in turn they call functions.
Where is the best way to manage a Exception?
For example, Prog1 call Proc1 and Proc1 call the Funct1, if in function I have an exception ("TOO MANY ROW" or "NO DATA FOUND"), which is the best way to raise a specific customized message and return immediatally to Prog1.
In this moment I have this
PROCEDURE_1
BEGIN
*code*
CALL FUNCTION_1
*code*
EXCEPTION
WHEN NO DATA FOUND THEN
*print customized message*
RETURN;
END;
FUNCTION_1
BEGIN
*code*
EXCEPTION
WHEN NO DATA FOUND THEN
RAISE;
END;
It is the best way?
Regards,
Marco

Ideally a procedure would be allowed to fail with an exception and error stack if something went totally wrong, but then the difference between a fatal error and an expected condition will vary depending on the business logic you are implementing, so it's hard to say what should happen in your particular case.
If the agreed interface is that the procedure should return a formatted message, and that message may include expected business conditions such as a product being out of stock, then you can handle that within the procedure using something like this (nonsense pseudocode to illustrate approach only):
create or replace procedure procedure_1
( p_result_message out varchar2 )
as
somevar number;
begin
do_stuff('fruit','cake');
begin
somevar := function_1('bananas');
exception
when no_data_found then
p_result_message := 'No kittens are available for this mission.';
return;
end;
p_result_message := 'Cake is available in aisle 3';
exception
when something_else then
p_result_message := 'Self-destruct sequence initiated.';
end;
(For a more purist approach you might prefer to rearrange the code so that it always reaches the end with a value for p_result_message, rather than quitting partway through if some condition pops up.)
Now you have a way to handle whatever exceptions might reasonably arise in function_1, without any special handling within the function itself.
You could also have the function raise an exception defined in a package, although then you lose the ability to define a diagnostic error message at the point of failure, and in my experience this just tends to complicate things. But to illustrate:
create or replace package starfleet
as
shield_failure exception;
warp_core_malfunction exception;
pragma exception_init(shield_failure, -20998);
pragma exception_init(warp_core_malfunction, -20999);
procedure check_status
( status_message out varchar2 );
function status
return number;
end starfleet;
create or replace package body starfleet as
function status
return number
is
status_ind number;
begin
select 1 into status_ind from dual where 1=2; -- fails with NDF
return status_ind;
exception
when no_data_found then raise shield_failure;
end status;
procedure check_status
( status_message out varchar2 )
is
status_ind number;
begin
status_ind := status();
status_message := 'Everything is fine';
exception
when warp_core_malfunction then status_message := 'Abandon ship';
when shield_failure then status_message := 'Increase power to shields';
end check_status;
end starfleet;
Now, the status() function can raise exceptions defined in the package (or any other package for that matter), and procedure check_status can catch it and decide how to handle it.
Example of calling it from SQL*Plus:
SQL> var status_message varchar2(100)
SQL>
SQL> begin
2 starfleet.check_status(:status_message);
3 end;
4 /
PL/SQL procedure successfully completed.
STATUS_MESSAGE
-------------------------
Increase power to shields

Related

Handling exception 1) when defining Oracle function 2) When Calling Oracle function

My Oracle function below(code1) have no exception handling.
Therefore if it is called(code2) with 0, error shows.
--Code 1
CREATE OR REPLACE FUNCTION TEST2
(P1 IN VARCHAR2)
RETURN NUMBER AS V_VALUE NUMBER;
BEGIN
SELECT(
SELECT 1/TO_NUMBER(P1)
FROM DUAL
)
INTO V_VALUE
FROM DUAL;
RETURN V_VALUE;
END;
/
--Code2
SELECT TEST2('0') FROM DUAL;
Please, help to add exception handling for each 1) 2) case as below.
case 1) when defining function, how to modify code1
for function to return -1 if a system exception,
including dividing by zero happen?
case 2) Without adding exception in my Oracle funtion,
how to modify code2 for the reslut to be -1 if a system excetion, including dividing zero happens in the function?
I think you are making this more complicated than it needs to be. I did not compile this code below, but should provide as an example. Regarding exceptions, it is OK to handle divide-by-zero, but hiding all other exception types is very, very bad design. Also, if I pass in test2(-1), then the result will be a valid value of -1. Are you assured your input parameter is always positive. Regardless, here is a solution which checks for a 0 parameter, and avoids the division problem. A better solution is to define TEST1 P1 as a NUMBER to begin with, and let the caller format it as needed. If not, I could pass in something like TEST2('fsfd') and get an exception.
CREATE OR REPLACE FUNCTION TEST2(P1
IN VARCHAR2)
RETURN NUMBER;
D_Result NUMBER : = -1;
BEGIN
IF P1 <> 0 THEN
D_result := 1/TO_NUMBER(P1);
END IF;
RETURN D_Result;
END
If you really want to throw a divide error, you can catch is like this:
DECLARE
result NUMBER;
BEGIN
result := test2(0);
EXCEPTION
WHEN OTHERS THEN
result := -1;
END;
#OldProgrammer shows how to prevent the exception from occurring, which is the best choice. However, if you want to allow the exception to occur and catch it in the function you could use:
CREATE OR REPLACE FUNCTION TEST2(P1 IN VARCHAR2)
RETURN NUMBER
AS
V_VALUE NUMBER;
BEGIN
BEGIN
V_VALUE := 1 / TO_NUMBER(P1);
EXCEPTION
WHEN OTHERS THEN
V_VALUE := -1;
END;
RETURN V_VALUE;
END TEST2;
or you could simplify this to
CREATE OR REPLACE FUNCTION TEST2(P1 IN VARCHAR2)
RETURN NUMBER
AS
BEGIN
RETURN 1 / TO_NUMBER(P1);
EXCEPTION
WHEN OTHERS THEN
RETURN -1;
END TEST2;

Exception in procedure

I have procedure like below, but when block is run it does not shows message for error if data is not found.
CREATE OR REPLACE
PROCEDURE DDPROJ_SP
(P_IDPROJ IN DD_PROJECT.IDPROJ%TYPE,
P_INFO OUT VARCHAR2,
p_check OUT VARCHAR2)
IS
CURSOR cur_ddproj IS
SELECT *
FROM dd_project
WHERE idproj = p_idproj;
lv_projinfo_txt VARCHAR2(100);
BEGIN
FOR rec_ddproj IN cur_ddproj LOOP
lv_projinfo_txt := (rec_ddproj.idproj||', '||rec_ddproj.projname||
', '||rec_ddproj.projstartdate||', '||rec_ddproj. projenddate||
', '||rec_ddproj.projfundgoal||', '||rec_ddproj.p rojcoord);
END LOOP;
P_INFO := LV_PROJINFO_TXT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
P_CHECK :='Please select another project';
DBMS_OUTPUT.PUT_LINE(P_CHECK);
END;
And block:
DECLARE
LV_INFO_TXT VARCHAR2(100);
LV_CHECK_TXT VARCHAR2(30);
BEGIN
DDPROJ_SP(00,lv_info_txt,lv_check_txt);
DBMS_OUTPUT.PUT_LINE(LV_INFO_TXT);
END;
After RUNNING BLOCK IF id provided is correct I would receive requested information but if ID is not found exception will not show message on print.
Firstly, as has been pointed out, your exception handler doesn't do anything really visible except call dbms_output, the results of which you'll only see if you set serverout on or otherwise access the results from dbms_output.
Secondly and more importantly, when you use a FOR loop to process the results of a cursor, the NO_DATA_FOUND exception will never be raised.
If you want to detect no rows found, you have a few options:
After the loop, check if the variable was set, e.g.:
...
end loop;
if lv_projinfo_txt is null then
raise no_data_found;
end if;
If you don't expect more than 1 record to be found by the query (which is suggested by your predicate on an "id"), you can avoid the FOR loop and use a simple select into:
PROCEDURE DDPROJ_SP
(P_IDPROJ IN DD_PROJECT.IDPROJ%TYPE,
P_INFO OUT VARCHAR2,
p_check OUT VARCHAR2)
IS
rec_ddproj dd_project%rowtype;
lv_projinfo_txt VARCHAR2(100);
BEGIN
SELECT *
into rec_ddproj
FROM dd_project
WHERE idproj = p_idproj;
lv_projinfo_txt := (rec_ddproj.idproj||', '||rec_ddproj.projname||
', '||rec_ddproj.projstartdate||', '||rec_ddproj.projenddate||
', '||rec_ddproj.projfundgoal||', '||rec_ddproj.projcoord);
P_INFO := LV_PROJINFO_TXT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
P_CHECK :='Please select another project';
DBMS_OUTPUT.PUT_LINE(P_CHECK);
END;
Notes:
A select into may raise NO_DATA_FOUND or TOO_MANY_ROWS.
Good practice is to never handle errors without re-raising the exception, unless your code actually handles the exception. In your case, your code merely sends a signal back to the calling process via the p_check parameter, which moves responsibility for handling the error to the caller. This might be ok in some circumstances but it assumes the caller actually heeds the signal. It would be better to raise an exception which forces the caller to handle it appropriately.
Good practice is to alias all columns and parameters in a query; having a SQL predicate like idproj = p_idproj makes the assumption that the table will never have a column called p_idproj in the future. Instead, it's good practice to deliberately alias all columns and parameters, e.g.
SELECT x.*
into rec_ddproj
FROM dd_project x
WHERE x.idproj = ddproj_sp.p_idproj;

Pl/SQL Nested Procedure Exception Handling

This is a best practice question on error handling through multiple levels of PL/SQL procedures. I've looked at a few other questions to help me out, in particular this one.
Currently, I have a program with Procedure 1, which calls Procedure 2, which calls Procedure 3. I'm trying to perform adequate error handling - but I'd like to output eventually the exact problem back to the application layer. I'm hoping to get some ideas on how I can do this efficiently and clearly.
My current solution method is below, but it seems rather messy to me, with lots of variable declarations. I am very new to PL/SQL (and SQL in general) so I'd appreciate any advice on:
Good error handling techniques when dealing with multiple layers of procedures.
Feeding error messages back up to application layer (in my procedure below, represented by "out_overall_output" variable.
Program Flow: UI -> Proc 1 -> Proc 2 -> Proc 3
Procedure 1:
--One input variable, one output.
in_id VARCHAR2;
out_overall_output VARCHAR2;
...
DECLARE
l_success BOOLEAN;
l_error_output VARCHAR2(100);
BEGIN
Proc2(id, l_success, l_error_output);
IF l_success = FALSE THEN
out_overall_output = l_error_output
END IF
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
out_overall_output:= 'Error calling Proc 2'
RETURN;
END;
--Normal flow continues if l_success is true...
Procedure 2:
-- One input variable, two output.
in_id
out_success
out_error_output
//other logic
DECLARE
l_success BOOLEAN;
l_error_output VARCHAR2(100)
BEGIN
Proc3(id, l_success, l_error_output)
IF l_success = FALSE THEN
out_error_output = l_error_output
END IF
EXCEPTION
WHEN OTHERS
out_error_output = 'Error calling Proc 3'
RETURN;
END;
Procedure 3:
--One input variable, two output.
in_id VARCHAR2;
out_success BOOLEAN;
out_error_message VARCHAR2;
...
BEGIN
DELETE
FROM table
WHERE id = in_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
out_success = FALSE;
out_error_message = 'Error - No data to delete'
WHEN OTHERS THEN
out_success = FALSE;
out_error_message = 'Error deleting data.'
END;
Note: The levels of procedure calling goes deeper than this. The snippets I have shown are greatly simplified. The error messages and variable names in my real procedures are more descriptive.
To show exact explanations of "what happens with a server" for application level you can try following. In procedures:
create or replace procedure p1 is
...
exception
when <some_error> then
<do something>
-- re-raise error:
raise_application_error(-20001, 'Client with ID '|| ID || ' has no right to perform action "' || ACTION_NAME || '"', true);
end;
create or replace procedure p2 is
begin
p1;
exception
when <another_error> then
<do something>
-- re-raise error:
raise_application_error(-20002, 'Action "' || ACTION_NAME || '" is not completed', true);
end;
create or replace procedure p3 is
begin
p2;
exception
when <another_error> then
<do something>
-- re-raise error:
raise_application_error(-20003, 'Purchasing of "' || CAR_NAME || '" cancelled', true);
end;
And in top level procedure:
create or replace procedure top_level_procedure is
begin
p1;
exception
when <one_more_error> then
<do something>
raise_application_error(-20004, dbms_utility.format_error_backtrace);
end;
After exception in p1 you will see something like this:
ORA-20003: Purchasing of "Cool red Ferrari" cancelled
ORA-20002: Action "car purchase" is not completed
ORA-20001: Client with ID 123 has no right to perform action "Spent all money of Bill Gates"
Third parameter of procedure raise_application_error with false value cuts all previous error messages. If you will use false value in procedure p3, you will see only one error message with code ORA-20003 in this example.
P. S. Also you can define your own exceptions and use them in WHEN .. THEN clause. Here you find more information and examples: https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/errors.htm#LNPLS00704
P. P. S. How to log. Log procedure:
create or replace procedure log(p_log_message varchar2) is
pragma autonomous_transaction;
begin
insert into log_table(..., log_message) values (..., p_log_message);
commit;
end;
Call log procedure:
when <one_more_error> then
<do something>
log(..., dbms_utility.format_error_backtrace);
raise_application_error(-20004, dbms_utility.format_error_backtrace);

Is it a bad practice to use global exceptions in PL/SQL?

Is it a bad practice to do what the code below does? Will bad things happen to me for writing it?
Edit: this is just an example. I would not use dbms_output for any real error reporting.
CREATE OR REPLACE PACKAGE my_package
AS
PROCEDURE master;
END;
/
CREATE OR REPLACE PACKAGE BODY my_package
AS
my_global_interrupt EXCEPTION;
PROCEDURE my_private_procedure
IS
BEGIN
-- in case some flag is raised, raise exception to stop process and prepare for resume
RAISE my_global_interrupt;
END;
PROCEDURE master
IS
BEGIN
my_private_procedure;
EXCEPTION
WHEN my_global_interrupt THEN
dbms_output.put_line('global interrupt, ');
-- prepare to resume
END;
END;
/
On the contrary globally defined user exceptions is good practice. Consider the following skeleton of a package body.
create or replace package body my_pkg
as
my_x1 exception;
my_x2 exception;
my_x3 exception;
PROCEDURE p1 is
begin
...
exception
when no_data_found then raise my_x1;
end p1;
PROCEDURE p2 is
begin
...
exception
when no_data_found then raise my_x2;
end p2;
PROCEDURE p3 is
begin
...
exception
when no_data_found then raise my_x3;
end p3;
PROCEDURE master is
begin
p1;
p2;
p3;
exception
when my_x1 then do_this;
when my_x2 then do_that;
when my_x3 then do_the_other;
end master;
end my_pkg;
/
The use of globally declared exceptions makes exception handling in the master procedure easier.
Also, bear in mind that sometimes we want to propagate the exception beyond the package, to say a program which calls our publicly declared procedure. We can do that by defining our exceptions in the package spec. This means other proecdures can reference them...
SQL> begin
2 my_pkg.master;
3 exception
4 when my_pkg.my_public_x1
5 then dbms_output.put_line('oh no!');
6 end;
7 /
oh no!
PL/SQL procedure successfully completed.
SQL>
We can also associate such exceptions with specific error numbers, so that they are recognisable even if the calling procedure doesn't explicitly handled them.
SQL> exec my_pkg.master
BEGIN my_pkg.master; END;
*
ERROR at line 1:
ORA-20999:
ORA-06512: at "APC.MY_PKG", line 32
ORA-06512: at line 1
SQL>
That's (slightly) more helpful than the generic ORA-06510 error.
Looks reasonable enough to me, provided you're happy that after the interrupt condition it's OK to resume processing. If you are going to log the interrupt in some way, it's probably better to insert a row into a log table using an autonomous transaction. You won't see anything from DBMS_OUTPUT until the whole procedure finishes. Then you'll see all the DBMS_OUTPUT at once.

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