Download - Why You Should Use TAPIs
![Page 1: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/1.jpg)
Why You Should Use TAPIs
Jeffrey KempAUSOUG Connect Perth, November
2016
![Page 2: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/2.jpg)
All artifacts including code are presented for illustration purposes only. Use at your own risk. Test thoroughly in a non-critical environment before use.
![Page 3: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/3.jpg)
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
![Page 4: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/4.jpg)
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/
![Page 5: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/5.jpg)
Why a data API?
![Page 6: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/6.jpg)
Why a data API?
“I’m building a simple Apex app.I’ll just use the built-in processes
to handle all the DML.”
![Page 7: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/7.jpg)
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
![Page 8: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/8.jpg)
![Page 9: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/9.jpg)
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
![Page 10: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/10.jpg)
Define all validations and processes in one place– Integrated error messages– Works with Apex single-row and tabular forms
![Page 11: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/11.jpg)
Simple wrapper to allow code re-use– Same validations and processes included– Reduced risk of regression– Reduced risk of missing bits
![Page 12: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/12.jpg)
• They get exactly the same logical outcome as we get• No hidden surprises from Apex features
![Page 13: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/13.jpg)
TAPIs
Business Rule ValidationsDefault ValuesReusabilityEncapsulationMaintainability
![Page 14: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/14.jpg)
Maintainability is in the eye of the beholder maintainer.
![Page 15: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/15.jpg)
Techniques
• DRY• Consistency• Naming• Single-purpose• Assertions
![Page 16: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/16.jpg)
Why use PL/SQL for your API?
![Page 17: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/17.jpg)
Why use PL/SQL for your API?
• Data is forever• UIs come and go• Business logic
– tighter coupling with Data than UI
![Page 18: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/18.jpg)
Business Logic
• your schema• your data constraints• your validation rules• your insert/update/delete logic
![Page 19: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/19.jpg)
• keep business logic close to your data
• on Oracle, PL/SQL is the best
![Page 20: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/20.jpg)
Performance
![Page 21: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/21.jpg)
#ThickDB
![Page 22: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/22.jpg)
#ThickDB
![Page 23: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/23.jpg)
How should you structure your API?
![Page 24: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/24.jpg)
How should you structure your API?
Use packages
![Page 25: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/25.jpg)
Focus each PackageFor example:
– “Employees” API– “Departments” API– “Workflow” API– Security (user roles and privileges) API– Apex Utilities
![Page 26: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/26.jpg)
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'));
![Page 27: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/27.jpg)
Apex processes, simplified
![Page 28: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/28.jpg)
MVC Architecture
entity$APEX
table$TAPI
![Page 29: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/29.jpg)
Process: load
![Page 30: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/30.jpg)
load
1. Get PK value2. Call TAPI to query record3. Set session state for each column
![Page 31: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/31.jpg)
Validation
![Page 32: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/32.jpg)
validate
1. Get values from session state into record2. Pass record to TAPI3. Call APEX_ERROR for each validation error
![Page 33: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/33.jpg)
process page request
![Page 34: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/34.jpg)
process
1. Get v('REQUEST')2. Get values from session state into record3. Pass record to TAPI
![Page 35: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/35.jpg)
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;
![Page 36: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/36.jpg)
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;
![Page 37: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/37.jpg)
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;
![Page 38: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/38.jpg)
PL/SQL in Apex
PKG.proc;
![Page 39: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/39.jpg)
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
![Page 40: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/40.jpg)
Pros• Fast development• Smaller apex app• Dependency analysis• Refactoring
• Modularity• Code re-use• Customisation• Version control
![Page 41: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/41.jpg)
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
![Page 42: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/42.jpg)
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)
![Page 43: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/43.jpg)
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
![Page 44: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/44.jpg)
TAPIs
• Encapsulate all DML for a table• Row-level validation• Detect lost updates• Generated
![Page 45: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/45.jpg)
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
![Page 46: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/46.jpg)
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!
![Page 47: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/47.jpg)
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
![Page 48: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/48.jpg)
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 );
![Page 49: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/49.jpg)
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;
![Page 50: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/50.jpg)
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;
![Page 51: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/51.jpg)
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;
![Page 52: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/52.jpg)
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;
![Page 53: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/53.jpg)
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.”
![Page 54: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/54.jpg)
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;
![Page 55: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/55.jpg)
What about queries?
Tuning a complex, general-purpose queryis more difficult than
tuning a complex, single-purpose query.
![Page 56: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/56.jpg)
Generating Code
• Only PL/SQL• Templates compiled in the schema• Simple syntax• Sub-templates (“includes”) for extensibility
![Page 57: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/57.jpg)
OraOpenSource TAPI
• Runs on NodeJS• Uses Handlebars for template processing• https://github.com/OraOpenSource/oos-tapi/• Early stages, needs contributors
![Page 58: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/58.jpg)
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}};
![Page 59: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/59.jpg)
oddgen• SQL*Developer plugin• Code generator, including TAPIs• Support now added in jk64 Apex TAPI generator
https://www.oddgen.org
![Page 60: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/60.jpg)
![Page 61: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/61.jpg)
Takeaways
Be Consistent
Consider Your Successors
![Page 62: Why You Should Use TAPIs](https://reader035.vdocuments.us/reader035/viewer/2022081507/58f277d51a28ab0b0b8b4607/html5/thumbnails/62.jpg)
Thank you
jeffkemponoracle.com