Download as pdf or txt
Download as pdf or txt
You are on page 1of 16

Oracle

Solutions for High-End


Oracle® DBAs and Developers Professional

Having Fun with


PL/SQL Collections
Steven Feuerstein
In this article, Steven Feuerstein discusses how to take advantage of PL/SQL
collections to solve some common problems every developer may encounter.

I
recently returned from a week in the United Kingdom teaching PL/SQL to
several hundred developers and DBAs. My time with these fans of PL/SQL
reinforced for me an unfortunate set of circumstances: Most programmers
are still not taking advantage of some of the most important new features in November 2004
PL/SQL—new as of Oracle8i, that is!
Volume 11, Number 11
Truly, we write code in rather deep ruts established over several or many
more years of programming. Truly, our managers didn’t often give us the time 1 Having Fun with
or space to explore new features in a language. Sadly, that puts so many of us PL/SQL Collections
at a severe disadvantage when writing new programs. Steven Feuerstein
Given that situation, I’ve decided to devote this article to sharing with
8 Alphanumeric Sequences
you the fun I have with PL/SQL collections. If you’re a regular reader of this in Oracle
newsletter, these list/array/table-like structures shouldn’t be new discoveries. Anunaya Shrivastava
Yet, I’m still willing to wager that you don’t use them as often or in as varied a and Alok Kundu
fashion as you could.
10 Working with
Rather than take up space explaining the syntax of declaring and Multi-Table Inserts
manipulating collections, I’m going to offer a few scenarios that I’ve Al Hetzel
encountered recently in which I found collections to be very helpful. Perhaps
they’ll inspire you to do the same with your own applications. 15 Tip: Foreign Key
Relationship Options
Dan Clamage
Avoid those long IF statements
My article in last month’s issue of Oracle Professional explored my efforts to 16 November 2004 Downloads
“fix” DBMS_UTILITY.NAME_RESOLVE, and make sure that it worked
correctly under all circumstances. DBMS_UTILITY.NAME_RESOLVE accepts
the name or possible name of a database object and resolves it into each of its
component pieces, including a “part type.” The part type is an integer value Indicates accompanying files are available online at
www.pinnaclepublishing.com.
Continues on page 3
D:
GE
A GE
PA LED
L W
FUL KNO
BAL
GLO

2 Oracle Professional November 2004 www.pinnaclepublishing.com


PL/SQL Collections... own drawbacks. It means that you have to install that
table, perhaps requiring DBA intervention, and then
Continued from page 1 maintain the code in the table via SQL. It makes using my
defined in the Oracle documentation as follows: NAME_RESOLVE utility a bit more complicated.
5 - Synonym A better solution, I think, would be to store this
7 - Procedure information in a table inside my PL/SQL program. That
8 - Function way, you don’t have to install a separate table to use
9 - Package my alternative to NAME_RESOLVE. You just run the
program. And the best way to define and populate a
When I built my first “added value” program on top “table” within a PL/SQL program is to use a collection.
of NAME_RESOLVE, I included a function to translate the So I transformed my implementation of this in-function
obscure type number to a string: translator function to the following pieces of code.
Step 1: In the package specification for my NAME_
FUNCTION object_type ( RESOLVE utility, I defined a set of constants, one for each
type_in IN PLS_INTEGER)
RETURN VARCHAR2 of my known part numbers:
IS
synonym_type
CONSTANT INTEGER := 5; CREATE OR REPLACE PACKAGE qnr
procedure_type AUTHID CURRENT_USER
CONSTANT INTEGER := 7; IS
function_type c_table_type CONSTANT PLS_INTEGER := 2;
CONSTANT INTEGER := 8; c_view_type CONSTANT PLS_INTEGER := 4;
package_type c_synonym_type CONSTANT PLS_INTEGER := 5;
CONSTANT INTEGER := 9; c_procedure_type CONSTANT PLS_INTEGER := 7;
retval VARCHAR2 (20); c_function_type CONSTANT PLS_INTEGER := 8;
BEGIN c_package_type CONSTANT PLS_INTEGER := 9;
IF type_in = synonym_type c_object_type CONSTANT PLS_INTEGER := 13;
THEN
retval := 'Synonym';
ELSIF type_in = procedure_type I expose this information in the package specification
THEN
retval := 'Procedure'; so others can make use of this information as needed. It’s
ELSIF type_in = function_type certainly not my own “private” set of values (“qnr”, by
THEN
retval := 'Function'; the way, is an acronym for Qnxo Name Resolve).
ELSIF type_in = package_type Step 2: I also make public the program for translating
THEN
retval := 'Package'; from number to name:
ELSE
retval := TO_CHAR (type_in);
END IF; FUNCTION name_for_type (type_in IN PLS_INTEGER)
RETURN VARCHAR2;
RETURN retval;
END;
Step 3: Within the package body, I define the
This code works, but I have several concerns about it: collection type and the collection I’ll use in the package:
• It’s very verbose code, which means there’s more
CREATE OR REPLACE PACKAGE BODY qnr
code to read and maintain. In fact, every time I IS
TYPE names_tt IS TABLE OF
discover another type number and string, I’ll need to all_objects.object_type%TYPE
add an ELSIF clause. INDEX BY BINARY_INTEGER;
• It’s very repetitive code, which of course leads to g_names names_tt;
verbosity (though they’re not entirely the same issue).
Beyond that, I like to maintain a healthy allergy to The type of collection is an associative array (formerly
repetitions and patterns. They usually indicate a known as the Index-by Table, a much superior name, I
waste of time and a danger of unrepaired bugs (I think), essentially a single-dimensioned list of the names
fixed it in six places, but missed the seventh). of object types. Notice that I anchor the datatype back to
the OBJECT_TYPE column of ALL_OBJECTS. Any part
Stepping back from this code, I take a moment to type must be reflected in that data dictionary view (or so I
think about the scenario: I have integer types and I have hypothesize), so why not self-document that fact?
descriptions for each type. Surely it would have made a The g_names variable is an instance of this type of
lot of sense for Oracle to put this information into a data collection, and it’s the structure I’ll use to hold all of my
dictionary view (hey, maybe they have and I just don’t names. Where do I do that?
know about it!). And certainly I could do the same for Step 4: Well, since I’ve defined this collection at the
myself (and everyone else): Build a table, put this package level, I can fill it up with values the first time
information into it, and make it available to the wider any program in my package is used, and then it will
world of PL/SQL developers. keep those values throughout my entire session. That’s
Storing this information in a table, however, has its convenient! The best way to populate the collection is

www.pinnaclepublishing.com Oracle Professional November 2004 3


