Tải bản đầy đủ
PLVmsg: Single-Sourcing PL/SQL Message Text

PLVmsg: Single-Sourcing PL/SQL Message Text

Tải bản đầy đủ

[Appendix A] Appendix: PL/SQL Exercises
v_max_row BINARY_INTEGER;

Note that a user of PLVmsg cannot make a direct reference to the msgtxt_table or the low and high row
values; these data structures are hidden in the package body. I am, in this way, able to guarantee the integrity
of the message text.

8.7 Implementing
PLVtab.display

9.2 Storing Message Text

Copyright (c) 2000 O'Reilly Associates. All rights reserved.

9. PLVmsg: Single−Sourcing PL/SQL Message Text

291

Chapter 9
PLVmsg: Single−Sourcing
PL/SQL Message Text

9.2 Storing Message Text
Before your programs can retrieve messages from the PLVmsg PL/SQL table, you must place these messages
in the table. You can do so in one of two ways:
1.
Load individual messages with calls to the add_text procedure.
2.
Load sets of messages from a database table with the load_from_dbms procedure.

9.2.1 Adding a Single Message
With add_text, you add specific strings to the message table at the specified row. Here is the header for
add_text:
PROCEDURE add_text (err_code_in IN INTEGER, err_text_in IN VARCHAR2);

The following statements, for example, define message text for several error numbers set aside by Oracle
Corporation for application−specific use (passed with a call to the RAISE_APPLICATION_ERROR builtin):
PLVmsg.add_text (−20000, 'General error');
PLVmsg.add_text (−20100, 'No department with that number.);
PLVmsg.add_text (−20200, 'Employee too young.');

Section 9.3, "Retrieving Message Text", later in this chapter, will show how you can extract these messages.

9.2.2 Batch Loading of Message Text
In many environments, a database table is used to store and maintain error messages, as well as other types of
message text. The load_from_dbms procedure can be used to make this information available through the
PLVmsg interface. The header for this procedure is:
PROCEDURE load_from_dbms
(table_in IN VARCHAR2,
where_clause_in IN VARCHAR2 := NULL,
code_col_in IN VARCHAR2 := 'error_code',
text_col_in IN VARCHAR2 := 'error_text');

This procedure reads the rows from the specified table and transfers them to the PL/SQL table. Recall that the
PLVmsg msgtxt_table is not filled sequentially; the rows defined in the table are determined by the
contents of the code column in the specified table.
To make the package as flexible as possible, PLVmsg relies on DBMS_SQL so that you can use whatever
database table fits (or already exists) in your schema. When you call load_from_dbms, you tell it the name
of the table and its columns, as well as an optional WHERE clause. The PLVmsg program then constructs the
292

[Appendix A] Appendix: PL/SQL Exercises
SQL necessary to grab the text data. The only requirement of the table is that it has a numeric column for
message numbers (used as PL/SQL table rows) and a string column for the message text.
You must, at a minimum, provide the name of the messages table. The default names of the columns are:
error_code
The error number for the message
error_text
The text of the error message
In the following call to load_from_dbms, I rely on the full set of defaults for the structure of the error table
to transfer all rows from the error_messages table:
PLVmsg.load_from_dbms ('error_messages');

This request will work only if the error_messages table has columns named error_code and
error_text.
In this next example, I supply customized values for all arguments:
PLVmsg.load_from_dbms
('errtxt',
'code BETWEEN −20000 AND −20999',
'code', 'text');

My table is named errtxt and has two columns, code and text. I further request that only the text for
messages with error numbers between −20,000 and −20,999 be placed in the PLVmsg PL/SQL table. This
WHERE clause implies that for all other errors, my program will rely on the message returned by SQLERRM
(see the next section for more details).
You might be asking yourself: why bother with PLVmsg if you already have a database table−driven
architecture for such messages? There are two key advantages:
1.
With PLVmsg you will be reading the message text from memory (after the initial transfer) without
having to go through the SQL layer. This will improve performance, though it will also require more
memory since each user of PLVmsg will have her own copy of the messages.
2.
PLVmsg is very flexible, in that you can dynamically direct your program to either the PLVmsg text
or the database error message.

9.1 PLVmsg Data
Structures

9.3 Retrieving Message
Text

Copyright (c) 2000 O'Reilly Associates. All rights reserved.

293

Chapter 9
PLVmsg: Single−Sourcing
PL/SQL Message Text

9.3 Retrieving Message Text
The text function hides all the logical complexities involved in locating the correct message text and
information about physical storage of text. You simply ask for the message and PLVmsg.text returns the
information. That message may have come from SQLERRM or from the PL/SQL table. Your application
doesn't have to address or be aware of these details. Here is the header for the text function (the full
algorithm is shown in Example 9.1):
FUNCTION text (num_in IN INTEGER := SQLCODE) RETURN VARCHAR2;

