Академический Документы
Профессиональный Документы
Культура Документы
WrittenbyJerryMattsson
This document was written some time ago but have not been published
anywhere, so if anyone can use it, that is fine. I revised it and put some new
comments in here and there. The contents is not version specific to the
database but covers only PL/SQL version 2.
The purpose of this document is to serve as a practical guide or as a standard
for programmers and system designers, writing, generating or specifying
program modules that access an Oracle Rdbms. The conventions stated here
should be applied as far as possible regardless of which tool or environment
that are used to create or execute PL/SQL code.
This document may be included in your system design guidelines or standards,
as long as it is included as is, or the reference is clearly stated.
Your own addition could be placed in an addendum or a preface, stating which
parts that are used, changes to the parts used, or the parts that you want to
exclude from your design guide.
Suggestions and questions are welcomed, e-mail:
jerry.mattsson@timehole.com.
ORACLE, Oracle*Forms. SQL* Forms, SQL*Plus, and SQL*ReportWriter are
registered trademarks, and CASE*Generator, and PL/SQL are trademarks of
Oracle Corporation.
Reformatted Dec-2002. I hope it looks a little bit better.
Contents:
• 1 General about PL/SQL
• 2. Naming Conventions
• 3.1.2 Layout
• 3.2.1 General
• 3.2.4 Locking
• 3.2.5 Security
• 3.3 Triggers
• 3.4.1 General
• 4. Utility packages
SQL is a set oriented language and has very limited procedural capabilities even
if a lot of tasks may be and should be solved using pure SQL. A lot of things are
possible to achieve with SQL, but we are more procedural in our thinking, than
set oriented. When things gets a little complex, breaking things down in
procedural code often gives us a better understanding ( and code ) than thinking
in terms of sets and set functions. If a problem is solved using both approaches,
the SQL solution is often more efficient than the procedural equivalent.
Do not try to solve your design problems with simple SQL-code and a lot of
procedural code, try to solve it the other way around. In most cases this will give
better performance, a more stable, more secure system and have a better code
structure.
Programs that access an Oracle database must use SQL. This can be done in a
lot of ways, with 3GL-languages like C/C++, Fortran, COBOL. It can also be
done using interfaces like ODBC or JDBC, by using a number of languages that
can call these database access methods, or it may be done through a tool
written in any language using one of these interfaces.
2. Naming Conventions
<Up to Contents>
<Up to Contents>
GetTax, CalcNetIncome
A program that wanted to call one of there procedures should then call:
CUST023.GetTax(parameters);
<Up to Contents>
/*
Title/File: CUST0012.PLS
Version: 1.0
Description: This program is intended to be used for .......
Author: Jerry Mattsson / Xyz Ab.
Creation date: 2/5 -97
*/
/*
Modification information, added at time of modification
Modified date:
Modification purpose:
Modified by:
*/
or in ADA style:
-- Title/File: CUST0012.PLS
-- Version: 1.0
-- Description: This program is intended to be used for .......
-- Author: Jerry Mattsson / Another XYZ company
-- Create date: 2/5 -97
When generating or regenerating code with Oracle's code generators, the code
will be documented in a similar way. It will use the text information entered in the
module text windows, Description, Notes and Modification History. Be sure to
maintain this text information in the case tool before generating new code or
when reverse engineering code.
<Up to Contents>
3.1.1 General
PL/SQL should be written in a structured way and follow a general coding
practice. Even if there exists a "GOTO" statement, these should not be used.
Comment and explanation of the code should be written, write the purpose of the
code, not how you've written it.
Calls to procedures should always be made with bind variables and not with
constants or strings.
E.g.
l_code := 'TST';
l_result := CUST.ChkCode(l_code);
<Up to Contents>
3.1.2 Layout
PL/SQL code should have a layout so that it is easy to read. Code should be
grouped logically and you can separate different parts from one other by leaving
a blank line, or by inserting comments.
SQL and PL/SQL code should in general be written in lower case, except for
keywords in SQL and PL/SQL.
PL/SQL keywords like:
DECLARE
x NUMBER := 0;
counter NUMBER := 0;
BEGIN
FOR i IN 1..4 LOOP
x := x + 1000;
counter := counter + 1;
INSERT into temp ( col1, col2, col3 )
VALUES (x, counter, 'in OUTER loop');
DECLARE
l_custno customer.custno%TYPE := 0;
BEGIN
or
DECLARE
/* Read table valid_number in descending order */
CURSOR cr_vnum1 IS
SELECT n1.next_number
FROM valid_numbers n1
ORDER BY n1.seq desc;
/* Read table valid_numbers in ascending order */
CURSOR cr_vnum2 IS
SELECT n2.next_number
FROM valid_numbers n2
ORDER BY n2.seq asc;
l_num1 valid_numbers.next_number%TYPE;
l_num2 valid_numbers.next_number%TYPE;
BEGIN
or
DECLARE
CURSOR cr_emp is
SELECT * FROM emp;
r_emp emp%ROWTYPE;
r_dept dept%ROWTYPE;
BEGIN
IF r_emp.comm is null THEN
/* do something smart with ............*/
END IF;
END;
<Up to Contents>
Implicit cursors:
SELECT ename
INTO l_ename
FROM emp;
Explicit cursor:
3.2.1 General
A procedure is a program made of PL/SQL commands and SQL statements that
can be executed by the database. It may have one or more input and output
variables.
A function is identical to the procedure except that it always have one return
value returned to the calling program.
If any of the underlying objects ( called procedures, tables etc. ) has changed,
the procedure will be marked invalid and it will be recompiled before execution.
• Performance.
-- Usage/syntax: SetAppRole(ApplicationName);
-- Sets the role for a specific application program. Returns true if
success.
function SetAppRole(p_AppName varchar2) returns boolean;
END;
The body of the package should be commented for each procedure or
function with create- and modification information, as well as descriptions
and notes about the code.
The package size should therefore ideally be less than a couple of hundred
rows.
Grouping of functions should be done in a way that loads the least amount of
code at execution time. The more general and common functions you are able to
use, the more probable it is that you save shared pool memory, and that you
reuse procedures already cached in memory. To achieve this a great deal of
work has to be done, modularizing your functions and programs. To create small
support packages with utility like procedures is probably a good way to cause
reuse of code, used by many users.
<Up to Contents>
E.g.
BEGIN
Savepoint before_UPDATE;
UPDATE emp SET sal = sal + 10;
DELETE FROM comm
WHERE empno in
( SELECT empno FROM emp WHERE comm is null );
EXCEPTION when NO_DATA_FOUND THEN
rollback to before_UPDATE;
END;
<Up to Contents>
CURSOR cr_usrinfo1 is
SELECT user, sessionid, uid, sysdate
FROM sys.dual;
3.2.4 Locking.
All code that performs manipulates data ( update and delete functions ) must
explicitly be coded to handle locking conflicts. How this is handled should be
documented in the package specification or in the procedure description.
This is a more complete example to give you coding suggestions about locking
functions and a little bit more.
CREATE OR REPLACE PROCEDURE SetGrooveWeight
(p_lb_weight IN grooves.weight%TYPE) IS
/* This procedure reads the three tables stand, roll_sets and grooves,
takes only the used roll_sets and updates the usage sum
of........
If the master record in stand is possible to lock,
we wait if necessary on the child record.......
the groove is updated via the rowid, fetched in .....*/
l_sum_weight NUMBER(7,1);
l_event NUMBER(1,0);
RowLocked exception;
pragma EXCEPTION_INIT(RowLocked, -0054);
BEGIN
FOR r_stand IN cu_stand LOOP
l_event := 0;
-- Test if stand exist and groove is used
IF r_stand.id is not null and r_stand.active_groove <> 0 THEN
l_sum_weight := 0; --set acc to zero
-- loop for each master roll
set and, check all grooves
FOR r_groove IN cu_groove (r_stand.id) LOOP
IF r_groove.id = r_stand.active_groove THEN
IF r_groove.limit > r_groove.weight
AND
r_groove.limit < (r_groove.weight + p_lb_weight) THEN
l_event := 1;
END IF;
l_sum_weight := l_sum_weight + p_lb_weight;
-- set weight on the groove with in parameter value
UPDATE grooves g
SET g.weight = (g.weight + p_lb_weight)
WHERE current of cu_groove; -- using CURSOR
END IF;
l_sum_weight := l_sum_weight + r_groove.weight;
END LOOP;
3.2.5 Security.
Stored procedures and triggers always execute in the owners schema The user
who has execute rights on a package, gets all the rights that is needed, and was
used by the creator, when the procedure executes. The user does not have to
have any rights to the tables and views that are read or updated in the
procedure, only execution right on the procedure or package.
<Up to Contents>
Input/output operations are much easier to handle in the front-end tool where you
can open and communicate with files and terminals in different ways, and this is
normally exactly what this tools and programs are made for.
While developing and debugging stored procedures, you do want to do some i/o
to trace and understand the behavior of the code. Use a front end tool like the
"Oracle procedure builder" that contains functions to help you manage and trace
your code. Oracle's tools are currently equipped with PL/SQL version 1 and the
database is using version 2, and this might cause some problems but it is still
useful for it's purpose.
To get information on what happens inside the server or to trace the code
execution you may:
Remember that if you write information to a table, this writing is a part of a your
transaction and might be rolled back or committed dependent on the program
code.
The database is delivered with a number of system packages. One of them is the
DBMS_OUTPUT package. This package contains procedures to send strings to
some front-end tools SQLPLUS ( or old Sqldba/Svrmgr ). This allows you to
"print" messages from within a procedure and this function can therefore be used
to debug the code when it is executing in the kernel. One effect using this type of
function is that the output buffer used to send information is not flushed just when
you write to the output function but when the database has terminated an internal
database operation and this often means that the output comes after your
procedure has terminated.
So limit the use of this package to simple trace-functions and should be the only
type of usage of this package. It should not normally be used in the application
code and make the execution dependent on this because of it's limited
functionality.
dbms_ouput.putline('CUST00022.GetUser '||to_char(user_number));
The code could be left for future trace of the execution. It does not have any
impact on the procedure if the "serveroutput" function is not active. But if used
general in your code you must include the procedure name in the text line, so
that it is possible to identify the function writing the message to you. When
enabling the output you enable all messages in all routines, you cannot enable
these only for some selected procedures.
in the SQLPLUS environment. The messages will then be buffered and sent to
your front-end session from the database when the code is executing. It is not
possible to receive this data in any other tool.
A trigger may also cause extensive i/o if the trigger contains SQL-statements
that access one or more tables when executed. The trigger is supposed to
execute for each row in a table that it is applicable for, and if we have a 1000
row table with a trigger that reads and checks data in one other table on an
update-statement. This will case 1000 reads in the table, when updating all
rows in the trigger table, hopefully very efficient reads and not full table
scans.
The safest types of triggers, in the aspect of performance, are triggers that
only:
a) verifies data on the record you are manipulating or
b) triggers that make computations or supplies other derived values to the
same record.
The example below uses another utility-package that sends alerts ( signal
changes ), that occurred in the database to front-end applications.
Alerts are transaction dependent, and is sent at the time a commit is done in
the database. Waiting for an alert holds ( locks ) a server-process to the user
while or because it is waiting. Use dbms_alerts with care.
To avoid multiple signaling, use code like the MU001 and MU002 programs
below. It handles the signaling process from a triggers. It uses a table to be
able to define the relations, between different applications and the signals
that they should listen to.
-- The trigger signals the events with Signal, and supplies the event
name.
procedure SigRegAll (ProgName in varchar2);
procedure Signal (EventName in varchar2);
-- Global variables
l_SigName varchar2(32);
l_message varchar2(100);
l_status number(2) := 1;
l_time number(2) := 3;
END;
Then we have an example of a triggers using the defined package. We need the
row trigger to send the signal and to accumulate the signal names in the table,
and we need an after statement trigger to reset the table after the statement is
done and the transaction is performed.
procedure SigDelAll is
BEGIN
-- replaces the dbms_alert standard function, deletes all alerts a
program has registered
dbms_alert.removeall;
END SigDelAll;
Procedure SigClean is
BEGIN
t_Signals := tab_Empty;
g_MaxSignals := 0;
END SigClean;
END;
<Up to Contents>
3.4.1 General
There are different types of exceptions or errors that we can identify:
• Predefined PL/SQL exceptions.
You should always declare exceptions for errors that you consider as "possible or
normal errors". This is errors that the procedures are supposed to handle or act
upon, and the program should therefore take some actions according to it.
The remaining errors are considered as abnormal and the error condition should
ripple out to the driving program. The driving or controlling program should then
take actions or report the problem to the user, the user program or to a log.
PL/SQL used in stored procedures may NEVER contain exception handling like:
Think of what will happen if the RDBMS returns ORA-00942, table or view does
not exist, or some other serious error, and your program ignores that.
If
... WHEN OTHERS....
is used it must at least contain
.... raise_application_error(Some_error_code, 'And Some message'),
and this should then be propagated to the correct program level for action.
<Up to Contents>
TOO_MANY_ROWS, CURSOR_ALREADY_OPEN,
VALUE_ERROR, INVALID_CURSOR,
NO_DATA_FOUND, INVALID_NUMBER,
DUP_VAL_ON_INDEX, LOGON_DENIED,
NOT_LOGGED_ON, PROGRAM_ERROR,
TIMEOUT_ON_RESOURCE, TRANSACTION_BACKED_OUT,
ZERO_DIVIDE
This is errors that may occur in most procedures and the consequences of these
should always be considered. The exceptions TOO_MANY_ROWS and
NO_DATA_FOUND will not occur if all SELECT-statements are declared as
explicit cursors.
<Up to Contents>
NOT_EXIST_OR_NO_PRIV exception;
pragma EXCEPTION_INIT(NOT_EXIST_OR_NO_PRIV, -6564);
PROC_NOT_IN_PKG exception;
pragma EXCEPTION_INIT(PROC_NOT_IN_PKG, -6563);
BEGIN
BEGIN
dbms_utility.name_resolve(object_name, 1,sch, part1, part2, db,
typ, objno);
EXCEPTION
when not_exist_or_no_priv or proc_not_in_pkg THEN
raise;
WHEN OTHERS THEN
raise_application_error(-20004,'syntax error attempting to parse
"'||
object_name ||'"', keeperrorstack => TRUE);
END;
-- if the translated object is local THEN query the local dictionary
IF (part1 is not null and part2 is null) THEN
raise_application_error(-20000,'ORU-10035: cannot describe a
package ('
|| object_name ||'); only a procedure within a package');
END IF;
END;
END;
<Up to Contents>
E.g. transactions that are very large, can result in storage errors or snapshot
errors and are therefore necessary to declare in this type of transaction program.
Pragma Exception_init(Old_Snapshot,-1555);
...
EXCEPTION
when Old_Snapshot THEN ....
Delete_cust_procedure ....
DELETE FROM customer
WHERE custno = l_custno;
IF sql%notfound THEN
raise_application_error(-20201,'XYZ:Customer does not exist.');
END IF;
The calling procedure ....
DECLARE
No_cust_Exist EXCEPTION;
pragma exception_init(No_cust_exist, -20201);
l_custno number(9);
BEGIN
....
Delete_Cust_procedure( l_custno );
....
EXCEPTION
when No_cust_Exist THEN ....
END;
The Raise_Application_Error call will terminate the procedure where it is raised,
rollback the statement and return the error number to the calling procedure or
program.
DECLARE
No_More_Capacity exception;
pragma ...
BEGIN
IF rec.capacity > l_MaxCapacity THEN
raise NO_More_Capacity;
...
EXCEPTION
when No_More_Capacity THEN ....
...
END;
<Up to Contents>
We should therefore decide and agree upon a specific and common set of error
codes. The range of these should be -20100 and upwards.
Application specific errors used in a program structure could be declared in the
range -20200 - 20299.
E.g.
raise_application_error(CE001.ERRH01,'CreateRange','TOO_MUCH');
END;
<Up to Contents>
Severity number(2,0)
Application varchar2(12)
Action varchar2(12)
<Up to Contents>
BEGIN
DBMS_OUTPUT.PUT_LINE('CEU002.ErrH: Code = ' || errcode ||''||
errtxt);
IF errcode is null THEN
IF errtxt is null THEN -- We got nothing, no code, no text!
raise_application_error(-20999,l_errtxt );
END IF;
END IF;
-- This is Oracle errors, catch all special error codes
--
-- Parent key not found
IF l_errno = -2291 Then
l_cname := GetCnstrName ( errtxt in varchar2 ); -- Get
Constr.name
IF l_cname is not null THEN
l_errtxt := GetErrm(l_cname);
END IF;
END IF;
-- End of specially treated error codes
-- Error code is a character string
l_errtxt := GetErrm(errcode);
IF l_errtxt is null and errtxt is null
and l_errno in not null THEN -- get the error
l_errtxt := GetErrm(l_errno);
END IF;
IF l_errno < -20000 and l_errno > -20999 THEN
raise_application_error(l_errno,l_errtxt);
-- User defined error
END IF;
raise_application_error(-20999,l_errtxt);
END ErrH;
4. Utility packages
There are a number of utility packages. The dbms_pipe package contains
routines to send and receive messages with database functions similar to the
Unix pipe functions. This allows sessions to pass information between them if
connected to the same database instance.
Oracle pipes have a number o special properties, here are some:
• Pipes operate independently of transactions.
• Pipes are created the first time they are referenced, and they
disappear when they does not contain any more data.
CURSOR c1 is
SELECT nvl(max(qid),0) + 1
FROM mmsqueue;
BEGIN
l_prc := 'MS101,GQReq: '; -- Message wait, default timeout is
approx. 3 years.
l_PipeStat := dbms_pipe.receive_message(PipeName);
/* Message has arrived */
-------------------------------------------------------------
IF l_PipeStat <> 0 THEN
MsgStr := 'MS101, Pipe receive error: '||to_char(l_PipeStat);
IF l_PipeStat = 3 THEN
MsgStr := MsgStr || ' - Process interrupted';
END IF;
IF l_PipeStat = 2 THEN
MsgStr := MsgStr || ' - Wait request timed out';
END IF;
raise_application_error(-20100,MsgStr);
END IF;
-- - - Wait for message - - --
dbms_pipe.unpack_message(l_PackType);
-- - - Start processing - - --
IF upper(l_PackType) not in ( 'MSREQ' , 'CMD' ) THEN
MsgStr := ' MS101: Wrong type of package received!';
raise_application_error(-20100,MsgStr);
END IF;
--------------------------------------------------------------------
IF upper(l_PackType) = 'CMD' THEN
dbms_pipe.unpack_message(l_cmd);
dbms_pipe.unpack_message(MsgStr);
dbms_output.put_line('-- Received Command package --');
dbms_output.put_line('Issued by '||MsgStr);
IF l_cmd = 'STOP' THEN
raise_application_error(-20109,Msgstr);
END IF;
IF l_cmd = 'PURGE' THEN
dbms_pipe.purge(PipeName);
END IF;
ELSE
dbms_output.put_line('-- Received MSREQ package --');
-- MSREQ packs 6 elements in the package
dbms_pipe.unpack_message(l_PgmId);
dbms_pipe.unpack_message(l_Pgmtype);
dbms_pipe.unpack_message(l_username);
dbms_pipe.unpack_message(l_time);
dbms_pipe.unpack_message(l_PgmParams);
p_PgmId := l_PgmId;
p_Pgmtype := l_Pgmtype;
p_PgmParams := l_PgmParams;
p_username := l_username;
OPEN c_usr1;
FETCH c_usr1 into l_username, l_time;
close c_usr1;
l_PackType := 'MSREQ';
dbms_pipe.reset_buffer;
dbms_pipe.pack_message(l_PackType);
dbms_pipe.pack_message(upper(p_PgmId));
dbms_pipe.pack_message(upper(p_Pgmtype));
dbms_pipe.pack_message(l_username);
dbms_pipe.pack_message(l_time);
dbms_pipe.pack_message(p_Params);
BEGIN
l_prc := 'MS101,Qcmd: ';
OPEN c1;
FETCH c1 into l_str;
close c1;
l_PackType := 'CMD';
dbms_pipe.pack_message(l_PackType);
dbms_pipe.pack_message(l_cmd);
dbms_pipe.pack_message(l_str);
BEGIN
ms101.Gqreq( :v_id, :v_usr, :v_pgm, :v_type, :v_dest, :v_params,
:v_time );
END;
/
rem the procedure stores the server command in a table but it is the
controlling
rem program that actually commits this when received.
commit;