encapsulating bulk pl/sql exceptions

One of the features of bulk PL/SQL processing is the SAVE EXCEPTIONS extension to FORALL. This clause has been available since Oracle 9i and instructs Oracle to skip any exceptions it might encounter during a FORALL DML operation. It enables us to continue processing a batch of data to completion rather than fail the entire statement. For example, we might be inserting 500 records using FORALL and if 1 record raises an exception, the remaining 499 rows will be successfully loaded. The bad row will be "set aside" for post-processing.

Most systems have an error-logging mechanism to write details of processing failures to files, tables or even queues. Most commonly this mechanism comprises a centralised error package and error-logging table, which will typically contain information such as timestamp, business date, failing package/procedure/function, some details on the nature of the exception (such as SQLERRM) and the keys to the source data that caused the exception (where applicable). Exception handling in these cases is quite simple: each procedure makes a single call to the error package, which in turns writes the exception details to the table/file/queue and optionally raises it.

One of the "features" of the SAVE EXCEPTIONS clause, however, is that the exception handling is quite code-intensive (i.e. we need to write quite a few lines of code to process the exceptions data). An example of this can be found in this oracle-developer.net article on 9i bulk PL/SQL features. It therefore makes sense for us to try to encapsulate this processing in an error-logging package and this article will suggest two methods for this, using the following Oracle features:

It is assumed that readers are familiar with these features of Oracle. For some background on these techniques, begin by reading the oracle-developer.net articles on ANYDATA and type enhancements in 9i (in particular the section on type polymorphism).

The examples in this article have been tested on 9i Release 2 (9.2) and should also work on any version of 10g.

setup: a simple error logger

As stated, we will be encapsulating FORALL .. SAVE EXCEPTIONS handling in an error package. We will start by building the error logging application that we wish to extend to include the new bulk handler. Like most systems, this package will record processing exceptions in an errors table. To keep this as simple as possible, we will exclude the rollback/raise management that such an error package should ideally encapsulate. We will begin by creating a simple error logging table (note that keys, constraints, indexes etc are deliberately ignored as they add nothing to the examples).

SQL> CREATE TABLE errors
  2  ( package_owner  VARCHAR2(30)
  3  , package_name   VARCHAR2(30)
  4  , procedure_name VARCHAR2(30)
  5  , action         VARCHAR2(100)
  6  , business_date  DATE
  7  , business_key   VARCHAR2(1000)
  8  , error_ts       TIMESTAMP
  9  , error_msg      VARCHAR2(4000)
 10  );

Table created.

Now we can create the simple error package. At this stage it doesn't handle bulk exceptions.

SQL> CREATE PACKAGE error AS
  2     PROCEDURE log( p_owner         IN errors.package_owner%TYPE,
  3                    p_package       IN errors.package_name%TYPE,
  4                    p_procedure     IN errors.procedure_name%TYPE,
  5                    p_action        IN errors.action%TYPE,
  6                    p_business_date IN errors.business_date%TYPE,
  7                    p_business_key  IN errors.business_key%TYPE DEFAULT NULL,
  8                    p_error         IN VARCHAR2 DEFAULT NULL );
  9  END;
 10  /

Package created.

So far we have a single procedure to log a single exception. The implementation of this logging procedure will typically be constructed as follows.

SQL> CREATE PACKAGE BODY error AS
  2
  3     PROCEDURE log( p_owner         IN errors.package_owner%TYPE,
  4                    p_package       IN errors.package_name%TYPE,
  5                    p_procedure     IN errors.procedure_name%TYPE,
  6                    p_action        IN errors.action%TYPE,
  7                    p_business_date IN errors.business_date%TYPE,
  8                    p_business_key  IN errors.business_key%TYPE DEFAULT NULL,
  9                    p_error         IN VARCHAR2 DEFAULT NULL ) IS
 10
 11        v_error errors.error_msg%TYPE := NVL(p_error,SQLERRM);
 12
 13        PRAGMA AUTONOMOUS_TRANSACTION;
 14
 15     BEGIN
 16
 17        INSERT INTO errors
 18           ( package_owner, package_name, procedure_name, action,
 19             business_date, business_key, error_ts, error_msg )
 20        VALUES
 21           ( p_owner, p_package, p_procedure, p_action,
 22             p_business_date, p_business_key, SYSTIMESTAMP, v_error );
 23
 24        COMMIT;
 25
 26     END log;
 27
 28  END error;
 29  /

