Professional Documents
Culture Documents
A Modified
Perspective on Error
Handling in PL/SQL
Steven Feuerstein
You can’t write a high-quality application without building a robust error handling
and reporting architecture. Steven Feuerstein has been thinking about this
challenge for years and has come to some surprising conclusions: Hard-coded
error names are good, and RAISE_APPLICATION_ERROR is a waste of time. In this
article, he explains his thinking and offers some new ideas for error handling in
PL/SQL applications.
September 2004
I
’VE long preached (and, yes, I can be preachy) about the importance of
Volume 11, Number 9
consistent error handling, based on a common infrastructure package. I
suggest a variety of best practices to go with this high-level suggestion,
1 A Modified Perspective on
including but not limited to: Error Handling in PL/SQL
• Don’t call RAISE_APPLICATION_ERROR directly. Steven Feuerstein
• Don’t hard-code error numbers, particularly those in the -20,NNN error
range, in your code. 8 Accessing java.util.zip from
PL/SQL: The GZIP Classes
• Provide simple ways for developers to handle errors that hide the Gary Menchen
implementation of your error log and enforce consistency.
12 Attribute Denormalization
Check out my Oracle PL/SQL Best Practices book (www.oreilly.com/ Peter Hamilton
catalog/orbestprac) for a more complete treatment of my exception handling 16 September 2004 Downloads
best practices.
I also implemented a package that satisfies these best practices in
PL/Vision, a library of reusable code now available as freeware from
Quest Software (http://quest-pipelines.com/pipelines/dba/PLVision/
plvision.htm). For example, suppose I had to write a trigger to ensure that Indicates accompanying files are available online at
www.pinnaclepublishing.com.
employees are “not too young.” This is, of course, an application-specific
error, so many of us are likely to write code that looks like think. They were right; both approaches have hard-
this in the trigger: coding. A change in the name (to make it more accurate,
to reflect changed meaning, and so on) would require a
IF :NEW.birthdate > ADD_MONTHS (SYSDATE, -1 * 18 * 12)
THEN
change in code with either approach.
RAISE_APPLICATION_ERROR After more discussion, we ended up agreeing on the
(-20070, 'Employee "' || :NEW.last_name
|| '" must be 18.'); following points regarding these two techniques for
END IF; avoiding the hard-coding of the error number itself:
• Use of a predefined, named constant has the
Following the PL/Vision approach, I would predefine advantage of offering compile-time verification of my
my error number and give it a name in an error numbers choice of error. In other words, if I typed “errnums_
package (preferably generated) and then call the pkg.en_emp_too_yng”, my code wouldn’t compile,
appropriate raise program (record the error, and then since that identifier isn’t defined in errnums_pkg. If,
stop)... and while I’m at it, I’ll also move that hard-coded on the other hand, I typed ‘EMPLOYEE-TOO-YNG’
business rule logic to a separate package: for my error name, the code would compile but I
wouldn’t identify the right error. Compile-time
IF employee_rules.emp_too_young (:NEW.birthdate)
THEN verification is always preferable over runtime errors.
PLVexc.recNstop (
errnums_pkg.en_emp_too_young,
• Predefined, named constants require more advanced
,'Employee "' || :NEW.last_name || '"'); planning or cause you to write code in a “stop and
END IF;
start” manner. It’s hard to know in advance of writing
one’s application the various types of errors that may
This approach is certainly an improvement over
be encountered or that you may want to raise and
calling RAISE_APPLICATION_ERROR directly. Over the
handle. If you rely on predefined constants for error
past year, however, I’ve changed my views about how
numbers, each time you encounter the need for a new
best to organize, raise, and handle errors. I’d like to share
error you’ll have to stop writing your program to
those new views with you in this article. First, I’ll explain
upgrade the error numbers package. You’ll then need
what brought me to rethink my recommendations on
to recompile that package—and all the programs now
this topic.
marked invalid by the change to that package. With
literals, I can use “top down design”: As I write the
Learning from my students
program, I specify new errors. My program will
In November 2003, Quest Software sponsored a series of
compile, since these are literals. I can maintain my
seminars by yours truly in Germany and Scandinavia.
focus on the program at hand. When done, I can
While in Munich, I presented the aforementioned
“batch define” all of my errors at once.
approach for error handling. Two students approached
me over a break with a concern about this line:
So, there are pluses and minuses for each approach,
PLVexc.recNstop ( as is pretty much always the case with any set of
errnums_pkg.en_emp_too_young, programming alternatives.
,'Employee "' || :NEW.last_name || '"');
They were trying to come up with a general error What Qnxo needs
handling architecture as well, but they had a problem In addition to learning from my students, I’ve reshaped
with my named exceptions. “Each time you define a my views on error handling through the real-world
new exception,” they reminded me, “the errnums_pkg experience of building Qnxo, the world’s first active
specification would be changed and recompiled. This mentoring software product (www.qnxo.com). Qnxo
would invalidate all of the programs that reference the (Delphi front end and Oracle-PL/SQL back end) needs a
package. In our system, that simply won’t do.” solid error handling infrastructure, in which back-end
These students suggested instead using literal strings errors are recorded and also communicated in a robust
to identify the error, as in: manner to its users. That’s nothing particularly unique.
But my goal with Qnxo is to help you build applications
PLVexc.recNstop ( the right way, faster than you can do them the “quick and
'EMPLOYEE-TOO-YOUNG',
,'Employee "' || :NEW.last_name || '"'); dirty” way. Since error handling is a key aspect of doing
things “the right way,” it’s my intention to expose the error
My first reaction was visceral: “You are putting hard- handling infrastructure I use in Qnxo, so that users of
coded literals in your programs. That’s bad.” They then Qnxo can apply it to their own applications. So I’d better
pointed out that my approach (errnums_pkg.en_emp_ get it right!
too_young) also “hard-coded” the name of the error. It As my error handling strategy for Qnxo evolved, I
simply wasn’t a literal. Now, that made me sit back and discovered, among other things, that I definitely preferred
“We could not find any columns for the object named
$object owned by $schema. Perhaps this is not a table or
you do not have sufficient privileges to see this table.”
The most interesting part of what Qnxo is doing What I’m doing here is bundling up the error
behind the scenes is creating an error instance row, instance handle and passing it back as the error message
associating lots of information with it, and then passing in this format:
back the primary key of that error instance row (the
ORA-20000: QNXO000000001345
handle) in the RAISE_APPLICATION_ERROR error
message. Let’s take a closer look at the heart of this logic, I use this format so that when the host environment
shown in Listing 7. calls qd_runtime.get_error_info (discussed in the next
section), I can be sure that the value 1345 is an error
Listing 7. Passing back the error instance handle. instance handle and not some random number string
that has nothing to do with Qnxo.
PROCEDURE issue_oracle_raise ( So I’ve come up with a way to pass a unique handle
err_instance_in IN
qd_err_instance_tp.qd_err_instance_rt to extensive error information back to the calling
) program. Now we need to see how I can get all of that
IS
l_error qd_error_tp.qd_error_rt; error information given the error message.
BEGIN
l_error := qd_error_qp.onerow
(id_in => err_instance_in.error_id); Handling errors with qd_runtime.get_error_info
-- 1. Ready to raise the application error.
Raising the exception is just the first step. Now we have to
IF l_error.code BETWEEN -20999 AND -20000 extract the information recorded with that error so it can
A
few years ago, I needed a base64 encoding There are two flavors of compression in the java.util.zip
function to allow sending binary e-mail classes: ZIP, which compresses a stream of bytes and
attachments within PL/SQL. The first version I stores it in an archive wrapped with information such as
wrote was in PL/SQL; performance wasn’t particularly file name, date, size, and comment; and GZIP, which
good, so I tried doing the same thing in Java. I began compresses a single stream, but doesn’t wrap the results
this more as a learning exercise than anything else, since in any metadata. GZIP is an appropriate choice when you
my Java skills are quite rudimentary, but I found the want to compress a single “thing” and you don’t need to
performance was dramatically better, and the Java store the identification of what you’re compressing in the
InputStream and OutputStream classes were easy to compressed data. Because the only operations available
work with. in GZIP are compression and decompression, the user
A good rule for programming is to use the simplest interface to access the classes from PL/SQL is very
tool available for any task, and while Oracle uses Java simple. These consist of the following:
extensively for its own purposes, as a developer I’ve had • Java procedures that compress CLOBs and
little reason to—why use Java when I can use PL/SQL just BLOBs, storing the result in a BLOB passed as an
as well? Some things, however, aren’t readily available in OUT parameter.
the version of Oracle (9.2) that I, like many others, have • Java procedures that decompress a BLOB containing
access to, and data compression is one of them. data compressed with GZIP, returning the
uncompressed data as either a CLOB or a BLOB.
Accessing LOBs from Java • PL/SQL procedures that publish the Java procedures.
The InputStream and OutputStream classes in Java • PL/SQL procedures that provide the direct API
provide simple and efficient access to input and for developers. These aren’t required to make use
output. Oracle provides the class oracle.sql.CLOB for of the Java routines, but they provide a more
processing CLOBs and oracle.sql.BLOB for processing convenient syntax.
BLOBs. These map to Oracle’s CLOB and BLOB
datatypes, and provide methods that return InputStream Compressing a BLOB using GZIPOutputStream
and OutputStream descendents for reading from and I’ve placed all of the Java code for the GZIP procedures
writing to their corresponding datatypes. These classes in a single class that I’ve named GZIPFORPLSQL. The
are documented in the Oracle manual Application complete source code is available in the Download file.
Developer’s Guide—Large Objects (LOBs). For BLOBs, I use The Java method for compressing a BLOB is displayed in
the oracle.sql.BLOB methods getBinaryStream() and Listing 1, followed by a description.
declare
vRaw raw(2000); As you can see, very little appears to happen to
vGZip raw (2000); Session PGA Memory. This is of interest because of what
vUnGZip raw(2000);
begin happens to Session PGA Memory Max—remember that
for rec in (select object_name from all_objects the statistics are collected at the point that the mystatistics
where rownum < 1000) loop
vRaw := utl_Raw.cast_to_Raw(rec.object_name); view is queried.
I believe that these two tables combined show pretty Listing 6. Breaking up a job into sections.
clearly that:
procedure compressJob( pInitial in number)
1. Session PGA Memory grows linearly in relation to is
the number of calls to the Java procedure in the vRaw raw(2000);
vGZip raw (2000);
processing loop. vUnGZip raw(2000);
2. The memory is released on the next process—Session vCtr number := 1;
vJob number;
PGA Memory was always “normal,” because the cursor testCursor(cpFrom number) is
query that retrieved the statistic called the PGA to select object_name from
(select rownum prownum, object_name
be released. from all_objects
order by owner, object_Name) where prownum
>= cpFrom and prownum < cpFrom + 500;
This looked to me like a problem with the JVM. I tried begin
various permutations in the Java code, including writing for rec in testCursor( pInitial) loop
vCtr := vCtr + 1;
them both as procedures and as functions, setting the vRaw := utl_Raw.cast_to_Raw(rec.object_name);
instance variables to null, calling System.gc() to try to vGZip := GZIP.gZipRaw(vRaw);
vUnGzip := GZIP.unGZipRaw(vGZip);
force garbage collection, and so on—none of that made if utl_raw.cast_to_Varchar2(vUnGZip) <>
any difference. rec.object_name then
raise_application_error(-20001,
I found two different bugs on Metalink that should be 'GZIP mismatch with ' || rec.object_name);
studied by anyone interested in using the code described Continues on page 16
in this article:
• Bug 2791857 (“ORA-4030 OUT OF PROCESS
MEMORY RUNNING JAVA STORED PROCEDURE”)
describes exactly the problem I encountered in this
article, apparently even with the same Java classes.
This is described as a bug in the Deflator classes
(from which GZIP inherits) causing a memory leak.
However, I see no evidence that the memory is
leaking in my example; it’s just not being released
until the next process is encountered after the loop.
• Bug 2875058 (“UNLIMITED PGA MEMORY
D:
GROWTH WHEN REPEATEDLY CALLING JSP”)
seems closer to the mark. In this bug, the problem GE
A
isn’t described as a memory leak, and it’s not specific PA
to the Deflator classes. In the Metalink document, the
RTER OFT
QU REALS
memory is reported as reused when the loop is next
run—still not exactly what I’m seeing here, since the
A
tables show that the Session PGA Memory returns to
normal as soon as the mystatistics view is queried.
Attribute Denormalization
Peter Hamilton
In this article, Peter Hamilton examines alternative methods the same place? I’m well on the way to deciding to
of merging logical entities representing few attributes into denormalize the attributes of the event queue.
a single physical database table, a frequent decision in the
database design process. Traditional approach
Traditionally, attribute denormalization is achieved by
T
HE process of physical database design invariably determining an upper threshold for the number of
requires us to make decisions about the adoption parameters that the event queue can have. Once decided,
of denormalized data structures. There can be the parameters are listed param_1, param_2, and so on.
many instances where controlled denormalization is So, for an event queue with 10 possible parameters, we’d
deemed appropriate. have this:
A typical example is an address, which, in its most
normalized state, yields an address entity and an address SQL> desc eq_denorm
Name
line entity. If the normalized structure were implemented, ------------------
ID
the result would be two physical tables (address and EVENT_TYPE
address line) that contain a single attribute, the address EQ_NAME
PARAM_VAL_1
line data. There’s clearly a case for denormalizing this PARAM_VAL_2
structure into a single physical table (address). This article ..
PARAM_VAL_10
focuses on this type of design approach, which I’ve
termed “attribute denormalization,” and examines some
I encounter numerous instances of this type of design.
methods by which it can be achieved.
It has the advantage that the event queue and associated
parameters are held together in a single record, benefiting
Problem domain the performance of both selection and manipulation. It
Figure 1 illustrates a normalized event queue, which
has several disadvantages, however:
I’ve used throughout this article. The idea is that
• The designer must enforce an arbitrary upper limit
events are stored in the queue for later processing by
on the number of parameters that an event queue
an event actioning process. Each event can have a
can have.
number of parameters.
• The data can be quite sparse if a typical event queue
As I look at the entity structure shown in Figure 1,
has a few parameters.
I get the feeling that the parameter table adds little
• Any SQL that manipulates the table must make the
value to the design. It merely stores the parameter
mapping between the list of parameters and the
values, which are always associated with a single event
actual data items.
queue element. As the parameters are always retrieved
along with the event queue item, why not store them in
The consequence of the last point is that the chosen
limit of the number of parameters per queue, in our
example, often manifests itself within the application
source code. For example, on selection of an event
queue record in a PL/SQL program, each parameter
may be received into an element of a collection. The
program needs to know how many elements of the
collection to fetch the data into, as illustrated in the
following sample code. In this way, the arbitrary
maximum limit of attributes becomes ingrained within
the whole application.
DECLARE
TYPE P_LIST IS TABLE OF
eq_denorm.param_val_1%TYPE;
Figure 1. Normalized event queue. p_id NUMBER := 1001;
size of each nested table row combined with the average l_i NUMBER := 0;
number of rows per parent row. As a result, the best guess l_j NUMBER := 0;
l_p_list PARAM_TAB_LIST := PARAM_TAB_LIST();
maximum parameters per event queue hasn’t vanished
BEGIN
completely in this solution. FOR l_i IN 1..1000
LOOP
l_list.EXTEND;
Benchmarking l_list( l_i ) := ( l_i * 10 );
Before drawing any conclusions about the advantages/ END LOOP;
disadvantages of each approach, I thought it advisable to -- Bulk select the parameters into a multi
consider performance issues. My analysis considered -- dimensional array
Conclusion
I’ve considered a variety of methods for implementing a
denormalized attribute structure and, as is often the case
with design, have found no single “best case” solution.
The denormalized enumeration of attribute_1..
attribute_n may be appropriate when the maximum
number of lines is known, and when insert/update
Figure 2. Nested table storage. performance is important. The client developer will have
14 Oracle Professional September 2004 www.pinnaclepublishing.com
to take the pain of coding around this. reap the rewards of a heterogeneous PL/SQL interface. ▲
Clustering may be appropriate when a set number
of values for a key is known (for instance, 48 half hourly 409HAMILTON.ZIP at www.pinnaclepublishing.com
meter readings per day), and when storage space is at
a premium. Peter Hamilton is an independent consultant specializing in Oracle
The nested table structure should be considered application development. He has more than 15 years of IT development
when insert/update performance of the nested table experience and has worked with Oracle products for the past 10 years.
elements isn’t critical, and when the number of nested He’s currently helping IBM develop the central fund clearing system for
table elements is uncertain. The client developer may then the UK banking industry. PeterHamilton@bacs.co.uk.
Error Handling in PL/SQL... one or more tables at the time the exception is raised, and
then pass the handle to the instance in my call to RAISE_
Continued from page 7 APPLICATION_ERROR. The host environment can then
The error instance handle is the key decide what it wants to do, how much information to
We offer more and better information to Qnxo users than display, and so on. ▲
I’ve previously been able to do with my error handling
Steven Feuerstein is considered one of the world’s leading experts
packages, such as PLVexc of PL/Vision—and I owe it all
on the Oracle PL/SQL language, having written nine books on PL/SQL,
to the idea of those error instances. If I rely on the error
including Oracle PL/SQL Programming and Oracle PL/SQL Best Practices
code and message passed by RAISE_APPLICATION_
(all from O’Reilly Media). Steven has been developing software since
ERROR, there’s only so much information I can pass—or I 1980 and serves as a Senior Technology Advisor to Quest Software.
pass it in a single string, packed with data, separated by His current projects include Qnxo (www.qnxo.com), a new active
various delimiters, and very hard to manage, particularly mentoring software product, and the Refuser Solidarity Network
by the front end. (www.refusersolidarity.net), which supports the Israeli military refuser
With the error instance, I can store information in movement. steven@stevenfeuerstein.com.
Know a clever shortcut? Have an idea for an article for Oracle Professional?
Visit www.pinnaclepublishing.com and click on “Write For Us” to submit your ideas.
Pinnacle, A Division of Lawrence Ragan Communications, Inc. ▲ 800-493-4867 x.4209 or 312-960-4100 ▲ Fax 312-960-4106
What’s next
end if; This article covered the use of the GZIP classes.
end loop;
if vCtr >= 499 then
java.util.zip also contains methods for processing ZIP
dbms_job.submit(vjob, files. ZIP is, of course, the standard format for both
'compressJob('||to_char(pInitial + 500)||');',
sysdate);
compressing and archiving files. Because a ZIP file (or
end if; BLOB!) may contain multiple files, along with names,
end;
comments, dates, and so on, the API that’s needed to
make full use of these classes is more complex than what
If the job processes a full 500 rows, it resubmits
I created here to use GZIP. I plan to discuss a PL/SQL
itself, passing <pInitial + 500> as the starting point for
API for creating and reading ZIP files in a future Oracle
the next iteration. This allows any number of rows to be
Professional article. ▲
processed without expanding Session PGA Memory to
unworkable limits. Using this strategy, I was able to 409MENCHEN.TXT at www.pinnaclepublishing.com
complete my test case of verifying compression and
decompression on all 25,000 plus rows in the ALL_ Gary Menchen is a senior programmer analyst at Dartmouth College
OBJECTS view. Please note that this exercise was for in Hanover, NH. One of his current interests is document generation
testing purposes only—there’s no space savings to be in PL/SQL, and he’s putting the finishing touches on a package that
realized in using GZIP to compress a column that will allow generation of PDF documents from within PL/SQL.
averages less that 50 characters in length; the resulting Gary.E.Menchen@Dartmouth.Edu.
For access to current and archive content and source code, log in at www.pinnaclepublishing.com.
Phone: 800-493-4867 x.4209 or 312-960-4100 Copyright © 2004 by Lawrence Ragan Communications, Inc. All rights reserved. No part
Fax: 312-960-4106 of this periodical may be used or reproduced in any fashion whatsoever (except in the
Email: PinPub@Ragan.com case of brief quotations embodied in critical articles and reviews) without the prior
written consent of Lawrence Ragan Communications, Inc. Printed in the United States
of America.
Advertising: RogerS@Ragan.com
Oracle, Oracle 8i, Oracle 9i, PL/SQL, and SQL*Plus are trademarks or registered trademarks of
Editorial: FarionG@Ragan.com Oracle Corporation. Other brand and product names are trademarks or registered trademarks
of their respective holders. Oracle Professional is an independent publication not affiliated
Pinnacle Web Site: www.pinnaclepublishing.com with Oracle Corporation. Oracle Corporation is not responsible in any way for the editorial
policy or other contents of the publication.
Subscription rates This publication is intended as a general guide. It covers a highly technical and complex
subject and should not be used for making decisions concerning specific products or
applications. This publication is sold as is, without warranty of any kind, either express or
United States: One year (12 issues): $199; two years (24 issues): $348 implied, respecting the contents of this publication, including but not limited to implied
Other:* One year: $229; two years: $408 warranties for the publication, performance, quality, merchantability, or fitness for any particular
purpose. Lawrence Ragan Communications, Inc., shall not be liable to the purchaser or any
Single issue rate: other person or entity with respect to any liability, loss, or damage caused or alleged to be
caused directly or indirectly by this publication. Articles published in Oracle Professional
$27.50 ($32.50 outside United States)* reflect the views of their authors; they may or may not reflect the view of Lawrence Ragan
Communications, Inc. Inclusion of advertising inserts does not constitute an endorsement by
* Funds must be in U.S. currency. Lawrence Ragan Communications, Inc., or Oracle Professional.