Upload
jeffrey-kemp
View
524
Download
4
Embed Size (px)
Citation preview
Why You Should Use TAPIs
Jeffrey KempAUSOUG Connect Perth, November
2016
All artifacts including code are presented for illustration purposes only. Use at your own risk. Test thoroughly in a non-critical environment before use.
Main Menu
1. Why a data API?2. Why choose PL/SQL?3. How to structure your API?4. Data API for Apex5. Table APIs (TAPIs)6. Open Source TAPI project
Background
“Building Maintainable Apex Apps”, 2014https://jeffkemponoracle.com/2014/11/14/sample-tapi-apex-application/https://jeffkemponoracle.com/2016/02/11/tapi-generator-mkii/https://jeffkemponoracle.com/2016/02/12/apex-api-call-a-package-for-all-your-dml/https://jeffkemponoracle.com/2016/02/16/apex-api-for-tabular-forms/https://jeffkemponoracle.com/2016/06/30/interactive-grid-apex-5-1-ea/
Why a data API?
Why a data API?
“I’m building a simple Apex app.I’ll just use the built-in processes
to handle all the DML.”
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
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
Define all validations and processes in one place– Integrated error messages– Works with Apex single-row and tabular forms
Simple wrapper to allow code re-use– Same validations and processes included– Reduced risk of regression– Reduced risk of missing bits
• They get exactly the same logical outcome as we get• No hidden surprises from Apex features
TAPIs
Business Rule ValidationsDefault ValuesReusabilityEncapsulationMaintainability
Maintainability is in the eye of the beholder maintainer.
Techniques
• DRY• Consistency• Naming• Single-purpose• Assertions
Why use PL/SQL for your API?
Why use PL/SQL for your API?
• Data is forever• UIs come and go• Business logic
– tighter coupling with Data than UI
Business Logic
• your schema• your data constraints• your validation rules• your insert/update/delete logic
• keep business logic close to your data
• on Oracle, PL/SQL is the best
Performance
#ThickDB
#ThickDB
How should you structure your API?
How should you structure your API?
Use packages
Focus each PackageFor example:
– “Employees” API– “Departments” API– “Workflow” API– Security (user roles and privileges) API– Apex Utilities
Package names as context
GENERIC_PKG.get_event (event_id => nv('P1_EVENT_ID'));GENERIC_PKG.get_member (member_id => nv('P1_MEMBER_ID'));
EVENT_PKG.get (event_id => nv('P1_EVENT_ID'));MEMBER_PKG.get (member_id => nv('P1_MEMBER_ID'));
Apex processes, simplified
MVC Architecture
entity$APEX
table$TAPI
Process: load
load
1. Get PK value2. Call TAPI to query record3. Set session state for each column
Validation
validate
1. Get values from session state into record2. Pass record to TAPI3. Call APEX_ERROR for each validation error
process page request
process
1. Get v('REQUEST')2. Get values from session state into record3. Pass record to TAPI
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;
get_rowfunction apex_get return VOLUNTEERS$TAPI.rvtype is rv VOLUNTEERS$TAPI.rvtype;begin
rv.vol_id := nv('P9_VOL_ID'); rv.given_name := v('P9_GIVEN_NAME'); rv.surname := v('P9_SURNAME'); rv.date_of_birth := v('P9_DATE_OF_BIRTH'); rv.address_line := v('P9_ADDRESS_LINE'); rv.suburb := v('P9_SUBURB'); rv.postcode := v('P9_POSTCODE'); rv.state := v('P9_STATE'); rv.home_phone := v('P9_HOME_PHONE'); rv.mobile_phone := v('P9_MOBILE_PHONE'); rv.email_address := v('P9_EMAIL_ADDRESS'); rv.version_id := nv('P9_VERSION_ID');
return rv;end apex_get;
set rowprocedure apex_set (r in VOLUNTEERS$TAPI.rowtype) isbegin
sv('P9_VOL_ID', r.vol_id); sv('P9_GIVEN_NAME', r.given_name); sv('P9_SURNAME', r.surname); sd('P9_DATE_OF_BIRTH', r.date_of_birth); sv('P9_ADDRESS_LINE', r.address_line); sv('P9_STATE', r.state); sv('P9_SUBURB', r.suburb); sv('P9_POSTCODE', r.postcode); sv('P9_HOME_PHONE', r.home_phone); sv('P9_MOBILE_PHONE', r.mobile_phone); sv('P9_EMAIL_ADDRESS', r.email_address); sv('P9_VERSION_ID', r.version_id);
end apex_set;
PL/SQL in Apex
PKG.proc;
SQL in Apexselect t.col_a ,t.col_b ,t.col_cfrom my_table t;
• Move joins, select expressions, etc. to a view– except Apex-specific stuff like generated APEX_ITEMs
Pros• Fast development• Smaller apex app• Dependency analysis• Refactoring
• Modularity• Code re-use• Customisation• Version control
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
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)
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
TAPIs
• Encapsulate all DML for a table• Row-level validation• Detect lost updates• Generated
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
Why not a simple rowtype?procedure ins (emp_name in varchar2 ,dob in date ,salary in number ) isbegin 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!
Validation record typetype rv is record ( emp_name varchar2(4000) , dob varchar2(4000) , salary varchar2(4000));
procedure ins (rv in rvtype) isbegin 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
Example Tablecreate 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 );
TAPI examplepackage 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 rowfunction val (rv IN rvtype) return varchar2;
-- insert a rowfunction ins (rv IN rvtype) return rowtype;
-- update a rowfunction upd (rv IN rvtype) return rowtype;
-- delete a rowprocedure del (rv IN rvtype);
end VENUES$TAPI;
TAPI insfunction 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;
TAPI valfunction val (rv in rvtype) return varchar2 isbegin
UTIL.val_not_null (val => rv.host_id, column_name => HOST_ID); UTIL.val_not_null (val => rv.event_type, column_name => EVENT_TYPE); UTIL.val_not_null (val => rv.title, column_name => TITLE); UTIL.val_not_null (val => rv.start_dt, column_name => START_DT); UTIL.val_max_len (val => rv.event_type, len => 100, column_name => EVENT_TYPE); UTIL.val_max_len (val => rv.title, len => 100, column_name => TITLE); UTIL.val_max_len (val => rv.description, len => 4000, column_name => DESCRIPTION); UTIL.val_datetime (val => rv.start_dt, column_name => START_DT); UTIL.val_datetime (val => rv.end_dt, column_name => END_DT); UTIL.val_domain (val => rv.repeat ,valid_values => t_str_array(DAILY, WEEKLY, MONTHLY, ANNUALLY) ,column_name => REPEAT); UTIL.val_integer (val => rv.repeat_interval, range_low => 1, column_name => REPEAT_INTERVAL); UTIL.val_date (val => rv.repeat_until, column_name => REPEAT_UNTIL); UTIL.val_ind (val => rv.repeat_ind, column_name => REPEAT_IND);
return UTIL.first_error;end val;
TAPI updfunction 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;
Lost update handlerprocedure 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.”
TAPI bulk_insfunction bulk_ins (arr in rvarraytype) return number isbegin 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;
What about queries?
Tuning a complex, general-purpose queryis more difficult than
tuning a complex, single-purpose query.
Generating Code
• Only PL/SQL• Templates compiled in the schema• Simple syntax• Sub-templates (“includes”) for extensibility
OraOpenSource TAPI
• Runs on NodeJS• Uses Handlebars for template processing• https://github.com/OraOpenSource/oos-tapi/• Early stages, needs contributors
OOS-TAPI Examplecreate 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}};
oddgen• SQL*Developer plugin• Code generator, including TAPIs• Support now added in jk64 Apex TAPI generator
https://www.oddgen.org
Takeaways
Be Consistent
Consider Your Successors
Thank you
jeffkemponoracle.com