through the initialization section of the package, which designed several years ago.
is all the way at the bottom, after any procedures and With CGML, I can create “aliases” that allow me to
functions defined in the package. In the case of my qnr implement flexible naming conventions and complex
package, I define this section as follows: logic within my scripts. For years (yes, I wrote the original
version of my generation engine several years ago, and
BEGIN have been enhancing it a lot in the past year), my aliases
initialize_types;
END qnr; have been defined and stored in an underlying table,
/ sg_doir (the Object Information Repository). I would add
an alias to the table by calling this program:
I don’t place the assignments to g_names directly in
the initialization section; rather, I call a program to do this PROCEDURE sg_doir_xp.setalias (
work. I generally prefer to call an initialization program tab IN VARCHAR2
, orig IN VARCHAR2
for two reasons: , subst IN VARCHAR2
• I can then call this program from other places in the , sch IN VARCHAR2 := NULL
);
package as needed.
• If I do lots of complex initialization, I can hide those where tab is the name of the object, orig is the name of the
details and manage them more easily in programs. alias, subst is the substitution value for the alias, and sch is
the owner name. This program would perform an insert
Step 5: Now I define my initialization program: into sg_doir.
I then retrieve an alias value with the alias function:
PROCEDURE initialize_types
IS
BEGIN FUNCTION sg_doir_xp.alias (
g_names (c_table_type) := 'Table'; tab IN VARCHAR2
g_names (c_view_type) := 'View'; , orig IN VARCHAR2
g_names (c_synonym_type) := 'Synonym'; , sch IN VARCHAR2
g_names (c_procedure_type) := 'Procedure'; )
g_names (c_function_type) := 'Function'; RETURN VARCHAR2
g_names (c_package_type) := 'Package';
g_names (c_object_type) := Object Type;
END initialize_types; A simple and clean API, and it was working just fine.
Recently, however, I found myself concerned about the
Notice that I take care to use the constants and not performance impact of this implementation.
the literal values for the row numbers. That way, While generating code from a complex script, I might
I avoid repetitive references to hard-coded values. set and get aliases hundreds and even thousands of times.
Step 6: And, finally, my new function to return the Furthermore, these alias values are specific to a given
name for a type number: database object, and will be refreshed each time I generate
against that object. So why would I want to persist those
FUNCTION name_for_type (
type_in IN PLS_INTEGER) values in a database table and endure the overhead of the
RETURN VARCHAR2 insert and then query lookup, over and over again?
IS
BEGIN Upon reflection, it suddenly seemed obvious that I
RETURN g_names (type_in); should be caching my alias values in a collection and then
EXCEPTION
WHEN NO_DATA_FOUND retrieving that information as needed. That approach
THEN could be substantially faster.
RETURN type_in;
END name_for_type;
Take a deep breath and plunge?
Ah, such simplicity! I’ve cleanly separated the Great! I’ve identified a potential optimization step for the
population of my data structure and maintenance of the generation engine, a very welcome prospect. Shall I just
data itself, from the code that retrieves the data. Now as dive in and start hacking up my code? Not a chance!
I need to add new type-name pairs, my name_for_type The code generation engine is one of the most
function doesn’t need to be changed, thanks to the use complicated pieces of code I’ve ever written—and also
of collections! the closest thing to spaghetti code I’ve ever written. So
it’s a bit scary to contemplate making a change to some
Avoiding unnecessary SQL execution fundamental mechanism in the engine. Have you ever
As many readers of Oracle Professional are likely aware, felt afraid of changing code? That’s a very bad situation
I’ve been busy the past year building Qnxo, the world’s to be in.
first active mentoring software (www.qnxo.com). One of The way to conquer such fears is clear:
its main features is a powerful, flexible code generation 1. Write a comprehensive regression test for the
engine. This engine accepts templates or scripts written in program to verify that it currently works (or
the Code Generation Markup Language (CGML) that I confirm the current known bugs).

4 Oracle Professional November 2004 www.pinnaclepublishing.com


2. Then, make changes in a very careful, step-by- and should therefore be done with the new cache.
step fashion. After each small change, run the
regression test to make sure no new bugs have PROCEDURE set_value

been introduced. For a given schema, object name, and alias name, set the
value in the cache.
Well... in this case, I must admit that I don’t really
FUNCTION get_value
have a distinct regression test for my code generation For a given schema, object name, and alias name, return
engine. On the other hand, I use Qnxo generation the value from the cache.
extensively myself to generate Qnxo code, and the
templates I use exercise the generation engine very
thoroughly. So I can make a change, generate a new set Listing 1. The sg_object_alias specification.
of package APIs, and compare those to the previous,
CREATE OR REPLACE PACKAGE sg_object_alias
current set of packages. Not bad, really. IS
Having, to some extent, conquered my fears, I SUBTYPE value_t IS VARCHAR2 (32767);

decided to launch into a “refactoring” or internal PROCEDURE clear_aliases;


restructuring of my alias get-and-set process. The FUNCTION is_object_alias (
sg_doir_xp package is very large and complicated. I’d schema_in IN VARCHAR2
,object_in IN VARCHAR2
like to figure out a way to very cleanly insert my new ,NAME_IN IN VARCHAR2
)
functionality with minimum possible impact on the rest RETURN BOOLEAN;
of sg_doir_xp. To do that, I need to figure out precisely
PROCEDURE set_value (
when and how I want to apply this new approach to schema_in IN VARCHAR2
,object_in IN VARCHAR2
aliases. After giving it some thought, I realize that I face ,NAME_IN IN VARCHAR2
the following situation: ,value_in IN VARCHAR2
);
• I use the alias mechanism for much more than object-
FUNCTION get_value (
level aliases. I can and do set aliases at global and schema_in IN VARCHAR2
schema levels. I don’t want to make any changes in ,object_in IN VARCHAR2
,NAME_IN IN VARCHAR2
this processing. )
• When I’m setting an object-level alias, I want to RETURN value_t;

bypass the sg_doir table and instead use collections. END sg_object_alias;
/
• An alias value is set by and retrieved according to
an object name-alias name string. In other words,
A very simple API. Now, how do I go about
the index into my alias value is a string and not a
implementing such a cache? The answer varies greatly
number or integer.
and unfortunately depends on the version of Oracle
you’re running. If you’re running on Oracle9i Release
The first two points steer me in the direction of
2 and above, you can take advantage of string-based
creating an entirely separate package from sg_doir, which
indexes in your collections, which makes the
I’ll call sg_object_alias. That way, I automatically isolate
implementation of this package almost transparently
the bulk of coding activity. Even before I fully implement
easy. In Oracle8i, I can only index collections by integers.
this approach, I can easily define the specification—the
That means that I’ll need to implement a hashed index,
programs I’ll need to use my cache. The package
converting each string to an integer and using that as the
specification is shown in Listing 1 and described here:
key to my alias value.
SUBTYPE value_t
I define my own application datatype for the value of Easy and quick: String-based indexes
an alias. This just helps improve the self-documentation Assuming you’re on an Oracle9i or above version of
level of my code. Oracle, we can very easily implement the sg_object_alias
package body. Here are the steps I followed:
PROCEDURE clear_aliases Step 1: Define the collection to hold the alias values.
If I’m going to build a cache, I can easily realize in
advance that I’ll want to have a way to clear out CREATE OR REPLACE PACKAGE BODY
sg_object_alias
the cache. IS
c_delimiter CONSTANT CHAR(1) := '^';
FUNCTION is_object_alias SUBTYPE alias_name_t IS VARCHAR2 (200);
Since I only want to use this new approach in a limited
TYPE alias_values_tt IS
number of cases, I need a function to help me determine if TABLE OF VARCHAR2(32767)
I do indeed have one of those cases. This function returns INDEX BY alias_name_t;

