or "Towards a Standard TAPI", presented at AUSOUG Connect Perth, November 2016. I've been using a combination of Table APIs and Transaction APIs to build complex but maintainable applications in Apex - something I encourage everyone to at least consider.
Modular Monolith - a Practical Alternative to Microservices @ Devoxx UK 2024
Why You Should Use TAPIs
1. Why You Should Use TAPIs
Jeffrey Kemp
AUSOUG Connect Perth, November 2016
2. All artifacts including code are presented for illustration
purposes only. Use at your own risk. Test thoroughly in
a non-critical environment before use.
3. Main Menu
1. Why a data API?
2. Why choose PL/SQL?
3. How to structure your API?
4. Data API for Apex
5. Table APIs (TAPIs)
6. Open Source TAPI project
6. Why a data API?
“I’m building a simple Apex app.
I’ll just use the built-in processes
to handle all the DML.”
7. Your requirements get more
complex.
– More single-row and/or tabular
forms
– More pages, more load routines,
more validations, more
insert/update processes
– Complex conditions
– Edge cases, special cases, weird
cases
8.
9. Another system must create the same data –
outside of Apex
– Re-use validations and processing
– Rewrite the validations
– Re-engineer all processing (insert/update) logic
– Same edge cases
– Different edge cases
10. Define all validations and processes in one place
– Integrated error messages
– Works with Apex single-row and tabular forms
11. Simple wrapper to allow code re-use
– Same validations and processes included
– Reduced risk of regression
– Reduced risk of missing bits
12. • They get exactly the same logical outcome as we get
• No hidden surprises from Apex features
35. Process a page requestprocedure process is
rv EVENTS$TAPI.rvtype;
r EVENTS$TAPI.rowtype;
begin
UTIL.check_authorization(SECURITY.Operator);
case
when APEX_APPLICATION.g_request = 'CREATE'
then
rv := apex_get;
r := EVENTS$TAPI.ins (rv => rv);
apex_set (r => r);
UTIL.success('Event created.');
when APEX_APPLICATION.g_request like 'SAVE%'
then
rv := apex_get;
r := EVENTS$TAPI.upd (rv => rv);
apex_set (r => r);
UTIL.success('Event updated.');
when APEX_APPLICATION.g_request = 'DELETE'
then
rv := apex_get_pk;
EVENTS$TAPI.del (rv => rv);
UTIL.clear_page_cache;
UTIL.success('Event deleted.');
else
null;
end case;
end process;
39. SQL in Apex
select t.col_a
,t.col_b
,t.col_c
from my_table t;
• Move joins, select expressions, etc. to a
view
– except Apex-specific stuff like generated APEX_ITEMs
40. Pros
• Fast development
• Smaller apex app
• Dependency analysis
• Refactoring
• Modularity
• Code re-use
• Customisation
• Version control
41. Cons
• Misspelled/missing item names
– Mitigation: isolate all apex code in one set of
packages
– Enforce naming conventions – e.g. P1_COLUMN_NAME
• Apex Advisor doesn’t check database package
code
42. Apex API Coding Standards
• All v() calls at start of proc, once per item
• All sv() calls at end of proc
• Constants instead of 'P1_COL'
• Dynamic Actions calling PL/SQL – use parameters
• Replace PL/SQL with Javascript (where possible)
43. Error Handling
• Validate - only record-level validation
• Cross-record validation – db constraints + XAPI
• Capture DUP_KEY_ON_VALUE and ORA-02292 for unique and
referential constraints
• APEX_ERROR.add_error
44. TAPIs
• Encapsulate all DML for a table
• Row-level validation
• Detect lost updates
• Generated
45. TAPI contents
• Record types
– rowtype, arraytype, validation record type
• Functions/Procedures
– ins / upd / del / merge / get
– bulk_ins / bulk_upd / bulk_merge
• Constants for enumerations
46. Why not a simple rowtype?
procedure ins
(emp_name in varchar2
,dob in date
,salary in number
) is
begin
if is_invalid_date (dob) then
raise_error('Date of birth bad');
elsif is_invalid_number (salary) then
raise_error('Salary bad');
end if;
insert into emp (emp_name, dob, salary) values (emp_name, dob, salary);
end ins;
ins (emp_name => :P1_EMP_NAME, dob => :P1_DOB, salary => :P1_SALARY);
ORA-01858: a non-numeric character was found where a numeric was expected
It’s too late to validate
data types here!
47. Validation record type
type rv is record
( emp_name varchar2(4000)
, dob varchar2(4000)
, salary varchar2(4000));
procedure ins (rv in rvtype) is
begin
if is_invalid_date (dob) then
raise_error('Date of birth bad');
elsif is_invalid_number (salary) then
raise_error('Salary bad');
end if;
insert into emp (emp_name, dob, salary) values (emp_name, dob, salary);
end ins;
ins (emp_name => :P1_EMP_NAME, dob => :P1_DOB, salary => :P1_SALARY);
I’m sorry Dave, I can’t do that - Date of birth bad
48. Example Table
create table venues
( venue_id integer default on null venue_id_seq.nextval
, name varchar2(200 char)
, map_position varchar2(200 char)
, created_dt date default on null sysdate
, created_by varchar2(100 char)
default on null sys_context('APEX$SESSION','APP_USER')
, last_updated_dt date default on null sysdate
, last_updated_by varchar2(100 char)
default on null sys_context('APEX$SESSION','APP_USER')
, version_id integer default on null 1
);
49. TAPI example
package VENUES$TAPI as
cursor cur is select x.* from venues;
subtype rowtype is cur%rowtype;
type arraytype is table of rowtype
index by binary_integer;
type rvtype is record
(venue_id venues.venue_id%type
,name varchar2(4000)
,map_position varchar2(4000)
,version_id venues.version_id%type
);
type rvarraytype is table of rvtype
index by binary_integer;
-- validate the row
function val (rv IN rvtype) return varchar2;
-- insert a row
function ins (rv IN rvtype) return rowtype;
-- update a row
function upd (rv IN rvtype) return rowtype;
-- delete a row
procedure del (rv IN rvtype);
end VENUES$TAPI;
50. TAPI ins
function ins (rv in rvtype)
return rowtype is
r rowtype;
error_msg varchar2(32767);
begin
error_msg := val (rv => rv);
if error_msg is not null then
UTIL.raise_error(error_msg);
end if;
insert into venues
(name
,map_position)
values(rv.name
,rv.map_position)
returning
venue_id
,...
into r;
return r;
exception
when dup_val_on_index then
UTIL.raise_dup_val_on_index;
end ins;
52. TAPI upd
function upd (rv in rvtype) return rowtype is
r rowtype;
error_msg varchar2(32767);
begin
error_msg := val (rv => rv);
if error_msg is not null then
UTIL.raise_error(error_msg);
end if;
update venues x
set x.name = rv.name
,x.map_position = rv.map_position
where x.venue_id = rv.venue_id
and x.version_id = rv.version_id
returning
venue_id
,...
into r;
if sql%notfound then
raise UTIL.lost_update;
end if;
return r;
exception
when dup_val_on_index then
UTIL.raise_dup_val_on_index;
when UTIL.ref_constraint_violation then
UTIL.raise_ref_con_violation;
when UTIL.lost_update then
lost_upd (rv => rv);
end upd;
53. Lost update handler
procedure lost_upd (rv in rvtype) is
db_last_updated_by venues.last_updated_by%type;
db_last_updated_dt venues.last_updated_dt%type;
begin
select x.last_updated_by
,x.last_updated_dt
into db_last_updated_by
,db_last_updated_dt
from venues x
where x.venue_id = rv.venue_id;
UTIL.raise_lost_update
(updated_by => db_last_updated_by
,updated_dt => db_last_updated_dt);
exception
when no_data_found then
UTIL.raise_error('LOST_UPDATE_DEL');
end lost_upd;
“This record was changed by
JOE BLOGGS at 4:31pm.
Please refresh the page to see
changes.”
“This record was deleted by
another user.”
54. TAPI bulk_ins
function bulk_ins (arr in rvarraytype) return number is
begin
bulk_val(arr);
forall i in indices of arr
insert into venues
(name
,map_position)
values (arr(i).name
,arr(i).map_position);
return sql%rowcount;
exception
when dup_val_on_index then
UTIL.raise_dup_val_on_index;
end bulk_ins;
55. What about queries?
Tuning a complex, general-purpose query
is more difficult than
tuning a complex, single-purpose query.
56. Generating Code
• Only PL/SQL
• Templates compiled in the schema
• Simple syntax
• Sub-templates (“includes”) for extensibility
57. OraOpenSource TAPI
• Runs on NodeJS
• Uses Handlebars for template processing
• https://github.com/OraOpenSource/oos-tapi/
• Early stages, needs contributors
58. OOS-TAPI Example
create or replace package body {{toLowerCase table_name}} as
gc_scope_prefix constant varchar2(31) := lower($$plsql_unit) || '.';
procedure ins_rec(
{{#each columns}}
p_{{toLowerCase column_name}} in {{toLowerCase data_type}}
{{#unless @last}},{{lineBreak}}{{/unless}}
{{~/each}}
);
end {{toLowerCase table_name}};
59. oddgen
• SQL*Developer plugin
• Code generator, including TAPIs
• Support now added in jk64 Apex TAPI generator
https://www.oddgen.org
It’s declarative – no code required to load, validate, insert, update and delete data.”
Apex handles so much for us, making the app more reliable and us more productive – such as automatic lost update detection, basic data type validations including maximum field lengths, date formats, mandatory fields and more.”
(Why would a sane developer want to rebuild any of this?)
(and you have a hard time remembering the details of what you built last week)
No need to reverse-engineer the logic, no need to replicate things with added risk of hidden surprises
Code that is easy to maintain is code that is easy to read, and easy to test.
Remember, maintainability is NOT a problem for you while you are writing the code. It is a problem you need to solve for the person 3 months later who needs to maintain your code.
How do we make code easier to read and test?
Organise and name your packages according to how they will be used elsewhere. This means your function and procedure names can be very short, because you no longer have to say “get_event”
Table APIs will form the “Model” part of the MVC equation.
Apex provides the “View” part.
The Controller is what we’ll implement almost completely in PL/SQL in database packages.
NV is only used for hidden items that should always have numerical values. The TAPI will handle all other data type conversions (such as numbers and dates).
For Dynamic Actions, since performance is the top priority, I’d always use parameters for any data required.
IF/ELSE and CASE statements instead of Apex conditions.
The code is more re-usable: both across pages within the application, as well as by other apex applications or even other UIs and system interfaces.
Easier to read, debug, diagnose and version control. Code merge has been solved for database PL/SQL source files, but not for Apex components.
Especially good for external interfaces, e.g. for inserting into an eBus interface table
Where multiple rows might need to be processed, always use bulk binding and bulk DML. Never ever call single-row routines from within a loop!
The val routine in a TAPI should rarely if ever query other tables – it usually only validates the row in isolation within its own context. Generally cross-record and cross-table validation should be done at the XAPI level, or rely on table constraints.