Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Why You Should Use TAPIs


Published on

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.

Published in: Technology

Why You Should Use TAPIs

  1. 1. Why You Should Use TAPIs Jeffrey Kemp AUSOUG Connect Perth, November 2016
  2. 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. 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
  4. 4. Background “Building Maintainable Apex Apps”, 2014
  5. 5. Why a data API?
  6. 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. 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. 8. 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
  9. 9. Define all validations and processes in one place – Integrated error messages – Works with Apex single-row and tabular forms
  10. 10. Simple wrapper to allow code re-use – Same validations and processes included – Reduced risk of regression – Reduced risk of missing bits
  11. 11. • They get exactly the same logical outcome as we get • No hidden surprises from Apex features
  12. 12. TAPIs Business Rule Validations Default Values Reusability Encapsulation Maintainability
  13. 13. Maintainability is in the eye of the beholder maintainer.
  14. 14. Techniques • DRY • Consistency • Naming • Single-purpose • Assertions
  15. 15. Why use PL/SQL for your API?
  16. 16. Why use PL/SQL for your API? • Data is forever • UIs come and go • Business logic – tighter coupling with Data than UI
  17. 17. Business Logic • your schema • your data constraints • your validation rules • your insert/update/delete logic
  18. 18. • keep business logic close to your data • on Oracle, PL/SQL is the best
  19. 19. Performance
  20. 20. #ThickDB
  21. 21. #ThickDB
  22. 22. How should you structure your API?
  23. 23. How should you structure your API? Use packages
  24. 24. Focus each Package For example: – “Employees” API – “Departments” API – “Workflow” API – Security (user roles and privileges) API – Apex Utilities
  25. 25. 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'));
  26. 26. Apex processes, simplified
  27. 27. MVC Architecture entity$APEX table$TAPI
  28. 28. Process: load
  29. 29. load 1. Get PK value 2. Call TAPI to query record 3. Set session state for each column
  30. 30. Validation
  31. 31. validate 1. Get values from session state into record 2. Pass record to TAPI 3. Call APEX_ERROR for each validation error
  32. 32. process page request
  33. 33. process 1. Get v('REQUEST') 2. Get values from session state into record 3. Pass record to TAPI
  34. 34. 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;
  35. 35. 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;
  36. 36. set row procedure apex_set (r in VOLUNTEERS$TAPI.rowtype) is begin 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;
  37. 37. PL/SQL in Apex PKG.proc;
  38. 38. 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
  39. 39. Pros • Fast development • Smaller apex app • Dependency analysis • Refactoring • Modularity • Code re-use • Customisation • Version control
  40. 40. 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
  41. 41. 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)
  42. 42. 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
  43. 43. TAPIs • Encapsulate all DML for a table • Row-level validation • Detect lost updates • Generated
  44. 44. 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
  45. 45. 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!
  46. 46. 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
  47. 47. 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 );
  48. 48. 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;
  49. 49. 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.map_position) returning venue_id ,... into r; return r; exception when dup_val_on_index then UTIL.raise_dup_val_on_index; end ins;
  50. 50. TAPI val function val (rv in rvtype) return varchar2 is begin 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;
  51. 51. 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.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;
  52. 52. 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.”
  53. 53. 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;
  54. 54. What about queries? Tuning a complex, general-purpose query is more difficult than tuning a complex, single-purpose query.
  55. 55. Generating Code • Only PL/SQL • Templates compiled in the schema • Simple syntax • Sub-templates (“includes”) for extensibility
  56. 56. OraOpenSource TAPI • Runs on NodeJS • Uses Handlebars for template processing • • Early stages, needs contributors
  57. 57. 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}};
  58. 58. oddgen • SQL*Developer plugin • Code generator, including TAPIs • Support now added in jk64 Apex TAPI generator
  59. 59. Takeaways Be Consistent Consider Your Successors
  60. 60. Thank you