TRUE if the alias that’s being set is an object-level alias g_alias_values alias_values_tt;

www.pinnaclepublishing.com Oracle Professional November 2004 5


I create a subtype to hold the datatype for an alias FUNCTION get_value (
schema_in IN VARCHAR2
name. The name of an alias is actually a composite ,object_in IN VARCHAR2
,NAME_IN IN VARCHAR2
structure made up of owner, object name, and alias name. )
To ensure uniqueness when I combine these multiple RETURN value_t
IS
values, I also will need a delimiter to separate the BEGIN
RETURN g_alias_values (one_string (schema_in
elements. So I declare a constant delimiter with a value of ,object_in
“^”. This can be changed to whatever will be guaranteed ,NAME_IN
,c_delimiter
not to appear in any of the elements. )
);
I then define a collection type indexed by the alias EXCEPTION
name, containing the largest strings possible. Finally, I WHEN NO_DATA_FOUND
THEN
declare a collection of that type. RETURN NULL;
END get_value;
Step 2: Define my clear_aliases program. There isn’t
much to it; just remove all the rows from the collection:
Once again, the code is fairly transparent and self-
PROCEDURE clear_aliases
documenting. Use one_string to construct the key and
IS then apply that key to the list to return the value. If I try
BEGIN
g_alias_values.DELETE; to retrieve a value for an undefined alias, return NULL.
END clear_aliases; And that’s all there is to it (well, almost all; I haven’t
shown you the implementation of the is_object_alias
Step 3: Create a function to return a consistent function, but it’s entirely application-dependent and of no
concatenated string from my alias name elements: interest outside of Qnxo).
Before reviewing the comparatively unnatural steps
FUNCTION one_string (
schema_in IN VARCHAR2 necessary in Oracle8i to implement my alias cache, let’s
,object_in IN VARCHAR2
,NAME_IN IN VARCHAR2 see how I can integrate this new functionality into my
,delimiter_in IN VARCHAR2 existing package, sg_doir_xp.
)
RETURN VARCHAR2 In my set program, I call the function to determine
IS whether this alias-setting request is for an object-level
BEGIN
RETURN UPPER ( schema_in alias. If so, I call my new packaged procedure to set
|| delimiter_in
|| object_in the alias value. Otherwise, I go back to my original
|| delimiter_in implementation. In this way, I’ve implemented a minimal
|| NAME_IN
); and very focused change in my critical package.
END one_string;
PROCEDURE sg_doir_xp.setalias (
tab IN VARCHAR2
This function is just about the most complicated , orig IN VARCHAR2
aspect of the package. Since my “key” is actually a , subst IN VARCHAR2
, sch IN VARCHAR2 := NULL
concatenated set of values, I need to make sure that )
IS
the key value is constructed the same for every BEGIN
combination of schema, object, and name. I also need to IF sg_object_alias.is_object_alias (
sch, tab, orig)
ensure uniqueness by inserting the delimiter between THEN
sg_object_alias.set_value (
each value. sch, tab, orig, subst);
Step 4: Implement the procedure to set a value. ELSE
<current implementation>
END IF;
PROCEDURE set_value ( END setalias;
schema_in IN VARCHAR2
,object_in IN VARCHAR2
,NAME_IN IN VARCHAR2 I take the same approach in my get program: I call the
,value_in IN VARCHAR2 function to determine whether this alias is for an object. If
)
IS so, I call my new packaged function to get the alias value.
BEGIN
g_alias_values (one_string (schema_in Otherwise, I rely on my original implementation.
,object_in
,NAME_IN FUNCTION sg_doir_xp.alias (
,c_delimiter tab IN VARCHAR2
) , orig IN VARCHAR2
) := value_in; , sch IN VARCHAR2
END set_value; )
RETURN VARCHAR2
IS
This is so easy! Just construct the index value from BEGIN
IF sg_object_alias.is_object_alias (
the individual strings using one_string, and then assign sch, tab, orig)
the value to that row. THEN
RETURN sg_object_alias.get_value (
Step 5: Implement the function to get a value. sch, tab, orig);

6 Oracle Professional November 2004 www.pinnaclepublishing.com


ELSE Swap out existing, SQL-based processing for collection-
RETURN <current implementation>;
END IF; based caching.
END alias;
In my move to a collection cache, I needed to very
carefully analyze my requirements and the impact on
Stuck in the past—Oracle8i
existing code. From my requirements, I was able to
Suppose, now, that I want to implement this alias cache,
deduce that a string-based index would be perfectly
but I’m stuck on Oracle8i. I’m not able to take advantage
suited to my task.
of string-based indices. I must instead take a more
I urge all readers to check out collections, and get
complicated approach, involving the hashing of my string
comfortable with how they work and all you can do in
key value (owner-name-alias) into an integer that is,
your version (for example, in Oracle9i, you have both
hopefully, very likely to be unique, which I’ll then use
string-based indexing and multi-level collection support;
as my index to the alias values.
in Oracle10g, you have high-level set operations for
I’ve written about hashing previously in these
nested tables).
pages (see “Oracle9i Release 2 Developments for
Then apply collections wherever possible to simplify
PL/SQL Collections” in the August 2002 issue of Oracle
and speed up your code. ▲
Professional). Briefly, you must do the following:
• Use the DBMS_UTILITY.GET_HASH_VALUE
411FEUER.ZIP at www.pinnaclepublishing.com
function to convert a string to an integer.
• There’s unfortunately no guarantee (no perfect
Steven Feuerstein is considered one of the world’s leading experts
algorithm so) that a distinct string will always
on the Oracle PL/SQL language, having written nine books on PL/SQL,
result in a unique integer. Consequently, when including Oracle PL/SQL Programming and Oracle PL/SQL Best Practices
working with hash algorithms, you must check for (all from O’Reilly Media). Steven has been developing software since
and resolve conflicts (two different strings hashing 1980 and serves as a Senior Technology Advisor to Quest Software.
to the same number). His current projects include Qnxo (www.qnxo.com), a new active
• Create a hash index collection to store the string key mentoring software product, and the Refuser Solidarity Network
values (alias name), and another collection to store (www.refusersolidarity.net), which supports the Israeli military refuser
the alias value itself. movement. steven@stevenfeuerstein.com.

It’s a much more complicated solution. Rather than


go over the details here (Oracle8i is, after all, being
desupported as of January 2005!), I’ll include the code
(sg_object_alias8i.pkb) in the accompanying Download.
I would like, however, to take a few paragraphs to
describe the process I used to implement this Oracle8i
solution in Qnxo (I was forced to take this approach since
Qnxo is supposed to be able to install and run on Oracle8i
and above).
Rather than write the complex code I described
D:
earlier, I started up Qnxo and did a search for “hash”
with the Qnxo Explorer. I found a script named “Template GE
A
hash index package with conflict resolution.” I then PA
TER
generated my alternate-index hash package from this
R OFT
template by providing a few arguments at generation
time. Next, I integrated this almost-ready-to-run code into
A
QU REAL S
my sg_object_alias package body.
In rather short order, I had a package doing hash
indexing with conflict resolution. It sure is nice having a
repository of predefined reusable code and templates
from which code can be generated!