Package body created.

Before we move onto the main subject of this article, we can see how the error logger might typically be used in a "traditional" PL/SQL data processing routine. Note that the following anonymous block is an approximation of a daily processing procedure. Elements such as business date will usually be passed as parameters.

SQL> DECLARE
  2
  3     v_package       errors.package_name%TYPE   := 'ANON. BLOCK';
  4     v_procedure     errors.procedure_name%TYPE := 'ANON. BLOCK';
  5     v_action        errors.action%TYPE;
  6     v_business_date errors.business_date%TYPE  := TRUNC(SYSDATE)-1;
  7     v_business_key  errors.business_key%TYPE;
  8
  9  BEGIN
 10
 11     v_action := 'Process source data';
 12     FOR r IN ( SELECT object_id   AS key_attr1
 13                ,      object_name AS key_attr2
 14                ,      object_type AS data_attr1
 15                FROM   user_objects )
 16     LOOP
 17
 18        v_action := 'Assign business key';
 19        v_business_key := r.key_attr1 || ',' || r.key_attr2;
 20
 21        v_action := 'Transformation and business rules';
 22        r.data_attr1 := RPAD('Oops',4000);  --<-- oh dear
 23
 24     END LOOP;
 25
 26  EXCEPTION
 27     WHEN OTHERS THEN
 28        error.log( p_owner         => USER,
 29                   p_package       => v_package,
 30                   p_procedure     => v_procedure,
 31                   p_action        => v_action,
 32                   p_business_date => v_business_date,
 33                   p_business_key  => v_business_key );
 34
 35        ROLLBACK; --<-- error package should encapsulate this...
 36        RAISE;    --<-- error package should also encapsulate this...
 37  END;
 38  /
DECLARE
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at line 36

