You are on page 1of 22

oracle-developer.

net
Home Art icle s

Search

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 11g New Features 10g New Features 9i New Features 8i New Features Misce llane o us 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 t his o racle d e ve lo p e r.ne t art icle 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: ANYDATA; and type substitution (" polymorphism" ). It is assumed that readers are familiar with these features of Oracle. For some background on these techniques, begin by re a d in g the oracle- developer.net articles on AN YD ATA and t yp e e nhance me nt s 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. PDFmyURL.com

Utilities Links Subscribe Disclaimer

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 3 4 5 6 7 8 9 10 ( package_owner , package_name , action , business_date , business_key , error_ts , error_msg ); VARCHAR2 (30) VARCHAR2 (30) VARCHAR2 (100) DATE VARCHAR2 (1000) TIMESTAMP VARCHAR2 (4000)

, procedure_name VARCHAR2 (30)

Table created. Now we can create the simple error package. At this stage it d o e sn' t handle bulk exceptions.

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

p_business_date IN errors.business_date% TYPE ,

Package created.

PDFmyURL.com

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

p_business_date IN errors.business_date% TYPE ,

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. PDFmyURL.com

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

v_business_date errors.business_date% TYPE

p_business_date => v_business_date,

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 p rint _t ab le 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 PACKAGE_NAME PROCEDURE_NAME ACTION BUSINESS_DATE BUSINESS_KEY ERROR_TS ERROR_MSG

: SCOTT : ANON. BLOCK : ANON. BLOCK : Transformation and business rules : 25-jul-2007 00:00:00 : 33944,ANOTHER_SUBTYPE_OT : 26-JUL-07 18.24.37.640000 : 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 PDFmyURL.com

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 f o rmat . 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.

e ncapsulat ing wit h anydat a


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: a generic collection type of VARCHAR2(4000) to be used in the implementation; a generic collection type of NUMBER to be used in the implementation; and an overloaded ERROR.LOG procedure to accept an ANYDATA parameter (of the business data we were processing at the time of failure). 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 PDFmyURL.com

exists of course).

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