Collect your thoughts, collect your data


I presented in this article two examples of applying
collections to solve problems. In the first, “name for
type,” I replaced ugly, repetitive code with an elegant use
of a simple associative array. In the second example, I
tackled a much more complicated and tricky problem:

www.pinnaclepublishing.com Oracle Professional November 2004 7


Oracle
Professional

Alphanumeric Sequences
in Oracle
Anunaya Shrivastava and Alok Kundu
In this article, Anunaya Shrivastava and Alok Kundu define Let’s say we want to create a four-digit alphanumeric
techniques for developing alphanumeric sequences that can sequence. There are multiple ways the sequence can start.
be used in your Oracle database applications. For instance, the sequence can start as A000, or 100A, or
AAAA, or 0000. However, whatever sequence you come

T
HE term sequence means a succession of things in up with, it has to follow the aforementioned sequence
a definite order, size order, or logical order. In rules. The sequence should also work with all Oracle
mathematics, a sequence is a list of objects (or built-in functions. The sorting on VARCHAR2 datatype
events) that have been ordered in a sequential fashion, columns containing numeric or date data behaves in an
such that each member either comes before, or after, every unexpected manner. Keeping that factor in mind, an
other member. Sequences are needed in software database alphanumeric sequence in Oracle should:
implementations to uniquely identify a record. Oracle • Follow the Order by clause.
introduced numeric sequences back in Oracle 7. First, • Support the MAX function (SQL Max) and the
you create a numeric sequence as a database object, GREATEST function.
and then you can use it for generating your sequential • Support relational operators (=, >, <).
numbers. The numeric sequence has attributes— • Generate the next element of the sequence.
minimum and maximum value of the sequence, cache
size, recycle mode, and so on. However, Oracle doesn’t To accommodate these factors, our sequence should
provide any sequences that support alphanumeric start at 0000 and end at ZZZZ. The point of inflection is
characters, though you may have some physical entities 999A, where the sequence changes from numeric to
where numeric sequences don’t suffice. Examples of alphanumeric, from 9999 to 999A. Another point of
where alphanumeric sequences might be useful are: inflection is AAAA, where the sequence changes from
• In building interfaces with legacy code where the alphanumeric to completely alphabetical, from 9ZZZ to
primary keys are non-numeric. AAAA. The last element of the sequence is ZZZZ.
• When you have limited space in a column in which The reason this sequence was developed with the
you want to accommodate a lot of data elements— pattern described in Figure 1—from numeric to
more than what a numeric series can offer. alphanumeric to alphabetical—was to support most of
• To maintain some alphanumeric codes in certain the Oracle built-in functions. If we were to develop a
places, such as in a library where you want to sequence 000A, 000B, 000C or any other way, the sequence
maintain book codes (for example, 999R). would run into issues with standard Oracle functions.
The data element 0001 will be greater than 000A in this
To develop an alphanumeric sequence, we need to series. However, by Oracle functioning, the default
keep some basic tenets of numeric sequences in mind. sorting would put 0001 before 000A. Also, the MAX
The alphanumeric sequence should follow general function will bring an unexpected value from such a
mathematical induction principles. If a statement is true sequence. So this series won’t be able to support the
for all n elements in the domain, the following statements default sorting on a VARCHAR2 column in Oracle.
must be maintained: Therefore, we took the approach displayed in Figure 1.
• The statement is true for n=1 (first element of We developed the functions GET_CURRENT_VAL, to
the sequence). generate the current sequence element in the database,
• If the statement is true for n= k, then it has to be true and GET_NEXT_VALUE, to give us the next sequence
for n = k+1 (subsequent elements of the sequence). element. These are displayed in Listing 1 and Listing 2.

Working with an example


The issue with alphanumeric
sequences is that they can start with
any alphabet-numeric combination. Figure 1. Alphanumeric sequence line.
8 Oracle Professional November 2004 www.pinnaclepublishing.com
This is akin to using currval and nextval operations on an --If the 1st byte of input string is '9'
--then set it to 'A'. Else increment the
Oracle database numeric sequence. --1st byte.

IF v_inp_str_char1 = '9' THEN


v_inp_str_char1 := 'A';
Listing 1. The GET_CURRENT_VAL function. ELSE
v_inp_str_char1 := CHR (ASCII (
v_inp_str_char1) + 1);
FUNCTION GET_CURRENT_VAL RETURN VARCHAR2 IS END IF;
V_curr_seq Varchar2(10); ELSE
Begin v_inp_str_char2:= CHR (
ASCII (v_inp_str_char2) + 1);
SELECT MAX(your seq_col) END IF;
INTO V_curr_seq END IF;
FROM <your table>; ELSE
v_inp_str_char3 := CHR (
Return v_curr_seq; ASCII (v_inp_str_char3) + 1);
End ; END IF;
END IF;
ELSE
v_inp_str_char4 := CHR(ASCII(v_inp_str_char4) +1);
Listing 2. The GET_NEXT_VALUE function. END IF;
END IF;
v_inp_str:=v_inp_str_char1 || v_inp_str_char2 ||
FUNCTION GET_NEXT_VALUE (p_val in VARCHAR2) v_inp_str_char3||v_inp_str_char4;
RETURN VARCHAR2 IS END IF;
v_inp_str Varchar2(10);
v_max_numeric NUMBER; return v_inp_str;
v_inp_str_char1 CHAR (1);--1st byte of the input str
v_inp_str_char2 CHAR (1);--2nd byte of the input str END;
v_inp_str_char3 CHAR (1);--3rd byte of the input str
v_inp_str_char4 CHAR (1);--4th byte of the input str
BEGIN To quickly check the number of elements present
v_inp_str:= p_val;
begin in the sequence, we use a small piece of code to loop
v_max_numeric:= to_number(v_inp_str);
exception through the entire sequence as described here:
When value_error then
v_max_numeric := -5;
declare
end;
v_start_val Varchar2(10):= '0000';
IF v_max_numeric NOT in (-5,9999) then v_cnt NUMBER:=0;
v_inp_str:= to_char(v_max_numeric +1,'0999'); begin
END If;
LOOP
IF v_max_numeric = 9999 then v_cnt:= v_cnt+1;
v_inp_str:= '999A' ; v_start_val :=Get_next_value(v_start_val );
END IF; Exit when v_start_val = 'ZZZZ';