You pass in a message number to retrieve the text for that message. If, on the other hand you do not provide a
number, PLVmsg.text uses SQLCODE.
The following call to PLVmsg.text is, thus, roughly equivalent to displaying SQLERRM:
p.l (PLVmsg.text);

I say "roughly" because with PLVmsg you can also override the default Oracle message and provide your
own text. This process is explained below.
Example 9.1: Algorithm for Choosing Message Text
FUNCTION text (num_in IN INTEGER := SQLCODE)
RETURN VARCHAR2
IS
msg VARCHAR2(2000);
BEGIN
IF (num_in
BETWEEN c_min_user_code AND c_max_user_code) OR
(restricting AND NOT oracle_errnum (num_in)) OR
NOT restricting
THEN
BEGIN
msg := msgtxt_table (num_in);
EXCEPTION
WHEN OTHERS
THEN
IF oracle_errnum (num_in)
THEN
msg := SQLERRM (num_in);
ELSE
msg := 'No message for error code.';
END IF;
END;
ELSE
msg := SQLERRM (num_in);
END IF;
RETURN msg;
EXCEPTION
WHEN OTHERS

294

[Appendix A] Appendix: PL/SQL Exercises
THEN
RETURN NULL;
END;

9.3.1 Substituting Oracle Messages
The following call to add_text is intended to override the default Oracle message for several rollback
segment−related errors:
FOR err_ind IN −1550 .. −1559
LOOP
PLVmsg.add_text
(err_ind, 'Database failure; contact SysOp at x1212');
END LOOP;

9.2 Storing Message Text

9.4 The Restriction Toggle

Copyright (c) 2000 O'Reilly Associates. All rights reserved.

9.3.1 Substituting Oracle Messages

295

Chapter 9
PLVmsg: Single−Sourcing
PL/SQL Message Text

9.4 The Restriction Toggle
Use the restriction toggle to determine whether messages for errors numbers that are legitimate Oracle error
numbers will be retrieved from the PLVmsg PL/SQL table (unrestricted) or from the SQLERRM function
(restricted). A legitimate Oracle error number is an integer that is negative or zero or 100 (equivalent to −1403
or "no data found").
The restriction toggle is composed of three programs:
PROCEDURE restrict;
PROCEDURE norestrict;
FUNCTION restricting RETURN BOOLEAN;

When you call the PLVmsg.restrict procedure (and this is the default setting), PLVmsg will rely on
SQLERRM whenever appropriate to retrieve the message for a legitimate Oracle error number.
If you call norestrict, PLVmsg will first check the PL/SQL table of PLVmsg to see if there is an error
message for that error. In unrestricted mode, therefore, you can automatically substitute standard Oracle error
messages with your own text −− and be as selective as you like about the substitutions.
The restricting function will let you know the status of the restrict toggle in PLVmsg. It returns
TRUE if you are restricting error messages to SQLERRM; otherwise, it will return FALSE.
Here are examples of the toggle in use:
1.
In a SQL*Plus script, direct all error messages to be retrieved from the PL/SQL table, if present.
PLVmsg.norestrict;
transfer_data;

2.
At the start of a SQL*Plus session, make sure that Oracle messages will be used whenever possible.
SQL> exec PLVmsg.restrict;

9.3 Retrieving Message
Text

9.5 Integrating PLVmsg
with Error Handling

Copyright (c) 2000 O'Reilly Associates. All rights reserved.

296

Chapter 9
PLVmsg: Single−Sourcing
PL/SQL Message Text

9.5 Integrating PLVmsg with Error Handling
Although PLVmsg can be used in other circumstances, PL/Vision uses it inside its exception handler package,
PLVexc, and you are most likely to use it that way as well. This section shows you how to do this.
Suppose that you have taken the time to write a procedure named showerr to consolidate error handling. It
accepts an error number−message combination and then both displays the message and records the error. If
you do not make use of PLVmsg, a typical exception section might look like this:
EXCEPTION
WHEN DUP_VAL_ON_INDEX
THEN
showerr (SQLCODE, 'Duplicate employee name.');
WHEN OTHERS
THEN
IF SQLCODE = −20200
THEN
showerr (−20200, 'Employee too young.');
ELSE
showerr (SQLCODE, SQLERRM);
END IF;
END;

