I am working on a total rewrite of a PL/SQL program which is an EBS inbound interface to a third party system. In this process we receive input from the 3rd party system which is loaded into a table, and the program loops through the table to call EBS APIs for modifying and creating records. The business logic is very complicated and the code has spiraled out of control which is the reason for the rewrite.
Before calling some of the EBS APIs, we need to validate the incoming data against what is in the Oracle EBS system. The current code has several (20+) procedures to check various pieces of incoming data. Each of these procedures returns a status and a message. Then there is one main procedure which calls all of these validation procedures, and after each procedure call it looks at the status and message - the main procedure is basically a sea of if-else statements and it is extremely cumbersome to follow.
I'm wondering if there's a more suitable practice for performing this quantity of validations so that the code is easier to read and maintain. I have considered making all of the procedures into functions that return boolean values but then I still have the problem of tons of if-else statements in the main calling procedure. I'm just looking for other ideas at this point, the ultimate goal is to improve maintainability of this program.
Thanks in advance
You can't escape the fact that if you have 20 things you have to evaluate you need to have 20 check points in a one way or another.
Your problem statement is very broad but below is one example how I have implemented the same pattern succesfully. The main idea is that the checks (or validation) do not pollute that main business logic but are clearly separated so the main business logic is easy to follow. PL/SQL is sometimes a bit verbose language - here the verbosity goes into exception handling.
Note that in the example I have made some assumptions that might not hold in you specific case.
Validation package:
--
-- NOTE: PL/SQL look alike pseudo code - won't compile
--
-- br = business rule
create or replace package body so46_br is
-- different exceptions required only if each case needs to be identified
-- later in exception handling
err_br_1 constant pls_integer := -20001;
ex_br_1 exception;
pragma exception_init(ex_br_1, -20001);
-- ...
err_br_20 constant pls_integer := -20020;
ex_br_20 exception;
pragma exception_init(ex_br_1, -20020);
procedure assert_br_1(/* input params */) is
v_fail boolean := false;
v_msg varchar2(2000);
begin
-- validate the business rule #1
if v_fail then
v_msg := 'Construct detailed reason why the validation failed.';
raise_application_error(err_br_1, v_msg, true);
end if;
end;
-- ...
procedure assert_br_20(/* input params */) is
v_fail boolean := false;
v_msg varchar2(2000);
begin
-- validate the business rule #20
if v_fail then
v_msg := 'Construct detailed reason why the validation failed.';
raise_application_error(err_br_20, v_msg, true);
end if;
end;
end;
Business logic package:
--
-- NOTE: PL/SQL look alike pseudo code - won't compile
--
-- bl = business logic
create or replace package body so46_bl is
procedure main(/* input params */) is
begin
-- #1 validate input params/business rules
-- assumes we can quit if any validation fails
so46_br.assert_br_1(/* input params */);
-- ...
so46_br.assert_br_20(/* input params */);
-- #2 do the other business logic things
exception
-- assumes each validation failure needs to be identifiable
when so46_br.ex_br_1 then
so46_log.log_br_1(dbms_utility.format_error_stack ||
dbms_utility.format_error_backtrace);
raise;
-- ...
when so46_br.ex_br_20 then
so46_log.log_br_20(dbms_utility.format_error_stack ||
dbms_utility.format_error_backtrace);
raise;
end;
end;
Related
The good (and bad) old Delphi taught us the "classic" way of building application because of the way we write code "behind" the IDE.
Based on this paradigm, I built some time ago a library that allows me to save/load the GUI to INI file with a single line of code.
LoadForm(FormName)
BAM! That's it! No more INI files!
The library only saves "relevant" properties. For example, for a TCheckBox it saves only its Checked property not also its Color or Top/Left. But it is more than enough.
Saving the form has no issues. However, I have "problems" during app initialization. They are not quite problems, but the initialization code is not that nice/elegant.
For example when I assign Checkbox1.Checked := True it will trigger the OnClick event (supposing that the checkbox was unchecked at design time). But assigning a False value (naturally) will not trigger the OnClick event.
Therefore, I have to manually call CheckBox1Click(Sender) after SaveForm(FormName) to make sure that whatever code is in CheckBox1Click, it gets initialized. But this raises another problem. The CheckBox1Click(Sender) might be called twice during application start up (once by SaveForm and once my me, manually).
Of course, in theory the logic of the program should be put in individual objects and separated from the GUI. But even if we do this, the initialization problem remains. You load the properties of the object from disk and you assign them to the GUI. When you set whatever value you have in your object to Checkbox1, it will (or not) call CheckBox1Click(Sender) which will set the value back into the object.
On app startup:
procedure TForm.FormCreate (Sender: TObject);
begin
LogicObject.Load(File); // load app logic
Checkbox1.Checked := LogicObject.Checked; // assign object to GUI
end;
procedure TForm.CheckBox1Click(Sender: TObject);
begin
LogicObject.Checked := Checkbox1.Checked;
end;
Probably the solution involves writing stuff like this for EVERY control on the form:
OldEvent := CheckBox1.OnClick;
CheckBox1.OnClick := Nil;
CheckBox1.Checked := something;
CheckBox1.OnClick := OldEvent;
Not elegant.
Question:
How do you solve this specific problem OR what is your approach when saving/restoring your GUI to/from disk?
This is one of the things which botthered me in some components from the beginning. What I know the are 3 options, except separating GUI and the business logic as #David said, which is not always an option.
As you wrote above, always unassign the events so they don't get triggered
Use non-triggered events such as OnMouseDown or OnMouseUp
Or a similar solution that I use and I think is the most elegant
Create a global variable FormPreparing which is set during initialization and check its value at the beginning of the events like below.
procedure TForm.FormCreate (Sender: TObject);
begin
FormPreparing := True;
try
LogicObject.Load(File); // load app logic
Checkbox1.Checked := LogicObject.Checked; // assign object to GUI
finally
FormPreparing := False;
end;
end;
procedure TForm.CheckBox1Click(Sender: TObject);
begin
if FormPreparing then
Exit;
LogicObject.Checked := Checkbox1.Checked;
end;
Suppose I have a procedure as follows:
PROCEDURE proc_name (args)
IS
-- declarations
...
BEGIN
-- code
...
EXCEPTION
WHEN an_error THEN
--error handling code
....
WHEN another_error THEN
-- error handling code, identical to the one for an_error
...
WHEN others THEN
---generic error handling code`
....
END;
Ideally, I would like to be able to catch both an_error and another_error in the same WHEN clause, since their handling is identical.
Is this possible in Oracle? If not, what are other possibilities to avoid code duplication?
Yes you can.
You can use OR conditions between the exceptions so
EXCEPTION
WHEN an_exception
OR another_exception
THEN
handle it here;
END;
See The Docs for more details on exception handling.
Yes.
But not for OTHERS exception. It can not be merged with another exception.
My package header code looks like this:
CREATE OR REPLACE PACKAGE INST_PKG IS
...
FUNCTION Check_View (
view_name_ IN VARCHAR2 ) RETURN BOOLEAN;
PRAGMA restrict_references (Check_View, WNDS);
...
END INST_PKG;
And the body of the function is defined as follows:
CREATE OR REPLACE PACKAGE BODY INST_PKG IS
....
FUNCTION View_Exist (
view_name_ IN VARCHAR2 ) RETURN BOOLEAN
IS
ck_ NUMBER := 0;
CURSOR check_view IS
SELECT 1
FROM user_views uv
WHERE uv.view_name = upper(view_name_);
BEGIN
OPEN check_view;
FETCH check_view INTO ck_;
IF check_view%FOUND THEN
CLOSE check_view;
RETURN true;
ELSE
CLOSE check_view;
RETURN false;
END IF;
END View_Exist;
....
END INST_PKG;
I get an error message which reads as follows, when I try to compile the package body:
Compilation errors for PACKAGE BODY INST_PKG
Error: PLS-00452: Subprogram 'VIEW_EXIST' violates its associated pragma
Line: 684
Text: FUNCTION View_Exist (
Clearly, my pragma of "Write No Database State" is not violated, as there are no DML statements in the function. Has anyone seen such behaviour before?
Of course, I could drop the Pragma reference, but that would kind of defeat the purpose.
Worthy of note:
My database has been exported from an Oracle 10g instance, and has been re-imported to 12c. (This is an upgrade test, as you might imagine). Hence I get the above error on Oracle 12c.
I have tried to drop and re-create the package, but that doesn't seem to change things.
I have a feeling that there may be a library reference somewhere that has been imported in error, because when I drop the package, the same error comes up in another package, which contains a function of the same name. But when I re-create the INST_PKG, the second package compiles fine, almost as though the problem in the first package is masking it from being flagged in the second.
It emerges from the link you showed, that the issue is a result of a bug in USER_VIEWS (Oracle forgot to associate PRAGMA restrict_references with NO_ROOT_SW_FOR_LOCAL).
In this case you can be certain that your function doesn't violate WNDS assertion (doesn't write to the database), therefore just use TRUST option to disable assertions validation during compilation:
PRAGMA restrict_references (Check_View, WNDS, TRUST);
Thanks for pointing me in the right direction #MuhammadMuzzam. The problem is user_views has some references which apparently violate Pragma in Oracle 12.
Seems to be an known issue:
https://community.oracle.com/thread/3650610?start=0&tstart=0
There are parameterized error messages in Oracle database. For example, there is 01919, 00000, "role '%s' does not exist" in oraus.msg.
If one issue some nonsense GRANT ... TO ... %s is substituted by this nonexistent privilege.
It is possible to raise exception -1919 and supply some string to %s?
Code:
not_system_privilege EXCEPTION;
PRAGMA EXCEPTION_INIT(not_system_privilege, -01919);
.......
RAISE not_system_privilege;
produces only ORA-01919: role '' does not exist message.
The purpose of user-defined exceptions is that we can trap specific exceptions in the exception section of our PL/SQL program and handle them elegantly. For instance, if we put some flesh around your code snippet....
create or replace grant_priv
( p_priv in varchar2
, p_grantee in varchar2 )
is
not_system_privilege EXCEPTION;
PRAGMA EXCEPTION_INIT(not_system_privilege, -01919);
begin
execute immediate 'grant '||p_priv||' to '||p_grantee;
exception
when not_system_privilege then
raise_application_error(-20000, p_priv||' is not a real privilege', true);
when others then
raise;
end;
We can put anything in the EXCEPTIONS section. Log the error in a table or file, raise alerts, whatever. It is good practice to propagate the exception upwards: only the toppermost layer of the callstack - the user-facing layer - shouldn't hurl exceptions.
An observation - it looks like you can use utl_lms.format_message for C-style printing - wish I'd known this earlier (as would have saved writing it). Seems to be Ora10 and above only.
begin
dbms_output.put_line(
utl_lms.format_message(
'A %s is here and a %s is there and a %s too','Giraffe','Lion','Spider'));
end;
I can't see any way to meet the OPs requirement - to RAISE a system-level exception and substitute in the right parameter.
However, if you can live with using a different exception number, you could write your own exception handling procedure that could
a) take in the serial of the required exception
b) use utl_lms.get_message to retrieve the text
c) use format_message to substitute in the parameters
d) raise a user defined exception using the generated text
The problem is that this would not work if your calling system expects an ORA-01919.
I want to know about error handling in PL/SQL. Can anyone help me to find brief description on this topic?
Every block can have an exception handler. Example:
DECLARE
/* declare your variables */
BEGIN
/*Here is your code */
EXCEPTION
WHEN NO_DATA_FOUND THEN
/* HAndle an error that gets raised when a query returns nothing */
WHEN TOO_MANY_ROWS THEN
/* HAndle the situation when too much data is returned such as with a select-into */
WHEN OTHERS THEN
/* Handle everything else*/
END;
This link will tell you more: http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10807/07_errs.htm
That link will show you more detail than I did, as well as examples on how to create your own exception names.
One item that always trips me up is that if you have a function and you fail to return a value in the exception handler, an exception gets thrown in the calling function. Not a big deal but I always seem to forget that one.
The Oracle article referenced in the other answer is well worth reading.
A couple of extra things to throw in - catching a PL/SQL exception loses the error stack - i.e. the information about exactly which line raised the exception.
This can make it difficult to debug blocks of code that contain multiple places that could raise the same exception (i.e. if you have more than one SQL statement that could return NO_DATA_FOUND). One option here is to log the full error stack as part of your exception handler.
EXCEPTION
WHEN TOO_MANY_ROWS THEN
myLogger('Some useful information',DBMS_UTILITY.FORMAT_ERROR_STACK);
END;
If you do need to catch exceptions, keep your exception handling as local as possible to the point you want to catch, and only use WHEN OTHERS in the last resort.
You can also 'do something and re-raise the same exception'
EXCEPTION
WHEN TOO_MANY_ROWS THEN
closeSmtpConnection;
RAISE;
END;
One of the most useful features is the ability to name and catch Oracle SQL internal exceptions.
DECLARE
recompile_failed EXCEPTION;
PRAGMA EXCEPTION_INIT (recompile_failed,-24344);
BEGIN
. . . . . .
EXCEPTION
WHEN recompile_failed THEN
emailErrors(pObjectType,pObjectName);
END;
The flipside to this is the ability to raise user defined 'SQL' exceptions
RAISE_APPLICATION_ERROR(-20001,'my text')
This is the only way to propagate user defined text to a calling application, as user-defined pl/sql exceptions do not cross the 'scope' boundary.
Unfortunately, despite the documentation saying that the range -20000 to -20999 is available for user-defined exceptions, some of the Oracle extension packages use these serials, so you cannot depend on serial alone to identify an error in the calling language.
(Most people tend to wrap RAISE_APPLICATION_ERROR in other code to also log the error, and often to derive the error text from a table)
One trick I've found useful is to create a package with 'stateful' variables in the package body, and simple setter and getter functions. Unlike database updates, information in packages is NOT rolled back on error.
At the point of error, set information in your package, then retrieve it using getters in your calling language, to construct a 'native' exception.
As for user-defined pl/sql exceptions - these can be useful in local code, but in many cases they can be avoided by using a different control structure (i.e. avoid using them as an alternative GOTO).
Creating global exceptions on package headers, to specify the possible exceptions a package may return seems like a good idea, but the end result is that your calling code ends up with having to handle every potential exception that could be cast in any of the underlying packages.
Having gone down this route myself in the past, I would now recommend against it - make packages self-contained and either use RAISE_APPLICATION_ERROR or pass back errors as text.