p_business_date IN errors.business_date% TYPE , p_business_data IN ANYDATA );

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 27 28 29 30 31 PROCEDURE log ( p_owner p_package p_procedure p_action IN errors.package_owner% TYPE , IN errors.package_name% TYPE , IN errors.procedure_name% TYPE , IN errors.action% TYPE , PDFmyURL.com PROCEDURE log ( ...snip...

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 /* /* /* BEGIN v_plsql v_type_name

p_business_date IN errors.business_date% TYPE , p_business_data IN ANYDATA ) IS VARCHAR2 (1024); VARCHAR2 (61); := number_ntt(); --<-- dynamic PL/SQL block --<-- underlying type name --<-- offsets to bad data

nt_error_indices number_ntt

nt_business_keys varchar2_ntt := varchar2_ntt(); --<-- keys of bad data

|| Determine the details of the collection type contained within the || ANYDATA parameter... */ v_type_name := p_business_data .gettypename ;

|| Determine where in our business data collection the bad records are... */ FOR i IN 1 .. SQL%BULK_EXCEPTIONS . COUNT LOOP nt_error_indices. EXTEND ; nt_error_indices(nt_error_indices. LAST ) := SQL%BULK_EXCEPTIONS (i). error_index ; END LOOP ;

|| Build a PL/SQL block to accept the ANYDATA and a collection of error indices. || This will extract the collection from the ANYDATA instance and probe it || with the error indices from SQL%BULK_EXCEPTIONS, building up a return || collection of the bad data keys... */ v_plsql := 'DECLARE ' || ' || ' || ' || ' || ' || ' || ' n PLS_INTEGER; ' c ' || v_type_name || '; ' a SYS.ANYDATA x number_ntt := :b1; ' := :b2; '

o varchar2_ntt := varchar2_ntt(); ' n := a.GetCollection(c); ' FOR i IN x.FIRST .. x.LAST LOOP ' PDFmyURL.com

|| 'BEGIN'

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 END error; / END log ; /* /*

|| ' || ' || ' || '

o.EXTEND; ' o(o.LAST) := c(x(i)).print_key; ' END LOOP; ' :b3 := o; '

|| 'END; ';

|| Execute the PL/SQL string to return the bad data keys... */ EXECUTE IMMEDIATE v_plsql USING IN IN p_business_data, nt_error_indices,

OUT nt_business_keys;

|| Now we can log the errors... */ FOR i IN 1 .. SQL%BULK_EXCEPTIONS . COUNT LOOP error. log ( p_owner p_package p_procedure p_action p_business_key p_error END LOOP ; => p_owner, => p_package, => p_procedure, => p_action, => nt_business_keys(i), => SQLERRM (-1* SQL%BULK_EXCEPTIONS (i). error_code ) );

p_business_date => p_business_date,

Package body created. Note the following elements of the bulk exceptions wrapper above:

PDFmyURL.com

Line 4 6 : we determine the name of the collection type that defines the data " inside" the ANYDATA instance. We will need to use this in a dynamic PL/SQL block to access the ANYDATA; Line s 51- 54 : we set- aside the offsets of the elements in the business data collection that raised exceptions. This is because the Native Dynamic PL/SQL block that follows cannot reference SQL% BULK_EXCEPTIONS in this context and will need some way of identifying these index values; Line s 6 2- 75: we build a Native Dynamic PL/SQL block to retrieve the exceptional business keys from the ANYDATA instance. We start by retrieving the ANYDATA instance into a variable of the correct underlying collection type. Using the error index collection we built earlier, the dynamic block loops through the business collection and derives the keys of the " bad" business data. These keys are added to a collection to be passed as an OUT bind variable; Line s 72: we ensure that all collection types used in FORALL .. SAVE EXCEPTIONS constructs have a PRINT_KEY() member function to simlify the retrieval of business key values; Line s 80- 82: we execute the dynamic PL/SQL block and receive a collection of business keys for " bad" records as an OUT parameter; and Line s 88- 98: we process the BULK_EXCEPTIONS pseudo- array and invoke the original ERROR.LOG procedure for each exception we encounter, including all of the critical information we have extracted (including business keys). 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 3 4 5 6 ( customer_id , first_name , last_name , start_date ); NUMBER PRIMARY KEY VARCHAR2 (30) VARCHAR2 (50) DATE

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.

PDFmyURL.com

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

, MEMBER FUNCTION print_key RETURN VARCHAR2

Type created.

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

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.

PDFmyURL.com

SQL> DECLARE 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 EXCEPTION PDFmyURL.com FROM customers) VALUES (nt_customer_data(i)); v_action := 'Customer load'; FORALL i IN 1 .. nt_customer_data. COUNT SAVE EXCEPTIONS INSERT INTO ( SELECT customer_ot( customer_id, first_name, last_name, start_date ) /* || Add two duplicates to ensure we hit SAVE EXCEPTIONS... */ FOR i IN 1 .. 2 LOOP nt_customer_data. EXTEND ; nt_customer_data(nt_customer_data. LAST ) := nt_customer_data(i); END LOOP ; FROM WHERE v_action := 'Fetch source data'; SELECT customer_ot( object_id, --<-- customer_id object_type, --<-- first_name object_name, --<-- last_name created ) BULK COLLECT INTO nt_customer_data all_objects ROWNUM < 9; --<-- start_date BEGIN v_package v_procedure v_action v_business_date errors.package_name% TYPE errors.action% TYPE ; errors.business_date% TYPE := TRUNC ( SYSDATE )-1; := 'ANYDATA_ENCAPSULATION'; errors.procedure_name% TYPE := 'CUSTOMER_LOAD_EG';

nt_customer_data customer_ntt := customer_ntt();

40 41 42 43 44 45 46 47 48 49 50 51

WHEN error.bulk_exceptions THEN error. log ( p_owner p_package p_procedure p_action => USER , => v_package, => v_procedure, => v_action,

p_business_date => v_business_date, p_business_data => ANYDATA.convertCollection (nt_customer_data) ); ROLLBACK ; --<-- error package should encapsulate this... RAISE ; END ; / --<-- error package should also encapsulate this...

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: Line s 12- 17: to bulk collect a set of columns into a collection of an object, we " wrap" the columns with the object's constructor. This casts the separate columns into attributes of a single object structure; Line s 32- 37: to use FORALL .. INSERT with collections of objects, we have two options. One is to cast every attribute using a technique described in t his art icle . The other is to insert into an object view of the target table, which is what we have used in our load above. This enables Oracle to map each instance of the object type in the collection to the columns in the target table. From 9i, we can declare this object view in- line as seen above; and Line 4 6 : the collection of customer data is converted to an instance of ANYDATA using the type's convertCollection static function. 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.

PDFmyURL.com

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

PACKAGE_OWNER PACKAGE_NAME PROCEDURE_NAME ACTION BUSINESS_DATE BUSINESS_KEY ERROR_TS ERROR_MSG ----------------PACKAGE_OWNER PACKAGE_NAME PROCEDURE_NAME ACTION BUSINESS_DATE BUSINESS_KEY ERROR_TS ERROR_MSG -----------------

: SCOTT : ANYDATA_ENCAPSULATION : CUSTOMER_LOAD_EG : Customer load : 31-jul-2007 00:00:00 : 17286 : 01-AUG-07 18.25.11.859000 : ORA-00001: unique constraint (.) violated : SCOTT : ANYDATA_ENCAPSULATION : CUSTOMER_LOAD_EG : Customer load : 31-jul-2007 00:00:00 : 7559 : 01-AUG-07 18.25.11.859000 : 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: each business load package requires an additional object type and a collection type of this object. The object type represents a single record of data that is to be loaded to the target table. The collection is simply an array of this record structure. The object type contains a PRINT_KEY member function to simplify access to the business data that we wish to log (in our example we have just printed the record's primary key); any exceptions we encounter during FORALL bulk processing are passed off to the ERROR.LOG API as an instance of ANYDATA; and the ERROR.LOG procedure is overloaded to accept a SYS.ANYDATA parameter. This parameter stores a collection of the business data that was being loaded at the time of hitting the exception(s). This business data can be of any structure, as defined by relevant underlying object and collection types.

e ncapsulat ing wit h t ype subst it ut io n


PDFmyURL.com

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 3 4 5 ( generic_id NUMBER , MEMBER FUNCTION print_key RETURN VARCHAR2 ) NOT FINAL NOT INSTANTIABLE ; /

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 3 4 5 6 7 / END ; MEMBER FUNCTION print_key RETURN VARCHAR2 IS BEGIN RETURN TO_CHAR ( SELF .generic_id); END ;

Type body created. PDFmyURL.com

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 10 11 12 13 14 15 16 17 18 19 20 21 22 END ; / PROCEDURE log ( p_owner p_package p_procedure p_action IN errors.package_owner% TYPE , IN errors.package_name% TYPE , IN errors.procedure_name% TYPE , IN errors.action% TYPE , bulk_exceptions EXCEPTION ; PRAGMA EXCEPTION_INIT (bulk_exceptions, -24381); PROCEDURE log ( ...snip...

p_business_date IN errors.business_date% TYPE , p_business_data IN bulk_load_ntt );

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.

PDFmyURL.com

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

p_business_date IN errors.business_date% TYPE , p_business_data IN bulk_load_ntt ) IS

p_business_date => p_business_date,

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 3 4 5 6 ( first_name , last_name , start_date ); / VARCHAR2 (30) VARCHAR2 (50) DATE

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 4 5 6 7 8 9 10 11 12 v_action := 'Fetch source data'; SELECT customer_ot( PDFmyURL.com BEGIN v_package v_procedure v_action v_business_date errors.package_name% TYPE errors.action% TYPE ; errors.business_date% TYPE := TRUNC ( SYSDATE )-1; := 'SUBTYPE_ENCAPSULATION'; errors.procedure_name% TYPE := 'CUSTOMER_LOAD_EG';

nt_customer_data bulk_load_ntt := bulk_load_ntt();

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 END ; / EXCEPTION /* FROM WHERE

object_id,

--<-- generic_id in lieu of customer_id

object_type, --<-- first_name object_name, --<-- last_name created ) BULK COLLECT INTO nt_customer_data all_objects ROWNUM < 10; --<-- start_date

|| Add two duplicates to ensure we hit SAVE EXCEPTIONS... */ FOR i IN 1 .. 2 LOOP nt_customer_data. EXTEND ; nt_customer_data(nt_customer_data. LAST ) := nt_customer_data(i); END LOOP ; v_action := 'Customer load'; FORALL i IN 1 .. nt_customer_data. COUNT SAVE EXCEPTIONS INSERT INTO ( SELECT customer_ot( customer_id, first_name, last_name, start_date ) FROM customers) VALUES ( TREAT (nt_customer_data(i) AS customer_ot));

WHEN error.bulk_exceptions THEN error. log ( p_owner p_package p_procedure p_action => USER , => v_package, => v_procedure, => v_action,

p_business_date => v_business_date, p_business_data => nt_customer_data ); ROLLBACK ; --<-- error package should encapsulate this... RAISE ; --<-- error package should also encapsulate this...

PDFmyURL.com

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: Line 7: we are fetching into and loading from a collection of the BULK_LOAD_NTT type, rather than a specific customer data collection; Line 12: we use the CUSTOMER_OT constructor to convert the source data columns into the correct format for bulk fetching into the business data collection. Because CUSTOMER_OT is a subtype of BULK_LOAD_OT, it can be used in the BULK_LOAD_NTT collection instance (this is the benefit of type substitution); Line 37: because we have substituted our CUSTOMER_OT data into a BULK_LOAD_NTT collection, Oracle now considers each element in our collection to be of the BULK_LOAD_OT structure. We must tell Oracle that we have in fact loaded the collection with multiple instances of CUSTOMER_OT instead (using type substitution). We do this when we access the data by using the TREAT function to " downcast" the data to its correct subtype. Quite simply, we have converted the type both on the " way in" and on the " way out" of the collection. 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 PACKAGE_NAME PROCEDURE_NAME ACTION BUSINESS_DATE BUSINESS_KEY ERROR_TS ERROR_MSG ----------------PACKAGE_OWNER

: SCOTT : SUBTYPE_ENCAPSULATION : CUSTOMER_LOAD_EG : Customer load : 31-jul-2007 00:00:00 : 17286 : 01-AUG-07 18.27.49.625000 : ORA-00001: unique constraint (.) violated : SCOTT PDFmyURL.com

PACKAGE_NAME PROCEDURE_NAME ACTION BUSINESS_DATE BUSINESS_KEY ERROR_TS ERROR_MSG -----------------

: SUBTYPE_ENCAPSULATION : CUSTOMER_LOAD_EG : Customer load : 31-jul-2007 00:00:00 : 7559 : 01-AUG-07 18.27.49.625000 : 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. we have a single " top- level" generic object type and collection type of this object. This single supertype is used to define generic parameters where the incoming data might be of different structures. The object type has a single ID attribute to be used by subtypes as appropriate and also a PRINT_KEY member function for simplifying access to the business data; each business load package requires its own object type to be created as a subtype of the single supertype. Each subtype represents a single record of data that is to be loaded to the target table and replaces the use of a PL/SQL record. The subtypes can optionally override the PRINT_KEY member function if they need to access data other than that contained in the single ID attribute; each business load package uses its own object type to structure each record but stores these in a variable of the generic collection type. This is made possible by type substitution and enables a single data type to be passed to generic APIs; any exceptional business data encountered during the FORALL processing is passed off to the ERROR.LOG API in a generic collection; and the ERROR.LOG procedure is overloaded to accept a BULK_LOAD_NTT parameter. This parameter stores a collection of the business data that was being loaded at the time of hitting the exception(s). This business data can be of any subtype structure in the overall type hierarchy that exists under the BULK_LOAD_OT supertype. In our example, we loaded customer data via this mechanism, but this could equally be of any other data format (ACCOUNTS, SALES and so on) as required.

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. PDFmyURL.com

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 Ap p licat io n D e ve lo p e r' s G uid e O b je ct - R e lat io nal Fe at ure s .

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

Ad rian B illing t o n, July 2007 B ack t o To p

o rac le -d e ve lo p e r.ne t 20 0 2-20 12 c o p yrig ht Ad rian Billing to n all rig hts re s e rve d | o rig inal te mp late b y SmallPark | las t up d ate d 0 2 Ap ril 20 12

PDFmyURL.com