End LOOP;
IF v_max_numeric =-5 then
dbms_output.put_line('The num of elements is'||v_cnt);
--Extract the 1st byte of the digits
v_inp_str_char1 := SUBSTR(v_inp_str, 1, 1); end;
--Extract the 1st byte of the input string /
v_inp_str_char2:= SUBSTR (v_inp_str, 2, 1);
--Extract the 2nd byte of the input string The num of elements is 485253.
v_inp_str_char3 := SUBSTR (v_inp_str, 3, 1);
--Extract the 3rd byte of the input string
v_inp_str_char4 := SUBSTR (v_inp_str, 4, 1); Future scope
--Extract the 4th byte of the input string
IF v_inp_str_char4 = '9' THEN
We’ve just given you a simple example with some items
v_inp_str_char4 := 'A'; to consider when working with non-numeric sequences.
ELSE
--If the 4th byte of input string is '9' then set it You can enhance this methodology by developing a
--to 'A'. Else if the 3rd byte of input string is Z system that expands the aforesaid approach to any
--then set it to 0 and increment the 1st or 2nd byte.
--Otherwise increment the 3rd byte only. number of digits for generating an alphanumeric
IF v_inp_str_char4 = 'Z' THEN
v_inp_str_char4 := 'A'; sequence that meets your specific requirements.
IF v_inp_str_char3= '9' THEN
v_inp_str_char3:= 'A';
ELSE Key benefit
--If the 3rd byte of input string is '9' then set it
--to 'A'. Else if the 3rd byte of input str is Z The biggest advantage of having an alphanumeric
--then set it to 0 and increment the 1st or 2nd byte. sequence is that you can have many more data elements
--Otherwise increment the 3rd byte only.
in the alphanumeric sequence than in a numeric sequence
IF v_inp_str_char3 = 'Z' THEN
v_inp_str_char3 := 'A'; of comparable size. Using our example with four digits,
the number of data elements that you can have in a
--If the 2nd byte of input string is '9' then
--set it to 'A'. Else if the 2nd byte of input str numeric sequence is 9999 (excluding 0000), but in an
--is Z then set it to 0 and increment the 1st byte.
--Otherwise increment the 2nd byte only.
alphanumeric series that follows the Oracle sorting rules
(refer to Figure 1) you can have 485,253 data elements
IF v_inp_str_char2 = '9'THEN
v_inp_str_char2 := 'A'; (excluding 0000)—almost 49 times the number of
ELSE elements that you can have with a numeric sequence.
IF v_inp_str_char2 = 'Z' THEN
v_inp_str_char2 := 'A'; Continues on page 14

www.pinnaclepublishing.com Oracle Professional November 2004 9


Oracle
Professional

Working with Multi-Table Inserts


Al Hetzel
If you’ve ever been in a situation where half of a transaction command with the INSERT keyword. Immediately after
was committed, you’ll appreciate the multi-table insert. This this keyword, you want to specify whether this is an “all
feature (first available with Oracle9i) allows you to insert the insert” or a “first insert” with the appropriate keyword.
results of a query into multiple tables simultaneously. In this To specify an all insert, you use the keyword ALL. An
article, Al Hetzel explains this new multi-table insert feature. all insert will check every line of data against all of the
conditions. If there aren’t any conditions, the data will be

C
OMMITS seem to be such a simple command. When inserted into every table listed. Otherwise, it will only be
you want to save the data changes, you simply type inserted into the tables where the conditions are met.
in “commit” and a semicolon and press Enter. Or, To specify a first insert, you use the keyword FIRST.
you add them to a procedure in order to save the data A first insert will check every line of data against all of
changes made in that procedure. However, as simple as the conditions as well. However, it will stop once one of
the commit is, it can cause you some major problems, the conditions is met. If you specify a first insert, you
particularly when you’re inserting new rows. need at least one condition.
The problem is data integrity. Each row of data may
need to be inserted into many different tables. Unless you [when {condition}]
violate some foreign key, Oracle won’t tell you that you
forgot a table. Instead, you’ll be blissfully unaware until This line of the multi-table insert is the condition
the omission is found. line. Since you can create an insert without any
You may never make a mistake like the one I’ve conditions, this line is optional. However, if you do
just described. However, it’s unlikely that you’re the include one or more conditions, you have to do so in the
only person who’s using your database. Non-Oracle format shown. Each condition line starts with the WHEN
programmers are adding records all the time. It doesn’t keyword followed by the actual condition statement. This
make a difference whether these are Web developers or statement is a Boolean value that generally relates to the
client/server developers, as you’re responsible for line of data being processed. For instance, if you were
making sure that their data is correct. inserting customer data and put the condition line “when
With Oracle9i and 10g, you have a new type of insert customer_id < 100”, then only the customers that had an
that will solve many of these issues: the multi-table insert. id less than 100 would be subject to the into statement(s)
that follow.
Command format You can have as many condition lines as you need. If
As you can probably figure out from the name, you can you have a first insert, you can also use the ELSE keyword
use a multi-table insert to insert data into several tables at to create a special condition. This condition will only be
the same time. The complete format for this command is met if all of the other conditions have failed. It’s a catch-
shown in Listing 1. all condition.
Now let’s look at the next statement:
Listing 1. Command format for the multi-table insert. into {table_name}
( {column_names} )
INSERT {all|first} values
[WHEN {condition}] ( {select_column_names} )
INTO {table_name}
( {column_names} )
VALUES These four lines of code make up the INTO clause. In
( {select_column_names} )
… other words, these lines determine exactly what data will
{select_statement}; be inserted into which table. You’ll probably notice that
the format of these lines is very similar to a regular insert
Before getting to some examples, let’s take a closer statement. The differences are detailed in the remainder of
look at the different parts of the multi-table insert. this section.
insert {all|first} The first line determines where the data will go. You
start this line with the INTO keyword, and then you need
Since this is an insert statement, you start the to list a table name. This table can be any table on the

10 Oracle Professional November 2004 www.pinnaclepublishing.com


