Ever since I started exploring the idea of using a TAPI approach with Apex, something I was never quite satisfied with was Tabular Forms.
They can be a bit finicky to work with, and if you’re not careful you can break them to the point where it’s easier to recreate them from scratch rather than try to fix them (although if you understand the underlying mechanics you can fix them [there was an article about this I read recently but I can’t find it now]).
I wanted to use the stock-standard Apex tabular form, rather than something like Martin D’Souza’s approach – although I have used that a number of times with good results.
In the last week or so while making numerous improvements to my TAPI generator, and creating the new Apex API generator, I tackled again the issue of tabular forms. I had a form that was still using the built-in Apex ApplyMRU and ApplyMRD processes (which, of course, bypass my TAPI). I found that if I deleted both of these processes, and replaced them with a single process that loops over the APEX_APPLICATION.g_f0x arrays, I lose a number of Tabular Form features such as detecting which records were changed.
Instead, what ended up working (while retaining all the benefits of a standard Apex tabular form) was to create a row-level process instead. Here’s some example code that I put in this Apex process that interfaces with my Apex API:
VENUES$APEX.apply_mr (rv => VENUES$TAPI.rv (venue_id => :VENUE_ID ,name => :NAME ,version_id => :VERSION_ID ));
The process has Execution Scope set to For Created and Modified Rows. It first calls my
TAPI.rv function to convert the individual columns from the row into an
rvtype record, which it then passes to the Apex API
apply_mr procedure. The downside to this approach is that each record is processed separately – no bulk updates; however, tabular forms are rarely used to insert or update significant volumes of data anyway so I doubt this would be of practical concern. The advantage of using the
rv function is that it means I don’t need to repeat all the column parameters for all my API procedures, making maintenance easier.
The other change that I had to make was ensure that any Hidden columns referred to in my Apply process must be set to Hidden Column (saves state) – in this case, the VERSION_ID column.
Here’s the generated Apex API apply_mr procedure:
PROCEDURE apply_mr (rv IN VENUES$TAPI.rvtype) IS r VENUES$TAPI.rowtype; BEGIN log_start('apply_mr'); UTIL.check_authorization('Operator'); IF APEX_APPLICATION.g_request = 'MULTI_ROW_DELETE' THEN IF v('APEX$ROW_SELECTOR') = 'X' THEN VENUES$TAPI.del (rv => rv); END IF; ELSE CASE v('APEX$ROW_STATUS') WHEN 'C' THEN r := VENUES$TAPI.ins (rv => rv); WHEN 'U' THEN r := VENUES$TAPI.upd (rv => rv); ELSE NULL; END CASE; END IF; log_end; EXCEPTION WHEN UTIL.application_error THEN log_end('application_error'); RAISE; WHEN OTHERS THEN UTIL.log_sqlerrm; RAISE; END apply_mr;
The code uses
APEX$ROW_STATUS to determine whether to insert or update each record. If the Delete button was pressed, it checks
APEX$ROW_SELECTOR to check that the record had been selected for delete – although it could skip that check since Apex seems to call the procedure for only the selected records anyway. The debug logs show Apex skipping the records that weren’t selected.
Now, before we run off gleefully inserting and updating records we should really think about validating them and reporting any errors to the user in a nice way. The TAPI
upd functions do run the validation routine, but they don’t set up UTIL with the mappings so that the Apex errors are registered as we need them to. So, we add a per-record validation in the Apex page that runs this:
VENUES$APEX.val_row (rv => VENUES$TAPI.rv (venue_id => :VENUE_ID ,name => :NAME ,version_id => :VERSION_ID ) ,region_static_id => 'venues'); RETURN null;
As for the single-record page, this validation step is of type PL/SQL Function (returning Error Text). Its Execution Scope is the same as for the apply_mr process – For Created and Modified Rows.
Note that we need to set a static ID on the tabular form region (the generator assumes it is the table name in lowercase – e.g.
venues – but this can be changed if desired).
val_row procedure is as follows:
PROCEDURE val_row (rv IN VENUES$TAPI.rvtype ,region_static_id IN VARCHAR2 ) IS dummy VARCHAR2(32767); column_alias_map UTIL.str_map; BEGIN log_start('val_row'); UTIL.pre_val_row (label_map => VENUES$TAPI.label_map ,region_static_id => region_static_id ,column_alias_map => column_alias_map); dummy := VENUES$TAPI.val (rv => rv); UTIL.post_val; log_end; EXCEPTION WHEN UTIL.application_error THEN log_end('application_error'); RAISE; WHEN OTHERS THEN UTIL.log_sqlerrm; RAISE; END val_row;
pre_val_row procedure tells all the validation handlers how to register any error message with APEX_ERROR. In this case,
column_alias_map is empty, which causes them to assume that each column name in the tabular form is named the same as the column name on the database. If this default mapping is not correct for a particular column, we can declare the mapping, e.g.
column_alias_map('DB_COLUMN_NAME') := 'TABULAR_FORM_COLUMN_NAME';. This way, when the errors are registered with APEX_ERROR they will be shown correctly on the Apex page.
Things got a little complicated when I tried using this approach for a table that didn’t have any surrogate key, where my TAPI uses ROWID instead to uniquely identify a row for update. In this case, I had to change the generated query to include the ROWID, e.g.:
SELECT t.event_type ,t.name ,t.calendar_css ,t.start_date ,t.end_date ,t.last_updated_dt ,t.version_id ,t.ROWID AS p_rowid FROM event_types t
I found if I didn’t give a different alias for ROWID, the tabular form would not be rendered at runtime as it conflicted with Apex trying to get its own version of ROWID from the query. Note that the
P_ROWID must also be set to Hidden Column (saves state). I found it strange that Apex would worry about it because when I removed* the ApplyMRU and ApplyMRD processes, it stopped emitting the ROWID in the
frowid_000n hidden items. Anyway, giving it the alias meant that it all worked fine in the end.
* CORRECTION (7/11/2016): Don’t remove the ApplyMRU process, instead mark it with a Condition of “Never” – otherwise Apex will be unable to map errors to the right rows in the tabular form.
The Add Rows button works; also, the Save button correctly calls my TAPI only for inserted and updated records, and shows error messages correctly. I can use Apex’s builtin Tabular Form feature, integrated neatly with my TAPI instead of manipulating the table directly. Mission accomplished.
Source code/download: https://bitbucket.org/jk64/jk64-sample-apex-tapi