The driving factor for developing this extension to PIT
came up when we wanted to reuse validation logic in the database layer for an APEX application. Even with APEX where all code resides within the database, it's a wise decision to carefully think about where to implement which part of the code. As a bare minimum, the APEX workspace schema should be separated from the schema containing the data and its attached validation and processing logic. This is to support future development, where you probably want to replace APEX with the new kid on the block. Wrapping all your validation and data processing logic in the vicinity of APEX will then turn out to be a desastrous idea.
But if we split the logic between a schema that contains the data and an APEX schema that only holds part of the logic that deals with user input and display issues, you may wonder where to put your validation logic. The end user's data input has to be validated within the application and this should be done in accordance to the APEX lifecycle. In APEX, a validation of a page is called either upon submit of the page or during lifetime of a page using an AJAX call to the database.
Unfortunately, validation of data within the database has a differnt aim than validation on the APEX UI side. Whereas the focus of validating data within a transaction API within the database is used to assert that no invalid data may be stored in the database, the UI is interested in getting all validation issues of all form fields at once. This is to avoid that the end user has to submit the page mutliple times to sort out all errors.
The existing validation logic on the other hand immediately stops if the first issue is detected, as this is sufficient to reject writing data to the database. So how do we overcome this different goals? It should be possible to switch the desired strategy between »find first issue« versus »find all issues«. If you set up your validation logic using PIT
(chances are you did, as you read this manual) then there is a simple solution available for you.
The solution is to switch PIT
to collect mode. This hinders it to immediately throw an exception once an assertion has gone wild or an exception has been thrown using PIT.raise_error
, PIT.raise_severe
or PIT.raise_fatal
. Instead, PIT
will silently push all messages on a stack until you tell it to stop collecting messages.
Switching collect mode on is done by calling PIT.start_message_collection
. This call should be used just before you call the validation code if you require the »find all issues« strategy. If you omit this call, PIT
will work in its default »find first issue« strategy just as PL/SQL does.
After you have called all validation logic of interest, you stop the collect mode by calling PIT.stop_message_collection
.
Now, PIT
will examine the collected messages and detect the most severe message during that period. Should at least one message of severity ERROR
, SEVERE
or FATAL
be found, PIT
will now throw exception PIT_BULK_ERROR
, PIT_BULK_SEVERE
or PIT_BULK_FATAL
respectively. You can catch those just like any other PIT
exception and get access to all collected messages by using PIT.get_message_collection
. This method returns an instance of type PIT_MESSAGE_TABLE
which is simply a table of MESSAGE_TYPE
instances, so you will have all attributes of a normal message, such as message text, message attributes, severity, call stack etc. But there's even more.
MESSAGE_TYPE
contains an attribute called ERROR_CODE
. This attribute is used to transport error codes from legacy systems. In collection mode, PIT
uses this attribute in a specific manner. The reason is that I had to find a solution for a repeated use of the same message. Say that a validation logic checks some input parameters for NOT NULL
checks. If will most likely use the same PIT
message for those tests. But how can you now tell, which parameter value caused this exception to be raised?
If you utilize a PIT
message more than once in a validation method, you may differ those by giving them a different error code. You can simply pass this code as a parameter to the PIT.assert...
methods. Think of an error code as a virtual exception if you like. It will not be fired and can't be catched but this is nothing you want to do in collect mode anyway. By introducing this concept to the collect mode, you can differentiate repeated uses of one »real« PIT
message easily. To the calling environment there is hardly any differnce between an error code and a message name. Rather, PIT
will populate the error code attribute with the message name if it is in collect mode and no explicit error code was provided. Therefore, it is save to base your logic on the error code attribute.
As a consequence of this, the possible error codes the validation logic may throw should be documented, fi by using a method comment.
Error codes serve a specific purpose even when looking into the opposite direction. As stated, you can use them to reuse the same PIT
message for various validations, but you can use error codes to group together validations as well. As an example, you may
want to check an IBAN number for a bank account. Chances are that you need several validations, leading to different errors. You check the length, the pattern, the checksum and even the content. If the length is incorrect, it does not make sense to run all
the other validations, as they cannot run successfully anyway. What `PIT does now is that it collects all the error codes along with their worst severity so far. So if a warning has been raised and there is a validation that is to throw an error, it will be executed. If, on the other hand, a validation raised an error, no other validation for that same error code will be evaluated within this collect run. Sorting your validation from generic to specialized will assure a meaningful validation even in complex use cases.
Note: There used to be a method calle pit.has_no_bulk_errors
that could be used to decide whether additional validations have to be performed. This method no longer exists but is replaced by two new methods with an enhanced functionality: pit.assert_is_ok
asserts that no error has been detected during this collect run. If you pass in a single error_code or a list of error codes (wrapped in a char_table
object), this method will check whether there were errors only against the mentioned error codes. The second method, pit.assert_is_not_ok
is just a logical conversion. Why would you need these methods? Imagine two date fields which form a date range. First, you want to check whether both dates are valid (they may have to be in the future or whatever else you require). If both date fields are valid, you need to check the date range they form. This validation does only make sense if both date fields are valid. So you could bind the validation to pit.assert_is_ok(char_table('date_from', 'date_until')
. If you are not in collect mode, you would never get the point of checking the date range if one of the date fields were invalid.
But there is another problem we need to solve. As the validation logic is part of the data schema, it should not have any knowledge about the details of the UI which is on a higher level. This means that the validation logic does not know any name or id of an input field where the information it validated came from. On the opposite, the UI logic should not have any knowledge about the implementation of the validation logic. So this boils down to the problem of how to match the messages which come back as a result of the validation to the input fields to show the right message at the right input field.
The easiest way that came to mind is to provide a mapping table between the error codes returned from the validation logic and the UI field names on the UI level. On the UI side, this should be encapsulated into a helper method. This method expects an instance of type char_table
containing a pair-wise mapping of the error code and the name of the input field the error code belongs to. The helper method can retrieves the message collection from PIT
and matches the error codes from the validation logic with the UI field names and show the validation errors next to those fields.
Neither the validation logic nor the UI logic crosses any permitted boundaries, the knowledge stays where it belongs. The possible error codes are part of the validation methods interface and it is therefore acceptable to expect that the calling logic knows about this list of possible exceptions.
The validation logic for a use case throws three possible error codes: ATTR_A_MISSING
, ATTR_B_MISSING
, INVALID_VALUE
.
From the documentation of the validation method, you know that ATTR_A_MISSING
belongs to page item P10_ITEM_A
, whereas the two other error codes belong to P10_ITEM_B
. You then call the validation logic using the following pattern:
begin
pit.start_message_collection;
xapi_pkg.validate_use_case(<params>);
pit.stop_message_collection;
exception
when msg.PIT_BULK_ERROR_ERR or msg.PIT_FATAL_ERROR_ERR then
my_ui_helper.handle_bulk_error(char_table(
'ATTR_A_MISSING', 'P10_ITEM_A',
'ATTR_B_MISSING', 'P10_ITEM_B',
'INVALID_VALUE', 'P10_ITEM_B'));
end;
The helper method my_ui_helper.handle_bulk_error
serves two purposes:
- it grabs the message collection by calling
pit.get_message_collection;
. This is a table ofmessage_type
instances. - it loops over this collection, reads and maps the error codes of any message within the collection to the
char_table
instance passed in as a parameter.
If found, it shows the message text next to the page item. If not found, the helper method should show the message without reference to a page item.
To wrap things up, I'd like to describe the proper use of the PIT
collect mode:
- Implement your validation logic in a separate method within the XAPI (A transaction API). Implement your validation logic using
pit.assert...
methods or by raising exceptions using thepit.raise_error/raise_severe/raise_fatal
methods. - In your XAPI, prior to saving the data to database tables, call your validation logic in normal mode.
- In your UI package dealing with the user data entries, enrich missing data, harmonize spelling issues, provide missing default values and the like and then call the validation logic in collect mode by wrapping the call of the validation logic with calls to
pit.start_message_collection
andpit.stop_message_collection
. If validation errors occur, this will throw an exception you can catch. - Handle exception
msg.PIT_BULK_FATAL_ERR
andmsg.PIT_BULK_ERROR_ERR
(exception pre- or postfix is based on your parameterization) and call your internal helper method to encapsulate the call toPIT.get_message_collection
and map it to a list of matching »error codes to UI field«.
An example of such a helper method can be found at my UTL_APEX
helper package in method handle_bulk_error
here.
This way you don't need to duplicate your validation logic but make it reusable in a user friendly manner.