database where you have the insert privilege. table holds the data that’s suggested by its name. For
The second line determines which columns of the example, the customer table holds all of the information
table will receive data. You list all columns that will directly related to the customer, including name, date of
receive data. Just like a regular insert statement, you birth, gender, and so on. The customer_address table
separate all of the column names with a comma. You holds all of the information related to the address,
should make sure that you’re including all required including the street address, city, state, and Zip Code.
columns. If you don’t, the insert will create an error. The primary key for the customer table is customer_
The third line is just the VALUES keyword. As you’ll id. This field is also a foreign key on customer_address,
see in a moment, a multi-table insert must receive the customer_phone, and customer_spouse. None of the data
data from a select statement. For a regular insert, when types is given here or in the examples that follow; data
the data comes from a select statement, you don’t include types aren’t relevant for these examples.
this keyword. However, for this version of the insert Now let’s try some inserts.
statement, it’s required.
The fourth line represents the data that will be Unconditional insert
entered into the table and columns listed above. This The most basic type of multi-table insert is the
data can come from two sources. First, you can include unconditional insert. The name of the insert gives the
constants just like a regular insert statement. Second, details. It’s unconditional—in other words, there will be
you can include a column name from the select statement no condition lines. Instead, every row of data that’s
that follows. returned by the select statement is inserted into the
As with the conditions, you can include any number various tables.
of INTO clauses. The only limitation is that you need at You’d use this type of insert when you need to
least one INTO clause for each condition or at least one quickly split all of the data up onto separate tables. In
for the entire multi-table insert if you don’t have any order to better illustrate this, let’s look at an example.
conditions. The last statement is: Using the customer_load table identified earlier, I want
to create an unconditional insert that will insert all of the
{select_statement};
data into the customer and customer_address tables (see
This line of the multi-table insert determines what Listing 2).
data will be inserted into the tables. As mentioned earlier, For this example, I won’t be using the customer_
this data must come from a select statement, and you can phone or customer_spouse table. Both of these tables
make the statement as simple or as complex as you want require conditions, as you’ll see later. Since the
to so long as it’s valid. unconditional insert won’t have any conditions, they’re
When creating the statement, you should remember not used in the example.
that you’ll be using the column names in the INTO clause.
In order to keep things simple, you should always name Listing 2. The unconditional insert example.
any column that’s not a single field. For instance, if you
combine two fields as part of the select statement, you insert all
into customer
should give that column a name. You can then refer to ( customer_id, first_name, middle_initial,
last_name, birth_date, gender, married )
that field by that name in the INTO clause. values
( customer_id, customer_first_name,
customer_middle_initial, customer_last_name,
Example tables customer_birth_date, customer_gender, married )
Multi-table inserts have three different forms—the into customer_address
( customer_id, address1, address2, city,
unconditional insert, the conditional all insert, and state, zip )
the conditional first insert. I’ll discuss all three types. values
( customer_id, address1, address2, city,
However, before I do that, I’ll need some sample tables state, zip )
select customer_id, customer_first_name,
to use as examples—I’ll use the five tables detailed in customer_middle_initial, customer_last_name,
this section. customer_birth_date, customer_gender, married,
address1, address2, city, state, zip
The first table is the customer_load table. This table is from customer_load;
a loader table. In other words, the data is stored here for
convenience until it can be inserted into the proper tables. This example has two INTO clauses. The first one
One very real possibility is that customer_load is an inserts records into the customer table. The second one
external table that doesn’t exist on the database. Instead, inserts records into the customer_address table. Since the
it could exist on the server as a flat file. Until you load it primary key on the customer table is the foreign key on
into the database, that is. the customer_address table, they must be inserted in this
The next four tables—customer, customer_address, order. Otherwise, you’ll get a constraint error, unless
customer_phone, and customer_spouse—are the you’re working with deferred foreign keys.
normalized tables that will ultimately hold the data. Each You should notice the data lines for the customer
www.pinnaclepublishing.com Oracle Professional November 2004 11
INTO clause. The names of the fields correspond exactly married and you want to insert the spouse information
with the names in the select statement. For instance, the into the customer_spouse table. You wouldn’t want to
customer_first_name field on the customer_load table always insert this row since, if the customer isn’t married,
matches up to the first_name field on the customer table. all of the spouse information would be null. Not only
When building the data lines, you always want to include is it a poor practice to insert null rows, but some of the
the exact name of the data column. fields on the customer_spouse table might have a not
null constraint.
Conditional all insert
The conditional all insert table is the next type of multi- Conditional first insert
table insert. Conditional simply means that there will be The conditional first insert table is similar in many ways to
one or more condition lines. There could be more, but the previous type. It will also have one or more condition
there will be at least one. lines. The major difference is the FIRST keyword. When
The word “all” in the name of the insert means that this keyword is used, the INTO clause of the first true
the keyword ALL was used, making this an all insert. condition will be executed and none of the others.
This type of insert will compare each of the data rows For this example (see Listing 4), I’ll rewrite the
from the select statement to every condition. When a previous example except using a first insert. Although
condition is met, the INTO clause under that condition I could just replace the keyword ALL with the keyword
is executed. FIRST, that wouldn’t give me what I want. Remember
For the next example (see Listing 3), I’m going to add that in the previous example, the first condition was
the customer_spouse table to the previous example. The always true. In a first insert, the two INTO clauses
customer table has a field on it called married. When this under that condition would be the only ones that ever
field is set to ‘Y’, the customer has a spouse who will be executed. The second condition would never be checked,
inserted into the customer_spouse table. and therefore the INTO clause under that condition
would never be executed. This syntax is similar to the
fall-through logic of an IF or CASE statement, where
Listing 3. The conditional all insert example.
once a condition is met, the statement is exited.
insert all
when married in ('Y','N') then
into customer Listing 4. The conditional first insert example.
( customer_id, first_name, middle_initial,
last_name, birth_date, gender, married )
values insert first
( customer_id, customer_first_name, when married = 'N' then
customer_middle_initial, customer_last_name, into customer
customer_birth_date, customer_gender, married ) ( customer_id, first_name, middle_initial,
into customer_address last_name, birth_date, gender, married )
( customer_id, address1, address2, city, values
state, zip ) ( customer_id, customer_first_name,
values customer_middle_initial, customer_last_name,
( customer_id, address1, address2, city, customer_birth_date, customer_gender, married )
state, zip ) into customer_address
when married = 'Y' then ( customer_id, address1, address2, city,
into customer_spouse state, zip )
( customer_id, first_name, middle_initial, values
last_name, birth_date, gender ) ( customer_id, address1, address2, city,
values state, zip )
( customer_id, spouse_first_name, when married = 'Y' then
spouse_middle_initial, spouse_last_name, into customer
spouse_birth_date, spouse_gender ) ( customer_id, first_name, middle_initial,
select customer_id, customer_first_name, last_name, birth_date, gender, married )
customer_middle_initial, customer_last_name, values
customer_birth_date, customer_gender, married, ( customer_id, customer_first_name,
spouse_first_name, spouse_middle_initial, customer_middle_initial, customer_last_name,
spouse_last_name, spouse_birth_date, spouse_gender, customer_birth_date, customer_gender, married )
address1, address2, city, state, zip into customer_address
from customer_load; ( customer_id, address1, address2, city,
state, zip )
values
This example has two conditions. The first ( customer_id, address1, address2, city,
state, zip )
condition checks to see if the married field is either into customer_spouse
( customer_id, first_name, middle_initial,
‘Y’ or ‘N’. This field is a flag that’s always one or the last_name, birth_date, gender )
other. In other words, this condition will always be values
( customer_id, spouse_first_name,
true. You always want to insert into the customer and spouse_middle_initial, spouse_last_name,
spouse_birth_date, spouse_gender )
customer_address tables. The customer table has the select customer_id, customer_first_name,
important customer_id field that’s used as a foreign key customer_middle_initial, customer_last_name,
customer_birth_date, customer_gender, married,
on all of the other tables. spouse_first_name, spouse_middle_initial,
The second condition checks to see if the married spouse_last_name, spouse_birth_date, spouse_gender,
address1, address2, city, state, zip
field is ‘Y’. If this is the case, then the customer is from customer_load;

12 Oracle Professional November 2004 www.pinnaclepublishing.com