What's the problem with this approach? I can think of several drawbacks:

You have to do lots of typing. It took me several minutes to type out this example and I type quickly.
It also provides lots of opportunities for errors.

The developer has to know about DUP_VAL_ON_INDEX (I, for one, always get it wrong the first
time; it seems that it should be IN_INDEX).

There is some dangerous hard−coding in this section: both the −20,200 and the associated error
message. What happens if you need to handle the same error in another program?
Now, suppose on the other hand that I had made use of PLVmsg. First, I would have added text to the
PLVmsg repository as follows:
PLVmsg.add_text (−1, 'Duplicate employee name.');
PLVmsg.add_text (−20200, 'Employee too young.');

Sure, I had to know that ORA−00001 goes with the DUP_VAL_ON_INDEX exception, but remember that I
will be writing this once for all developers on an application team. After setting these values I would also have
called the norestrict toggle. This allows PLVmsg to override the usual error message for ORA−00001
with my own message.

297

[Appendix A] Appendix: PL/SQL Exercises
PLVmsg.norestrict;

With the text in place and restrictions removed on accessing override messages, I can reduce my exception
section from what you saw earlier to just this:
EXCEPTION
WHEN OTHERS
THEN
showerr (SQLCODE, PLVmsg.text);
END;

When the SQLCODE is −1, PLVmsg.text is routed to the contents of the PL/SQL table in row −1 (and
does not use SQLERRM). When SQLCODE is −20,200, the value in row −202000 is returned. Finally, for all
other regular Oracle error numbers, PLVmsg obtains the text from SQLERRM.
The result is a dramatically cleaned−up exception section and an application in which all error text
management is performed in one place: the PLVmsg repository.

9.5.1 Using PLVmsg in PL/Vision
As mentioned earlier, the PLVexc packages relies on PLVmsg to obtain error message text. The
PLVmsg.text function is called by terminate_and_handle, which acts as a bridge between the
high−level handlers, such as recNgo, and the low−level handle procedure. The implementation of
terminate_and_handle is shown below:
PROCEDURE terminate_and_handle
(action_in IN VARCHAR2,
err_code_in IN INTEGER)
IS
BEGIN
PLVtrc.terminate;
handle
(PLVtrc.prevmod, err_code_in, action_in,
PLVmsg.text (err_code_in));
END;

The value passed in as err_code_in might be SQLCODE, or it might be some application−specific value.
Whatever its value, PLVmsg.text translates the error number into message text and passes that to the
low−level handler. The handle procedure then might display this string or store it in the PL/Vision log.
By calling PLVmsg.text at this point in the exception−handling architecture, PLVexc offers its users a lot
of flexibility. Suppose that when you first built your application, you didn't have time to work on error
messages. You took advantage of PLVexc, but ignored completely the PLVmsg package capabilities. In this
case, PLVmsg.text acted simply as a passthrough to SQLERRM. Somewhere down the line, however, you
decided to enhance the error messages for your application.
To accomplish this enhancement, you would not have to change your application. All of your exception
handlers that call the high−level PLVexc exception handlers are already calling PLVmsg.text. All you
have to do is store all of your message text in a database table and then call PLVmsg.load_from_dbms at
a good startup point for the application (in a When−New−Form−Instance trigger in an Oracle Forms−based
application or in the initialization section of a common package).
From that point on (and remember: without changing any of your code!), the new error text will be used in the
application.

9.5.1 Using PLVmsg in PL/Vision

298

[Appendix A] Appendix: PL/SQL Exercises
Special Notes on PLVmsg
Here are some factors to consider when working with PLVmsg:

The maximum size of a message is 2,000 bytes.

The number 100 and all negative numbers that are not between −20,000 and −20999 are considered to
be Oracle error codes.

The load_from_dbms is a useful example of the kind of code you need to write to transfer data
from a database table to a PL/SQL table −− even to the extent of allowing the user to specify the
relevant names. You should be able to easily adapt this PLVmsg procedure to your own purposes.

9.4 The Restriction Toggle

9.6 Implementing load_
from_dbms

Copyright (c) 2000 O'Reilly Associates. All rights reserved.

9.5.1 Using PLVmsg in PL/Vision

299

Chapter 9
PLVmsg: Single−Sourcing
PL/SQL Message Text