We can see that the exception handling is simplified by the error package, especially if it encapsulates ROLLBACK and RAISE logic as well (which typically it should though for simplicity we've ignored it for this article). The aim is to hand-off all exception handling mechanics to the error package.

Finally, using Tom Kyte's print_table procedure to make the format easier to read, we can see our single logged exception as follows.

SQL> exec print_table('SELECT * FROM errors');

PACKAGE_OWNER   : SCOTT
PACKAGE_NAME    : ANON. BLOCK
PROCEDURE_NAME  : ANON. BLOCK
ACTION          : Transformation and business rules
BUSINESS_DATE   : 25-jul-2007 00:00:00
BUSINESS_KEY    : 33944,ANOTHER_SUBTYPE_OT
ERROR_TS        : 26-JUL-07 18.24.37.640000
ERROR_MSG       : ORA-06502: PL/SQL: numeric or value error: character string buffer too small
-----------------

PL/SQL procedure successfully completed.

Now that we have some context (i.e. we have an error logging package in place), we can move on to how we might log errors in bulk.

encapsulating bulk exceptions

Using the error logging framework that we have created above, we could quite simply decide to manage exception logging in every processing routine we write. We would need to code a loop through SQL%BULK_EXCEPTIONS, determine the bad business data and make associated calls to the ERROR.LOG procedure to record the exceptional data. However, this would be a lot of repetition across multiple procedures and, as stated in the introduction, this can be code-intensive. It is far better, therefore, to encapsulate this extension of exception handling, as we shall now see.

There are two primary elements we need to consider for this encapsulation. First, we need to process the SQL%BULK_EXCEPTIONS pseudo-array that Oracle populates following an exception with a FORALL statement (with or without SAVE EXCEPTIONS). Second, we use the metadata in this pseudo-array to determine the locations of the exceptional business data and the exceptions themselves. This means that an encapsulated error logger will need to accept an array of business data in any format. It is this requirement that leads us to the two Oracle features described in the introduction; namely ANYDATA and polymorphism. We will begin with ANYDATA.

encapsulating with anydata

ANYDATA is a built-in type that has been available since Oracle 9i. We can use ANYDATA as a container to store any form of structured data for which we have a named SQL type (either built-in or user-defined). We can exploit this feature to encapsulate generic collection handling (such as that required for bulk exceptions).

We will add the following components to our existing error logging application:

We will begin by creating the generic collection types that will "assist" with the encapsulation as follows.

SQL> CREATE OR REPLACE TYPE varchar2_ntt AS TABLE OF VARCHAR2(4000);
  2  /

Type created.

SQL> CREATE OR REPLACE TYPE number_ntt AS TABLE OF NUMBER;
  2  /

Type created.

It will become clear how these are used when we add our encapsulation to the ERROR package. First we will add an overloaded LOG procedure to the package specification. For brevity, the original LOG procedure is removed from the output (though it still exists of course).

SQL> CREATE OR REPLACE PACKAGE error AS
  2
  3     PROCEDURE log( ...snip...
 10
 11     bulk_exceptions EXCEPTION;
 12     PRAGMA EXCEPTION_INIT(bulk_exceptions, -24381);
 13
 14     PROCEDURE log( p_owner         IN errors.package_owner%TYPE,
 15                    p_package       IN errors.package_name%TYPE,
 16                    p_procedure     IN errors.procedure_name%TYPE,
 17                    p_action        IN errors.action%TYPE,
 18                    p_business_date IN errors.business_date%TYPE,
 19                    p_business_data IN ANYDATA );
 20
 21  END;
 22  /

Package created.

This overloaded LOG procedure takes similar parameters to the original, single-row version, with the key difference being that we now have the ability to pass in a collection of business data via the ANYDATA type. This business data will be held in the collection that we are processing when FORALL .. SAVE EXCEPTIONS is invoked (i.e. when we hit an exception). We have also encapsulated the exception that Oracle raises when this happens (ORA-24381).

We can now implement the overloaded LOG procedure by re-creating the package body as follows. Again, the original LOG procedure is removed from the output for brevity.

SQL> CREATE OR REPLACE PACKAGE BODY error AS
  2
  3     PROCEDURE log( ...snip...
 27
 28     PROCEDURE log( p_owner         IN errors.package_owner%TYPE,
 29                    p_package       IN errors.package_name%TYPE,
 30                    p_procedure     IN errors.procedure_name%TYPE,
 31                    p_action        IN errors.action%TYPE,
 32                    p_business_date IN errors.business_date%TYPE,
 33                    p_business_data IN ANYDATA ) IS
 34
 35        v_plsql          VARCHAR2(1024);                 --<-- dynamic PL/SQL block
 36        v_type_name      VARCHAR2(61);                   --<-- underlying type name
 37        nt_error_indices number_ntt   := number_ntt();   --<-- offsets to bad data
 38        nt_business_keys varchar2_ntt := varchar2_ntt(); --<-- keys of bad data
 39
 40     BEGIN
 41
 42        /*
 43        || Determine the details of the collection type contained within the
 44        || ANYDATA parameter...
 45        */
 46        v_type_name := p_business_data.gettypename;
 47
 48        /*
 49        || Determine where in our business data collection the bad records are...
 50        */
 51        FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
 52           nt_error_indices.EXTEND;
 53           nt_error_indices(nt_error_indices.LAST) := SQL%BULK_EXCEPTIONS(i).error_index;
 54        END LOOP;
 55
 56        /*
 57        || Build a PL/SQL block to accept the ANYDATA and a collection of error indices.
 58        || This will extract the collection from the ANYDATA instance and probe it
 59        || with the error indices from SQL%BULK_EXCEPTIONS, building up a return
 60        || collection of the bad data keys...
 61        */
 62        v_plsql := 'DECLARE '
 63                || '   n PLS_INTEGER; '
 64                || '   c ' || v_type_name || '; '
 65                || '   a SYS.ANYDATA  := :b1; '
 66                || '   x number_ntt   := :b2; '
 67                || '   o varchar2_ntt := varchar2_ntt(); '
 68                || 'BEGIN'
 69                || '   n := a.GetCollection(c); '
 70                || '   FOR i IN x.FIRST .. x.LAST LOOP '
 71                || '      o.EXTEND; '
 72                || '      o(o.LAST) := c(x(i)).print_key; '
 73                || '   END LOOP; '
 74                || '   :b3 := o; '
 75                || 'END; ';
 76
 77        /*
 78        || Execute the PL/SQL string to return the bad data keys...
 79        */
 80        EXECUTE IMMEDIATE v_plsql USING IN  p_business_data,
 81                                        IN  nt_error_indices,
 82                                        OUT nt_business_keys;
 83
 84
 85        /*
 86        || Now we can log the errors...
 87        */
 88        FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
 89
 90           error.log( p_owner         => p_owner,
 91                      p_package       => p_package,
 92                      p_procedure     => p_procedure,
 93                      p_action        => p_action,
 94                      p_business_date => p_business_date,
 95                      p_business_key  => nt_business_keys(i),
 96                      p_error         => SQLERRM(-1*SQL%BULK_EXCEPTIONS(i).error_code) );
 97
 98        END LOOP;
 99
100     END log;
101
102  END error;
103  /

Package body created.

Note the following elements of the bulk exceptions wrapper above:

We now have an extension to our error logger/handler that enables us to work with bulk PL/SQL and capture the exceptions without having to repeatedly code a complicated exception block. To test this, we will build a dummy customer table and load it with bulk PL/SQL, ensuring we have some "bad" data. We will begin by creating a CUSTOMERS table as follows.

SQL> CREATE TABLE customers
  2  ( customer_id   NUMBER PRIMARY KEY
  3  , first_name    VARCHAR2(30)
  4  , last_name     VARCHAR2(50)
  5  , start_date    DATE
  6  );

Table created.

One of the pre-requisites of being able to pass around collections of records with ANYDATA is that we use SQL object and collection types (i.e. types creates using the CREATE TYPE... syntax). Most developers will be familiar with using PL/SQL types (records and associative arrays that are declared in a package or procedure) for array processing. It is not much of a diversion to use objects and collections and, in some cases, using the SQL types provides greater flexibility than the PL/SQL-only type structures. Given this, we will now create a customer object to define a "record" of CUSTOMERS source data as follows.

SQL> CREATE TYPE customer_ot AS OBJECT
  2  ( customer_id   NUMBER
  3  , first_name    VARCHAR2(30)
  4  , last_name     VARCHAR2(50)
  5  , start_date    DATE
  6  , MEMBER FUNCTION print_key RETURN VARCHAR2
  7  );
  8  /

Type created.
SQL> CREATE TYPE BODY customer_ot AS
  2     MEMBER FUNCTION print_key RETURN VARCHAR2 IS
  3     BEGIN
  4        RETURN TO_CHAR(SELF.customer_id);
  5     END;
  6  END;
  7  /

Type body created.

Note that we have included a member function named PRINT_KEY. This is for convenience. Remember that the dynamic PL/SQL block in the error logger will invoke this to extract the business keys of the exceptional data. To work with multiple records of customer data, we must create a collection type of the customer "record", which we do as follows.

SQL> CREATE TYPE customer_ntt AS TABLE OF customer_ot;
  2  /

Type created.

We now have the elements we require to test the encapsulated FORALL .. SAVE EXCEPTIONS handler. The following anonymous block represents a batch load of CUSTOMERS. We will fetch the source data first and then manufacture two duplicate records to ensure our subsequent FORALL .. SAVE EXCEPTIONS construct hits some exceptions. Note that we would usually expect this load to include some complex transformations between the fetch and load stages (else we would be using bulk SQL and not PL/SQL), but these are assumed and omitted for brevity.

SQL> DECLARE
  2
  3     v_package        errors.package_name%TYPE   := 'ANYDATA_ENCAPSULATION';
  4     v_procedure      errors.procedure_name%TYPE := 'CUSTOMER_LOAD_EG';
  5     v_action         errors.action%TYPE;
  6     v_business_date  errors.business_date%TYPE := TRUNC(SYSDATE)-1;
  7     nt_customer_data customer_ntt := customer_ntt();
  8
  9  BEGIN
 10
 11     v_action := 'Fetch source data';
 12     SELECT customer_ot(
 13               object_id,   --<-- customer_id
 14               object_type, --<-- first_name
 15               object_name, --<-- last_name
 16               created      --<-- start_date
 17               )
 18     BULK COLLECT INTO nt_customer_data
 19     FROM   all_objects
 20     WHERE  ROWNUM < 9;
 21
 22     /*
 23     || Add two duplicates to ensure we hit SAVE EXCEPTIONS...
 24     */
 25     FOR i IN 1 .. 2 LOOP
 26        nt_customer_data.EXTEND;
 27        nt_customer_data(nt_customer_data.LAST) := nt_customer_data(i);
 28     END LOOP;
 29
 30     v_action := 'Customer load';
 31     FORALL i IN 1 .. nt_customer_data.COUNT SAVE EXCEPTIONS
 32        INSERT INTO (SELECT customer_ot( customer_id,
 33                                         first_name,
 34                                         last_name,
 35                                         start_date )
 36                     FROM   customers)
 37        VALUES (nt_customer_data(i));
 38
 39  EXCEPTION
 40     WHEN error.bulk_exceptions THEN
 41        error.log( p_owner         => USER,
 42                   p_package       => v_package,
 43                   p_procedure     => v_procedure,
 44                   p_action        => v_action,
 45                   p_business_date => v_business_date,
 46                   p_business_data => ANYDATA.convertCollection(nt_customer_data) );
 47
 48        ROLLBACK; --<-- error package should encapsulate this...
 49        RAISE;    --<-- error package should also encapsulate this...
 50  END;
 51  /
ERROR:
ORA-24381: error(s) in array DML
ORA-06512: at line 49



Warning: PL/SQL compilation errors.

We can see from the error messages that we found some exceptions during our processing. Before we examine these and determine whether our encapsulation works, it might be helpful to comment on some of the syntax in the load example. Using SQL objects/collections, rather than PL/SQL records/arrays, requires that we modify our syntax slightly as follows:

Remember that we rigged two duplicate rows in our example CUSTOMERS load. We can now query our ERRORS table to see if both exceptions were logged. Note that this table was truncated before running the example load above.

SQL> exec print_table('SELECT * FROM errors');

PACKAGE_OWNER      : SCOTT
PACKAGE_NAME       : ANYDATA_ENCAPSULATION
PROCEDURE_NAME     : CUSTOMER_LOAD_EG
ACTION             : Customer load
BUSINESS_DATE      : 31-jul-2007 00:00:00
BUSINESS_KEY       : 17286
ERROR_TS           : 01-AUG-07 18.25.11.859000
ERROR_MSG          : ORA-00001: unique constraint (.) violated
-----------------
PACKAGE_OWNER      : SCOTT
PACKAGE_NAME       : ANYDATA_ENCAPSULATION
PROCEDURE_NAME     : CUSTOMER_LOAD_EG
ACTION             : Customer load
BUSINESS_DATE      : 31-jul-2007 00:00:00
BUSINESS_KEY       : 7559
ERROR_TS           : 01-AUG-07 18.25.11.859000
ERROR_MSG          : ORA-00001: unique constraint (.) violated
-----------------

PL/SQL procedure successfully completed.

To summarise, therefore, the key elements of the encapsulation of FORALL .. SAVE EXCEPTIONS with ANYDATA are as follows:

encapsulating with type substitution

In the previous section, we saw that ANYDATA enables us to encapsulate the processing of the SQL%BULK_EXCEPTIONS pseudo-array and the related business data. One of the drawbacks of the ANYDATA method is that it requires dynamic PL/SQL that will be generated and executed every time the error logger is called. An alternative to this method is to use type substitution (known as polymorphism) and we will now build an example of how we might implement this.

Briefly, polymorphism enables us to create a hierarchy of types and use any of the subtypes wherever their respective supertypes are expected. We can take advantage of this to build a data-loading framework that utilises a single supertype as a consistent parameter type and yet uses underlying types of different structures for specific load targets. In relation to our encapsulated FORALL .. SAVE EXCEPTIONS handler, we will create a single "generic" load type as follows.

SQL> CREATE TYPE bulk_load_ot AS OBJECT
  2  ( generic_id NUMBER
  3  , MEMBER FUNCTION print_key RETURN VARCHAR2
  4  ) NOT FINAL NOT INSTANTIABLE;
  5  /

Type created.

Note how we define this type as being NOT FINAL and NOT INSTANTIABLE. The former means we have not yet completed the implementation of the type hierarchy (of which BULK_LOAD_OT is a supertype) and the latter means that we will not be able to directly use this type for variables in our PL/SQL programs. We create this type with a single ID attribute that will be inherited by all subtypes and also a PRINT_KEY member function, as before, that will print the current value of the business key contained within the data structure.

As we have a member function, we must also have a type body. Often with polymorphism, we might exclude a type body in the supertype and instead include an overriding function in every subtype. For simplicity, we will create one function in the BULK_LOAD_OT supertype as follows and allow all subtypes to inherit and use this function.

SQL> CREATE TYPE BODY bulk_load_ot AS
  2     MEMBER FUNCTION print_key RETURN VARCHAR2 IS
  3     BEGIN
  4        RETURN TO_CHAR(SELF.generic_id);
  5     END;
  6  END;
  7  /

Type body created.

Finally, because we are dealing with collections of data and not single records, we will create a collection type based on our BULK_LOAD_OT object.

SQL> CREATE TYPE bulk_load_ntt AS TABLE OF bulk_load_ot;
  2  /

Type created.

We now have the elements we require to implement another version of the ERROR.LOG procedure. The ERROR package specification is as follows (the original LOG procedure definition is cut for brevity).

SQL> CREATE OR REPLACE PACKAGE error AS
  2
  3     PROCEDURE log( ...snip...
 10
 11     bulk_exceptions EXCEPTION;
 12     PRAGMA EXCEPTION_INIT(bulk_exceptions, -24381);
 13
 14     PROCEDURE log( p_owner         IN errors.package_owner%TYPE,
 15                    p_package       IN errors.package_name%TYPE,
 16                    p_procedure     IN errors.procedure_name%TYPE,
 17                    p_action        IN errors.action%TYPE,
 18                    p_business_date IN errors.business_date%TYPE,
 19                    p_business_data IN bulk_load_ntt );
 20
 21  END;
 22  /

Package created.

The specification differs from the ANYDATA version only by the type of the p_business_data parameter. This time, we use the BULK_LOAD_NTT type for the business data, which is a collection type based on BULK_LOAD_OT. As we will see later, this means that the collection can contain data of any subtype in a type hierarchy created under BULK_LOAD_OT. Before we can see this, we must create the package body for our new LOG overload as follows.

SQL> CREATE OR REPLACE PACKAGE BODY error AS
  2
  3     PROCEDURE log( ...snip...
 27
 28     PROCEDURE log( p_owner         IN errors.package_owner%TYPE,
 29                    p_package       IN errors.package_name%TYPE,
 30                    p_procedure     IN errors.procedure_name%TYPE,
 31                    p_action        IN errors.action%TYPE,
 32                    p_business_date IN errors.business_date%TYPE,
 33                    p_business_data IN bulk_load_ntt ) IS
 34
 35        v_error_index PLS_INTEGER;
 36        v_error_code  PLS_INTEGER;
 37
 38     BEGIN
 39
 40        /*
 41        || Simply log the errors...
 42        */
 43        FOR i IN 1 .. SQL%BULK_EXCEPTIONS.COUNT LOOP
 44
 45           v_error_index := SQL%BULK_EXCEPTIONS(i).error_index;
 46           v_error_code  := SQL%BULK_EXCEPTIONS(i).error_code;
 47
 48           error.log( p_owner         => p_owner,
 49                      p_package       => p_package,
 50                      p_procedure     => p_procedure,
 51                      p_action        => p_action,
 52                      p_business_date => p_business_date,
 53                      p_business_key  => p_business_data(v_error_index).print_key(),
 54                      p_error         => SQLERRM(-1 * v_error_code) );
 55
 56        END LOOP;
 57
 58     END log;
 59
 60  END error;
 61  /

Package body created.

We can see immediately that this implementation is far simpler than the ANYDATA example from earlier. We are working with a known collection type (BULK_LOAD_NTT) and we also know that each "record" in this collection will have a PRINT_KEY member function. These two factors make the processing of the SQL%BULK_EXCEPTIONS pseudo-array and the probing of the business data collection much easier. The resulting code is therefore short, simple and self-explanatory.

We are now ready to test our implementation. We can see that the ERROR.LOG encapsulation expects an instance of BULK_LOAD_NTT. By using type substitution, we can pass in a collection of any subtype that is defined under BULK_LOAD_OT. Using our CUSTOMERS example from earlier, we will now create a CUSTOMER_OT subtype under BULK_LOAD_OT as follows.

SQL> CREATE TYPE customer_ot UNDER bulk_load_ot
  2  ( first_name    VARCHAR2(30)
  3  , last_name     VARCHAR2(50)
  4  , start_date    DATE
  5  );
  6  /

Type created.

This subtype inherits the GENERIC_ID attribute and PRINT_KEY member function from the BULK_LOAD_OT supertype. It represents a "record" of customer-specific data, yet can be used wherever a BULK_LOAD_OT record is expected. We will test this using the same example that we used to demonstrate the ANYDATA method. In other words, we will bulk fetch some "source" data, manufacture two exceptions and load the CUSTOMERS table using FORALL .. SAVE EXCEPTIONS.

SQL> DECLARE
  2
  3     v_package        errors.package_name%TYPE   := 'SUBTYPE_ENCAPSULATION';
  4     v_procedure      errors.procedure_name%TYPE := 'CUSTOMER_LOAD_EG';
  5     v_action         errors.action%TYPE;
  6     v_business_date  errors.business_date%TYPE := TRUNC(SYSDATE)-1;
  7     nt_customer_data bulk_load_ntt := bulk_load_ntt();
  8
  9  BEGIN
 10
 11     v_action := 'Fetch source data';
 12     SELECT customer_ot(
 13               object_id,   --<-- generic_id in lieu of customer_id
 14               object_type, --<-- first_name
 15               object_name, --<-- last_name
 16               created      --<-- start_date
 17               )
 18     BULK COLLECT INTO nt_customer_data
 19     FROM   all_objects
 20     WHERE  ROWNUM < 10;
 21
 22     /*
 23     || Add two duplicates to ensure we hit SAVE EXCEPTIONS...
 24     */
 25     FOR i IN 1 .. 2 LOOP
 26        nt_customer_data.EXTEND;
 27        nt_customer_data(nt_customer_data.LAST) := nt_customer_data(i);
 28     END LOOP;
 29
 30     v_action := 'Customer load';
 31     FORALL i IN 1 .. nt_customer_data.COUNT SAVE EXCEPTIONS
 32        INSERT INTO (SELECT customer_ot( customer_id,
 33                                         first_name,
 34                                         last_name,
 35                                         start_date )
 36                     FROM   customers)
 37        VALUES (TREAT(nt_customer_data(i) AS customer_ot));
 38
 39  EXCEPTION
 40     WHEN error.bulk_exceptions THEN
 41        error.log( p_owner         => USER,
 42                   p_package       => v_package,
 43                   p_procedure     => v_procedure,
 44                   p_action        => v_action,
 45                   p_business_date => v_business_date,
 46                   p_business_data => nt_customer_data );
 47
 48        ROLLBACK; --<-- error package should encapsulate this...
 49        RAISE;    --<-- error package should also encapsulate this...
 50  END;
 51  /
ERROR:
ORA-24381: error(s) in array DML
ORA-06512: at line 49


Warning: PL/SQL compilation errors.

As before, our two exceptions generated the Oracle error we expected. There are a few small differences between this example and the ANYDATA example, most notably the following:

Finally, we can confirm that our encapsulation works by checking the ERRORS table (this was truncated before the previous example was executed).

SQL> exec print_table('SELECT * FROM errors');

PACKAGE_OWNER      : SCOTT
PACKAGE_NAME       : SUBTYPE_ENCAPSULATION
PROCEDURE_NAME     : CUSTOMER_LOAD_EG
ACTION             : Customer load
BUSINESS_DATE      : 31-jul-2007 00:00:00
BUSINESS_KEY       : 17286
ERROR_TS           : 01-AUG-07 18.27.49.625000
ERROR_MSG          : ORA-00001: unique constraint (.) violated
-----------------
PACKAGE_OWNER      : SCOTT
PACKAGE_NAME       : SUBTYPE_ENCAPSULATION
PROCEDURE_NAME     : CUSTOMER_LOAD_EG
ACTION             : Customer load
BUSINESS_DATE      : 31-jul-2007 00:00:00
BUSINESS_KEY       : 7559
ERROR_TS           : 01-AUG-07 18.27.49.625000
ERROR_MSG          : ORA-00001: unique constraint (.) violated
-----------------

PL/SQL procedure successfully completed.

In summary, the key elements of the type substitution method for encapsulating bulk exceptions are listed below.

summary

We have seen two methods for encapsulating SQL%BULK_EXCEPTIONS and logging the underlying business information during exceptions handling. We have managed to avoid lengthy and repetitive exception-handling in each load process we write. Instead, we hand off our business data to a generic utility that does this for us.

The ANYDATA method enables us to pass any format of data (defined as collections of SQL object types) to the API, whereas type substitution limits us to whatever we define in the object type hierarchy. The latter method, however, is much simpler to work with and the resulting implementation is much cleaner. Both methods require that we divert away from PL/SQL records and arrays and instead use SQL object types and collections in their place. As stated, however, the SQL types can provide much greater flexibility and scope than their PL/SQL-only counterparts.

further reading

For more information on FORALL .. SAVE EXCEPTIONS, ANYDATA and type substitution, follow the links provided earlier in this article. For a general discussion of object-relational features in Oracle, see the online Application Developer's Guide - Object-Relational Features.

source code

The source code for the examples in this article can be downloaded from here.

Adrian Billington, July 2007

Back to Top