As with the previous example, this one has two customer_birth_date, customer_gender, married )
into customer_address
conditions. The first condition checks to see if the married ( customer_id, address1, address2, city,
state, zip )
field is set to ‘N’. In other words, it checks to see if the values
customer isn’t married. If the condition is true and the ( customer_id, address1, address2, city,
state, zip )
customer isn’t married, you insert into the customer and when married = 'Y' then
into customer_spouse
customer_address tables. You don’t insert into the ( customer_id, first_name, middle_initial,
customer_spouse table. last_name, birth_date, gender )
values
The second condition checks to see if the married ( customer_id, spouse_first_name,
field is Y’ (the customer is married). You could replace spouse_middle_initial, spouse_last_name,
spouse_birth_date, spouse_gender )
this condition with an ELSE statement if you wanted to. when phone_number is not null then
into customer_phone
For this situation, either would work. If this condition is ( customer_id, phone_type, phone_number )
met and the customer is married, you insert information values
( customer_id, 'Phone', phone_number )
into the customer, customer_address, and customer_ when fax_number is not null then
into customer_phone
spouse tables. ( customer_id, phone_type, phone_number )
You’ll notice that two of the tables (customer and values
( customer_id, 'Fax', fax_number)
customer_address) are included in both conditions. As when cell_number is not null then
mentioned previously, these tables will always get inserts. into customer_phone
( customer_id, phone_type, phone_number )
So, you have to include them regardless of whether the values
( customer_id, 'Cell', cell_number )
customer is married or not. Often in a conditional first select customer_id, customer_first_name,
insert, you’ll find yourself including the same INTO customer_middle_initial, customer_last_name,
customer_birth_date, customer_gender, married,
clause for several different conditions. spouse_first_name, spouse_middle_initial,
spouse_last_name, spouse_birth_date,
spouse_gender, address1, address2, city, state,
Pivoting the insert zip, phone_number, fax_number, cell_number
from customer_load;
In addition to the three main types of multi-table inserts,
you can also use the pivoting sub-type. A pivoting insert
The last three conditions show the pivoting insert.
isn’t truly a separate type of insert. Instead, it’s a variation
In the first condition, the phone_number field is
of the other three types. This type of insert is used to
checked on the customer_load table to make sure it’s not
insert multiple rows of data into the same table.
null (in other words, if it exists). If this field does exist,
Remember that in the conditional first insert, I used
the INTO clause loads that data into the customer_phone
the same INTO clause multiple times. However, since
table. If it doesn’t exist, you don’t want to insert an
only one of the conditions could be true, the INTO clause
empty row. The phone_type field isn’t populated from
was only executed once. Therefore, each table will have a
the customer_load table. Instead, you use a constant,
single INSERT clause. For a pivoting insert, this isn’t the
‘Phone’, as the type.
case. Each table could have multiple INSERT clauses.
The second and third conditions do the exact same
In order to illustrate this point, I’ll rewrite my
thing, except for the fax_number and cell_number fields.
example. I’ll start with the conditional all insert statement
The phone_type fields for these two INTO clauses are set
and add in the customer_phone table. This table is made
to ‘Fax’ and ‘Cell’, respectively. Each row of data in the
up of three columns—customer_id, phone_type, and
customer_load table can create up to three rows of data in
phone_number. Although customer_id and phone_
the customer_phone table.
number are self-explanatory, phone_type could use
some explanation. Phone_type determines whether the A look back
phone listed is a regular phone number, a cell number, In the previous example, I used a multi-table insert
or a fax number. including pivoting. This insert consisted of five
On the customer_load table, there’s one column conditional statements. It also had six INTO clauses
for each phone type—phone_number, cell_number, and within those different conditions. For the next example
fax_number. In this example (see Listing 5), I want to (see Listing 6), I’ll re-create the entire command without
pivot those three columns into three inserts of the using multi-table inserts.
customer_phone table.

Listing 6. How to do it without multi-table inserts.


Listing 5. The pivoting insert.
insert into customer
insert all ( customer_id, first_name, middle_initial, last_name,
when married in ('Y','N') then birth_date, gender, married )
into customer select customer_id, customer_first_name,
( customer_id, first_name, middle_initial, customer_middle_initial, customer_last_name,
last_name, birth_date, gender, married ) customer_birth_date, customer_gender, married
values from customer_load;
( customer_id, customer_first_name,
customer_middle_initial, customer_last_name, insert into customer_address

www.pinnaclepublishing.com Oracle Professional November 2004 13


( customer_id, address1, address2, city, state, zip ) of data could be resource-intensive.
select customer_id, address1, address2, city, state,
zip Fortunately, we can get around this problem.
from customer_load;
Regardless of the number of INTO clauses or conditions,
insert into customer_spouse a multi-table insert will only need to do one full access
( customer_id, first_name, middle_initial, last_name,
birth_date, gender ) (see Figure 1).
select customer_id, spouse_first_name,
spouse_middle_initial, spouse_last_name,
As you can see from the figure, our multi-table insert
spouse_birth_date, spouse_gender only scanned the customer_load table once, although it
from customer_load
where married = 'Y'; performed six inserts. That’s much more efficient than
insert into customer_phone
inserting into each table individually.
( customer_id, phone_type, phone_number ) You should keep in mind that all inserts are affected
select customer_id, 'Phone', phone_number
from customer_load by certain elements. For instance, triggers and indexes
where phone_number is not null; can slow down an insert. The efficiency of the select
insert into customer_phone statement also plays a major part. Since these elements
( customer_id, phone_type, phone_number )
select customer_id, 'Fax', fax_number
affect both the regular and multi-table inserts, I haven’t
from customer_load covered them here.
where fax_number is not null;

insert into customer_phone


( customer_id, phone_type, phone_number )
Conclusion
select customer_id, 'Cell', cell_number Whenever you need to do multiple inserts off one set of
from customer_load
where cell_number is not null; data, you should consider using a Multi-Table Insert. The
command may look a little strange, but you’ll never have
As you can see, I couldn’t create a single insert to worry about partial transactions again. ▲
command that would do exactly the same thing as my
multi-table insert. Instead, I had to create six statements. Al Hetzel is a freelance database/Web developer located in Dallas, TX.
al@hetzel.com.
Each of these statements inserts into a different table.
Since the multi-table insert and the preceding
example are approximately the same size, you may
wonder why I’d want to use the more complex
multi-table insert. One reason is the commit problem
that I outlined previously. If one insert was committed
without committing the others, we’d have a major
transaction problem.
The multi-table insert doesn’t have this problem.
It’s a single command. If it’s designed correctly, it can’t
commit half a transaction.

Performance issues
Another reason to use a multi-table insert is the
performance gains. In the six inserts shown previously,
each statement uses a similar select statement to provide
the data for the insert. This means that each insert will
have to complete a full access on the customer_load table.
Since this is a loader table, it could have a huge amount of
data on it. Cycling through this potentially large amount Figure 1. Comparing the explain plans.

Alphanumeric Sequences... create your own alphanumeric sequences, be sure to


take into consideration the pitfalls we discussed with
Continued from page 9
Oracle functions, so that you aren’t surprised with
Conclusion unexpected results. ▲
The alphanumeric sequence described in this article
Anunaya Shrivastava, PMP, OCP Financials, OCP Internet Developer,
can be a powerful tool for solving complex problems
OTC, has been working with Oracle technology for more than eight
where you convert data from a legacy system and need years. He works for HCL Technologies (IL). anunaya@hotmail.com.
to build a solution to accommodate the alphanumeric
codes. It’s also handy when you have to accommodate Alok Kundu, PMP, has been working with Oracle technology for
a lot of index information in a limited space column and more than 14 years. He works as a Manager at Bearingpoint.
can’t modify the structure of your table. If you plan to alok_kundu@yahoo.com.

14 Oracle Professional November 2004 www.pinnaclepublishing.com


Tip: Foreign Key Relationship Options
Dan Clamage

Suppose the relationship you wish to enforce is that when you || SQL%ROWCOUNT);
END;
delete from table A, you must also delete the child rows from /
table B; but if you attempt to delete from table B directly, you
must forbid it. To accommodate this, I’ll create the tables and To see what the call stack looks like when I delete from B, I’ll
insert some data: create a trigger and then delete a row.