9.6 Implementing load_ from_dbms
The load_from_dbms procedure serves as a good example of a program for loading number−text
combinations from any database table into a PL/SQL table using dynamic SQL. Since you can specify the
table name, WHERE clause, and column names, you can load message text from multiple sources and for
multiple purposes. You can even copy this program, modify it, and use it in other programs.
The implementation of this procedure is shown in Example 9.2. It is explained in the next section. (continued)
Example 9.2: The Implementation of load_ from_dbms
PROCEDURE load_from_dbms
(table_in IN VARCHAR2,
where_clause_in IN VARCHAR2 := NULL,
code_col_in IN VARCHAR2 := 'error_code',
text_col_in IN VARCHAR2 := 'error_text')
IS
select_string PLV.max_varchar2%TYPE :=
'SELECT ' || code_col_in || ', ' || text_col_in ||
' FROM ' || table_in;
cur INTEGER;
error_code INTEGER;
error_text VARCHAR2(2000);
PROCEDURE set_minmax (code_in IN INTEGER) IS
BEGIN
IF min_row IS NULL OR min_row > code_in
THEN
v_min_row := code_in;
END IF;
IF max_row IS NULL OR max_row < code_in
THEN
v_max_row := code_in;
END IF;
END;
BEGIN
IF where_clause_in IS NOT NULL
THEN
select_string := select_string || ' WHERE ' || where_clause_in;
END IF;
cur := PLVdyn.open_and_parse (select_string);
DBMS_SQL.DEFINE_COLUMN (cur, 1, error_code);
DBMS_SQL.DEFINE_COLUMN (cur, 2, error_text, 2000);
PLVdyn.execute (cur);
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS (cur) = 0;
DBMS_SQL.COLUMN_VALUE (cur, 1, error_code);
DBMS_SQL.COLUMN_VALUE (cur, 2, error_text);

300

[Appendix A] Appendix: PL/SQL Exercises
set_minmax (error_code);
add_text (error_code, error_text);
END LOOP;
DBMS_SQL.CLOSE_CURSOR (cur);
END;

When performing dynamic SQL, you construct the SQL statement at runtime. In load_from_dbms, I
declare and initialize my SELECT string as follows:
select_string PLV.max_varchar2%TYPE :=
'SELECT ' || code_col_in || ', ' || text_col_in ||
' FROM ' || table_in;

Notice that I use all of the name arguments to the program to build the SELECT statement. There is nothing
hard coded here except for the mandatory syntax elements like SELECT and FROM. That assignment (which
takes place in the declaration section) covers the basic query.
What if the user provided a WHERE clause? The first line in the procedure's body adds the WHERE clause if
it is not NULL:
IF where_clause_in IS NOT NULL
THEN
select_string := select_string || ' WHERE ' || where_clause_in;
END IF;

Notice that you do not have to provide a WHERE keyword. That is inserted automatically by the program.
My string is ready to be parsed, so I call the PLVdyn open_and_parse procedure to take those two steps.
Then I define the two columns (number and string) that are specified in the SELECT statement:
cur := PLVdyn.open_and_parse (select_string);
DBMS_SQL.DEFINE_COLUMN (cur, 1, error_code);
DBMS_SQL.DEFINE_COLUMN (cur, 2, error_text, 2000);

Now the cursor is fully defined and ready to be executed. The final step in load_from_dbms is to run the
equivalent of a cursor FOR loop: for every record dynamically fetched, add the text to the table and update the
high and low indicators:
PLVdyn.execute (cur);
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS (cur) = 0;
DBMS_SQL.COLUMN_VALUE (cur, 1, error_code);
DBMS_SQL.COLUMN_VALUE (cur, 2, error_text);
set_minmax (error_code);
add_text (error_code, error_text);
END LOOP;
DBMS_SQL.CLOSE_CURSOR (cur);

I fetch a row (exiting immediately if nothing is returned). I then extract the values into local variables with
COLUMN_VALUE. Following that, I update the minimum and maximum row numbers and, finally, add the
text to the PL/SQL table using that same add_text program that users of PLVmsg would use to add text to
the table. When I am done with the loop, I close the cursor.
To make the main body of the procedure as readable as possible, I create a local procedure (set_minmax) to
keep track of the lowest and highest row numbers used in the PL/SQL table. This is necessary in releases of
PL/SQL earlier than 2.3 since there is no way to query the PL/SQL runtime engine for this information. The
local procedure, set_minmax, also serves to hide this annoying level of detail and weakness in PL/SQL
table design. When you upgrade to PL/SQL Release 2.3 or above, you can just strip out this code.
301