CREATE OR REPLACE TRIGGER bd_b


CREATE TABLE a (pk_a NUMBER); BEFORE DELETE ON b
ALTER TABLE a ADD PRIMARY KEY (pk_a); FOR EACH ROW
BEGIN
CREATE TABLE b (pk_b NUMBER, fk_a NUMBER); dbms_output.put_line('b: '
ALTER TABLE b ADD PRIMARY KEY (pk_b); || dbms_utility.format_call_stack);
ALTER TABLE b ADD FOREIGN KEY (fk_a) END;
REFERENCES a (pk_a); /

INSERT INTO a (pk_a) VALUES (1); DELETE FROM a


INSERT INTO a (pk_a) VALUES (2); WHERE pk_a = 1;
INSERT INTO a (pk_a) VALUES (3);
INSERT INTO a (pk_a) VALUES (4); 1 row(s) deleted
INSERT INTO b (pk_b, fk_a) VALUES (10,1);
INSERT INTO b (pk_b, fk_a) VALUES (11,1); a: ----- PL/SQL Call Stack -----
INSERT INTO b (pk_b, fk_a) VALUES (12,2); object line object
INSERT INTO b (pk_b, fk_a) VALUES (13,2); handle number name
127980A8 2 SCOTT.BD_A
b: ----- PL/SQL Call Stack -----
object line object
I’ll create a trigger on A to guarantee the child rows in B are handle number name
first deleted. 1279A9D8 2 SCOTT.BD_B
127980A8 3 SCOTT.BD_A
b: ----- PL/SQL Call Stack -----
CREATE OR REPLACE TRIGGER bd_a object line object
BEFORE DELETE ON a handle number name
FOR EACH ROW 1279A9D8 2 SCOTT.BD_B
BEGIN 127980A8 3 SCOTT.BD_A
dbms_output.put_line('a: ' a: #child rows deleted: 2
|| dbms_utility.format_call_stack);
DELETE FROM b From the call stack, you can see that the calling context
WHERE fk_a = :OLD.pk_a;
dbms_output.put_line('a: #child rows deleted: ' for the delete from B includes the trigger on A. Therefore, I can

Don’t miss another issue! Subscribe now and save!


Subscribe to Oracle Professional today and receive a special one-year introductory rate:
Just $179* for 12 issues (that’s $20 off the regular rate)

NAME ❑ Check enclosed (payable to Pinnacle Publishing)


❑ Purchase order (in U.S. and Canada only); mail or fax copy
COMPANY
❑ Bill me later
❑ Credit card: __ VISA __MasterCard __American Express
ADDRESS
CARD NUMBER EXP. DATE

CITY STATE/PROVINCE ZIP/POSTAL CODE


SIGNATURE (REQUIRED FOR CARD ORDERS)

COUNTRY IF OTHER THAN U.S.


Detach and return to:
Pinnacle Publishing ▲ 316 N. Michigan Ave. ▲ Chicago, IL 60601
E-MAIL Or fax to 312-960-4106

* Outside the U.S. add $30. Orders payable in


INS4
PHONE (IN CASE WE HAVE A QUESTION ABOUT YOUR ORDER) U.S. funds drawn on a U.S. or Canadian bank.

Pinnacle, A Division of Lawrence Ragan Communications, Inc. ▲ 800-493-4867 x.4209 or 312-960-4100 ▲ Fax 312-960-4106

www.pinnaclepublishing.com Oracle Professional November 2004 15


rework the trigger on B to only succeed when called from the A object line object
handle number name
trigger. I’ll define a global user-defined exception to represent 1279A9D8 4 SCOTT.BD_B
1279A438 2 anonymous block
the error. not called from the trigger!

CREATE OR REPLACE PACKAGE app_except IS


e_delete EXCEPTION; Now I make sure I can still delete from B from A:
END app_except;
/
DELETE FROM a
CREATE OR REPLACE TRIGGER bd_b WHERE pk_a = 2;
BEFORE DELETE ON b 1 row(s) deleted
FOR EACH ROW
DECLARE a: ----- PL/SQL Call Stack -----
v_stack VARCHAR2(255); object line object
BEGIN handle number name
v_stack := dbms_utility.format_call_stack; 127980A8 2 SCOTT.BD_A
dbms_output.put_line('b: ' || v_stack); b: ----- PL/SQL Call Stack -----
IF (INSTR(v_stack, 'BD_A', 1) = 0) THEN object line object
-- not called from trigger bd_a handle number name
RAISE app_except.e_delete; 1279A9D8 4 SCOTT.BD_B
END IF; 127980A8 4 SCOTT.BD_A
END; b: ----- PL/SQL Call Stack -----
/ object line object
handle number name
1279A9D8 4 SCOTT.BD_B
127980A8 4 SCOTT.BD_A
Next, I test it out: a: #child rows deleted: 2

BEGIN
DELETE FROM b Remove the dbms_output from the triggers and you’re
WHERE fk_a = 2;
dbms_output.put_line('anon: #B rows deleted: ' good to go. ▲
|| SQL%ROWCOUNT);
EXCEPTION WHEN app_except.e_delete THEN
dbms_output.put_line('not called from the trigger!'); Dan Clamage has been working with Oracle and especially PL/SQL since
END; 1995. He lives in Pittsburgh, PA, with his wife, Robin, and their two
/
b: ----- PL/SQL Call Stack ----- daughters, Patricia and Daniele. danielj@clamage.com.

November 2004 Downloads


• 411FEUER.ZIP—Source code to accompany Steven Feuerstein’s article, “Having Fun with PL/SQL Collections.”

For access to current and archive content and source code, log in at www.pinnaclepublishing.com.

Editor: Garry Chan (gchan@procaseconsulting.com) Oracle Professional (ISSN 1525-1756)


Contributing Editor: Bryan Boulton is published monthly (12 times per year) by:
CEO & Publisher: Mark Ragan
Pinnacle Publishing
Group Publisher: Michael King A Division of Lawrence Ragan Communications, Inc.
Executive Editor: Farion Grove 316 N. Michigan Ave., Suite 300
Chicago, IL 60601
Questions?
POSTMASTER: Send address changes to Lawrence Ragan Communications, Inc., 316
Customer Service: N. Michigan Ave., Suite 300, Chicago, IL 60601.

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
other person or entity with respect to any liability, loss, or damage caused or alleged to be
Single issue rate:
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.

16 Oracle Professional November 2004 www.pinnaclepublishing.com

You might also like