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.
9. “Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.”
- Martin Golding / John F. Woods (?)
10. “Always code as if the guy who ends up maintaining your code:
is reasonably smart but has not read it all;
knows about 50% of the language; and
will probably present some of your code at a future user group meeting.”
- J.K.
19. FUNCTION get_name
(event_id IN events.event_id%TYPE)
RETURN events.event_name%TYPE IS
event_name events.event_name%TYPE;
BEGIN
IF event_id IS NOT NULL THEN
SELECT e.event_name INTO event_name
FROM events e
WHERE e.event_id = get_name.event_id;
END IF;
RETURN event_name;
END get_name;
Aliases
table alias
function alias
21. FUNCTION get_name
(event_id IN events.event_id%TYPE)
RETURN events.event_name%TYPE IS
event_name events.event_name%TYPE;
BEGIN
IF event_id IS NOT NULL THEN
SELECT e.event_name INTO event_name
FROM events e
WHERE e.event_id = get_name.event_id;
END IF;
RETURN event_name;
END get_name;
Clean code
22. FUNCTION get_name (event_id IN events.event_id%TYPE)
RETURN events.event_name%TYPE
IS event_name events.event_name%TYPE;
BEGIN
IF event_id IS NOT NULL THEN
SELECT events.event_name
INTO event_name
FROM events
WHERE events.event_id = get.event_id;
END IF; RETURN event_name;
END get;
“Ugly” code?
23. Fun for future maintainers
PROCEDURE rtrv_evnamev1
(p_no NUMBER
,p_nm OUT VARCHAR2
,p_dt OUT DATE) IS
BEGIN
--IF p_id < -100 THEN
-- g_nm := 'Invalid';
--END IF;
FOR r IN (SELECT event_name, start_date
FROM events
WHERE event_id = p_no ) LOOP
p_nm := r.event_name;
p_dt := r.start_date;
END LOOP;
UPDATE events SET start_date = TRUNC(start_date) WHERE event_id = p_no;
END;
25. Large government department
New Apex 4.2 app
Co-hosted with eBus database instance
Integrate with Oracle Financials and OBIEE
Interfaces with other transactional systems
In-house experience - PL/SQL
Project Background
36. logic
PROCEDURE set_session_state
(p_name IN VARCHAR2, p_value IN VARCHAR2) AS
BEGIN
IF v(p_name) = p_value OR (v(p_name) IS NULL AND p_value IS NULL) THEN
UPDATE apex_session_data SET item_value = p_value WHERE session_id = nv('SESSION') AND app_id = nv('APP_ID')
AND item_name = p_name;
COMMIT;
END IF;
END set_session_state;
DISCLAIMER: this is fictional code!!! WARNING: DO NOT copy & paste!!!
If the value has changed…
update it…
then commit.
37. PROCEDURE sv
(p_name IN VARCHAR2
,p_value IN VARCHAR2 := NULL) AS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
APEX_UTIL.set_session_state
(p_name => p_name
,p_value => p_value);
COMMIT;
END sv;
Wrapper for set_session_state
38. Process a page request
PROCEDURE p9_process IS
rv VOLUNTEERS$TAPI.rvtype;
r VOLUNTEERS$TAPI.rowtype;
BEGIN
CASE
WHEN APEX_APPLICATION.g_request IN ('CREATE','SAVE') THEN
rv := p9_get;
IF rv.vol_id IS NULL THEN
r := VOLUNTEERS$TAPI.ins (rv => rv);
success('Record created.');
ELSE
r := VOLUNTEERS$TAPI.upd (rv => rv);
success('Record updated.');
END IF;
p9_set(r);
WHEN APEX_APPLICATION.g_request = 'DELETE' THEN
VOLUNTEERS$TAPI.del
(vol_id => nv('P9_VOL_ID'));
success('Record deleted.');
clear_page_cache;
ELSE
NULL;
END CASE;
END p9_process;
39. PROCEDURE success (msg IN VARCHAR2) IS
BEGIN
IF APEX_APPLICATION.g_print_success_message
IS NOT NULL THEN
APEX_APPLICATION.g_print_success_message
:= APEX_APPLICATION.g_print_success_message || '<br>';
END IF;
APEX_APPLICATION.g_print_success_message
:= APEX_APPLICATION.g_print_success_message || msg;
END success;
Set success message
40. PROCEDURE clear_page_cache IS
BEGIN
APEX_UTIL.clear_page_cache(APEX_APPLICATION.g_flow_id);
END clear_page_cache;
clear_page_cache
41. It’s ok to call v().
A lot.
Controversial Statement #2
45. 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
Always alias everything
SQL in Apex
46. Templates, Regions, Items, Reports, etc.
Conditions (simple)
Branches
Authorisation Schemes
Build Options
Keep in Apex
47. Pros
Fast development
Smaller apex app
Dependency analysis
Procedural control
Refactoring
Modularity
Code re-use
Customisation
Version control
48. Misspelled/missing item names
Mitigation: isolate all apex code in one package
Enforce naming conventions – e.g. P1_COLUMN_NAME
Apex Advisor doesn’t check database package code
Performance?
Cons
49. All v() calls at start of proc, once per item
All sv() calls at end of proc
Dynamic Actions calling PL/SQL – use parameters
Replace PL/SQL with Javascript (where possible)
- maintainable?
Performance
50. Validate - only record-level validation
Cross-record validation – db constraints or XAPI
Capture DUP_KEY_ON_VALUE and ORA-02292 for unique and referential constraints
RAISE APPLICATION_ERROR (-20000, 'User-friendly error message');
Error Handling
52. Encapsulate all DML for a table
Row-level validation
Detect lost updates
Don’t write first cut by hand – generate them all
(first cut doesn’t have to be 100% perfect)
TAPIs
53. Record types
rowtype, arraytype, validation record
Functions/Procedures
ins / upd / del / merge / get as needed
bulk_ins / bulk_upd / bulk_merge / get_all as needed
Constants for enumerations
All parameters are VARCHAR2 or TAPI record/array types
TAPI contents
55. TAPI example (variation 1)
PACKAGE XXSPS_PAYMENT_GATEWAYS$TAPI AS
SUBTYPE rowtype IS xxsps_payment_gateways%ROWTYPE;
TYPE arraytype IS TABLE OF rowtype INDEX BY BINARY_INTEGER;
FUNCTION val
(pg_name IN VARCHAR2
,pg_notes IN VARCHAR2
,pg_start_date IN VARCHAR2 := NULL
,pg_end_date IN VARCHAR2 := NULL
) RETURN VARCHAR2;
FUNCTION ins
(pg_name IN VARCHAR2
,pg_notes IN VARCHAR2
,pg_start_date IN VARCHAR2 := NULL
,pg_end_date IN VARCHAR2 := NULL
) RETURN rowtype;
FUNCTION upd
(pg_id IN NUMBER
,pg_name IN VARCHAR2
,pg_notes IN VARCHAR2
,pg_start_date IN VARCHAR2 := NULL
,pg_end_date IN VARCHAR2 := NULL
,version_id IN NUMBER
) RETURN rowtype;
PROCEDURE del
(pg_id IN NUMBER
,version_id IN NUMBER);
FUNCTION get (pg_id IN NUMBER) RETURN rowtype;
FUNCTION get_id (pg_name IN VARCHAR2) RETURN NUMBER;
FUNCTION get_next (yr_year IN NUMBER := NULL) RETURN NUMBER;
FUNCTION is_valid (pg_id IN NUMBER) RETURN BOOLEAN;
END XXSPS_PAYMENT_GATEWAYS$TAPI;
56. TAPI example (variation 2)
PACKAGE XXSPS_PAYMENT_GATEWAYS$TAPI AS
SUBTYPE rowtype IS xxsps_payment_gateways%ROWTYPE;
TYPE arraytype IS TABLE OF rowtype INDEX BY BINARY_INTEGER; TYPE rvtype IS RECORD (pg_id NUMBER ,pg_name VARCHAR2(4000) ,pg_notes VARCHAR2(4000) ,pg_start_date VARCHAR2(4000) ,pg_end_date VARCHAR2(4000) ,version_id NUMBER);
FUNCTION val (rv IN rvtype) RETURN VARCHAR2;
FUNCTION ins (rv IN rvtype) RETURN rowtype;
FUNCTION upd (rv IN rvtype) RETURN rowtype;
PROCEDURE del (rv IN rvtype);
FUNCTION get (pg_id IN NUMBER) RETURN rowtype;
FUNCTION get_id (rv IN rvtype) RETURN NUMBER;
FUNCTION get_next (yr_year IN NUMBER := NULL) RETURN NUMBER;
FUNCTION is_valid (pg_id IN NUMBER) RETURN BOOLEAN;
END XXSPS_PAYMENT_GATEWAYS$TAPI;
57. FUNCTION get (pg_id IN NUMBER) RETURN rowtype IS
r rowtype;
BEGIN
IF pg_id IS NOT NULL THEN
SELECT x.* INTO r
FROM xxsps_payment_gateways x
WHERE x.pg_id = get.pg_id;
END IF;
RETURN r;
END get;
TAPI get
58. FUNCTION get (pg_id IN NUMBER) RETURN rowtype IS
r rowtype;
BEGIN UTIL_PKG.log_start($$PLSQL_UNIT, 'get');
IF pg_id IS NOT NULL THEN
SELECT x.* INTO r
FROM xxsps_payment_gateways x
WHERE x.pg_id = get.pg_id;
END IF;
UTIL_PKG.log_end(r.pg_id || ',' || r.pg_name);
RETURN r;
EXCEPTION
WHEN NO_DATA_FOUND THEN
UTIL_PKG.log_end('NO_DATA_FOUND');
RAISE;
WHEN UTIL_PKG.application_error THEN
UTIL_PKG.log_end('application_error');
RAISE;
WHEN OTHERS THEN
UTIL_PKG.raise_error(SQLERRM);
END get;
Unexpurgated Version
59. TAPI ins
FUNCTION ins
(pg_name IN VARCHAR2
,pg_notes IN VARCHAR2
,pg_start_date IN VARCHAR2 := NULL
,pg_end_date IN VARCHAR2 := NULL
) RETURN rowtype IS
r rowtype;
error_msg VARCHAR2(32767);
BEGIN
error_msg := val
(pg_name => pg_name
,pg_notes => pg_notes
,pg_start_date => pg_start_date
,pg_end_date => pg_end_date);
IF error_msg IS NOT NULL THEN
UTIL_PKG.raise_error(error_msg);
END IF;
INSERT INTO xxsps_payment_gateways
(pg_id
,pg_name
,pg_notes
,pg_start_date
,pg_end_date)
VALUES(XXSPS_PG_ID_SEQ.NEXTVAL
,ins.pg_name
,ins.pg_notes
,TO_DATE(ins.pg_start_date,UTIL_PKG.DATE_FORMAT)
,TO_DATE(ins.pg_end_date,UTIL_PKG.DATE_FORMAT))
RETURNING
pg_id
,...
INTO r.pg_id
,r....;
RETURN r;
END ins;
60. FUNCTION val
(pg_name IN VARCHAR2
,pg_notes IN VARCHAR2
,pg_start_date IN VARCHAR2 := NULL
,pg_end_date IN VARCHAR2 := NULL
) RETURN VARCHAR2 IS
buf VARCHAR2(32767);
BEGIN
UTIL_PKG.val_not_null (val => pg_name, label => 'Gateway Name', buf => buf);
UTIL_PKG.val_max_len (val => pg_name, len => 30, label => 'Gateway Name', buf => buf);
UTIL_PKG.val_max_len (val => pg_notes, len => 300, label => 'Notes', buf => buf);
UTIL_PKG.val_date_range
(start_date => pg_start_date
,end_date => pg_end_date
,label => 'Gateway'
,buf => buf);
RETURN buf;
END val;
TAPI val
61. TAPI upd (page 1 of 2)
FUNCTION upd
(pg_id IN NUMBER
,pg_name IN VARCHAR2
,pg_notes IN VARCHAR2
,pg_start_date IN VARCHAR2 := NULL
,pg_end_date IN VARCHAR2 := NULL
,version_id IN NUMBER
) RETURN rowtype IS
r rowtype;
error_msg VARCHAR2(32767);
BEGIN
UTIL_PKG.assert(pg_id IS NOT NULL ,'pg_id cannot be NULL');
UTIL_PKG.assert(version_id IS NOT NULL ,'version_id cannot be NULL');
error_msg := val
(pg_name => pg_name
,pg_notes => pg_notes
,pg_start_date => pg_start_date
,pg_end_date => pg_end_date);
IF error_msg IS NOT NULL THEN
UTIL_PKG.raise_error(error_msg);
END IF;
62. TAPI upd (page 2 of 2)
UPDATE xxsps_payment_gateways x
SET x.pg_name = upd.pg_name
,x.pg_notes = upd.pg_notes
,x.pg_start_date = TO_DATE(upd.pg_start_date ,UTIL_PKG.DATE_FORMAT)
,x.pg_end_date = TO_DATE(upd.pg_end_date ,UTIL_PKG.DATE_FORMAT)
WHERE x.pg_id = upd.pg_id
AND x.version_id = upd.version_id
RETURNING
pg_id
,...
INTO r.pg_id
,r....;
IF SQL%NOTFOUND THEN
RAISE UTIL_PKG.lost_update;
END IF;
RETURN r;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
UTIL_PKG.raise_dup_val_on_index;
WHEN UTIL_PKG.lost_update THEN
lost_upd (pg_id => pg_id);
END upd;
63. PROCEDURE lost_upd (pg_id IN NUMBER) IS
db_last_updated_by xxsps_payment_gateways.last_updated_by%TYPE;
db_last_updated_date xxsps_payment_gateways.last_updated_date%TYPE;
BEGIN
SELECT x.last_updated_by
,x.last_updated_date
INTO lost_upd.db_last_updated_by
,lost_upd.db_last_updated_date
FROM xxsps_payment_gateways x
WHERE x.pg_id = lost_upd.pg_id;
UTIL_PKG.raise_lost_update_error
(last_updated_by => db_last_updated_by
,last_updated_date => db_last_updated_date);
EXCEPTION
WHEN NO_DATA_FOUND THEN
UTIL_PKG.raise_error('LOST_UPDATE_DELETED');
END lost_upd;
Lost update handler
64. PROCEDURE del
(pg_id IN NUMBER
,version_id IN NUMBER) IS
r rowtype;
error_msg VARCHAR2(32767);
BEGIN
UTIL_PKG.assert(pg_id IS NOT NULL ,'pg_id cannot be NULL');
UTIL_PKG.assert(version_id IS NOT NULL ,'version_id cannot be NULL');
r := get(pg_id => pg_id);
-- delete child records
XXSPS_PAYMENT_GATEWAY_YEA$TAPI.del_pg (pgy_pg_id => r.pg_id);
TAPI del
DELETE xxsps_payment_gateways x
WHERE x.pg_id = del.pg_id
AND x.version_id = del.version_id;
IF SQL%NOTFOUND THEN
RAISE UTIL_PKG.lost_update;
END IF;
EXCEPTION
WHEN UTIL_PKG.ref_constraint_violation THEN
UTIL_PKG.raise_del_ref_con_violation;
WHEN UTIL_PKG.lost_update THEN
lost_upd (pg_id => pg_id);
END del;
65. PROCEDURE bulk_ins (arr IN arraytype) IS
i BINARY_INTEGER;
error_msg VARCHAR2(32767);
BEGIN
i := arr.FIRST;
LOOP
EXIT WHEN i IS NULL;
error_msg := val
(ftl_ft_id => arr(i).ftl_ft_id
,ftl_line_no => arr(i).ftl_line_no
,ftl_line_type => arr(i).ftl_line_type
,ftl_line_description => arr(i).ftl_line_description
,ftl_line_amount => arr(i).ftl_line_amount
,ftl_sch_code => arr(i).ftl_sch_code);
IF error_msg IS NOT NULL THEN
UTIL_PKG.raise_error(error_msg);
END IF;
i := arr.NEXT(i);
END LOOP;
TAPI bulk_ins
FORALL i IN INDICES OF arr
INSERT INTO xxsps_funding_trans_lines
(ftl_ft_id
,ftl_line_no
,ftl_line_type
,ftl_line_description
,ftl_line_amount
,ftl_sch_code)
VALUES (arr(i).ftl_ft_id
,arr(i).ftl_line_no
,arr(i).ftl_line_type
,arr(i).ftl_line_description
,arr(i).ftl_line_amount
,arr(i).ftl_sch_code);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
XXSPS_ERROR.raise_dup_val_on_index;
END bulk_ins;
66. PROCEDURE bulk_upd
(rowids IN rowidarray
,arr IN arraytype) IS
i BINARY_INTEGER;
error_msg VARCHAR2(32767);
lost_upd_index NUMBER;
BEGIN
i := arr.FIRST;
LOOP
EXIT WHEN i IS NULL;
error_msg := val
(ftl_ft_id => arr(i).ftl_ft_id
,ftl_line_no => arr(i).ftl_line_no
,ftl_line_type => arr(i).ftl_line_type
,ftl_line_description => arr(i).ftl_line_description
,ftl_line_amount => arr(i).ftl_line_amount
,ftl_sch_code => arr(i).ftl_sch_code
,ftl_hold_payment_ind => arr(i).ftl_hold_payment_ind);
IF error_msg IS NOT NULL THEN
UTIL_PKG.raise_error(error_msg);
END IF;
i := arr.NEXT(i);
END LOOP;
TAPI bulk_upd
FORALL i IN INDICES OF arr
UPDATE xxsps_funding_trans_lines
SET ftl_line_type = arr(i).ftl_line_type
,ftl_line_description = arr(i).ftl_line_description
,ftl_line_amount = arr(i).ftl_line_amount
,ftl_sch_code = arr(i).ftl_sch_code
,ftl_hold_payment_ind = arr(i).ftl_hold_payment_ind
WHERE ROWID = rowids(i)
AND version_id = arr(i).version_id;
FOR i IN 1..arr.COUNT LOOP
IF SQL%BULK_ROWCOUNT(i) = 0 THEN
lost_upd_index := i;
RAISE UTIL_PKG.lost_update;
END IF;
END LOOP;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
UTIL_PKG.raise_dup_val_on_index;
WHEN UTIL_PKG.lost_update THEN
lost_upd (p_rowid => rowids(lost_upd_index));
END bulk_upd;
67. TAPI_PACKAGE_SPEC CONSTANT VARCHAR2(32767) := q'[
CREATE OR REPLACE PACKAGE #TAPI# AS
SUBTYPE rowtype IS #table#%ROWTYPE;
TYPE arraytype IS TABLE OF rowtype INDEX BY BINARY_INTEGER;
TYPE rvtype IS RECORD
(#REC_COLS#);
FUNCTION val (rv IN rvtype) RETURN VARCHAR2;
FUNCTION ins (rv IN rvtype) RETURN rowtype;
FUNCTION upd (rv IN rvtype) RETURN rowtype;
PROCEDURE del (rv IN rvtype);
FUNCTION get (rv IN rvtype) RETURN rowtype;
END #TAPI#;
]';
TAPI Generator
68. FUNCTION cols
(table_name IN VARCHAR2
,template IN VARCHAR2
,sep IN VARCHAR2
) RETURN VARCHAR2 IS
buf VARCHAR2(32767);
BEGIN
FOR r IN (
SELECT c.column_name
FROM user_tab_cols c
WHERE c.table_name = UPPER(cols.table_name)
AND c.virtual_column = 'NO'
AND c.hidden_column = 'NO'
ORDER BY c.column_id
) LOOP
IF buf IS NOT NULL THEN
buf := buf || CHR(12) || sep;
END IF;
buf := buf || REPLACE(template, '#col#', LOWER(r.column_name));
END LOOP;
RETURN buf;
END cols;
TAPI Generator
69. PROCEDURE create_tapi (table_name IN VARCHAR2) IS
buf CLOB;
BEGIN
buf := REPLACE(TAPI_PACKAGE_SPEC, '#TAPI#', SUBSTR(table_name,25) || '$TAPI');
buf := REPLACE(TAPI_PACKAGE_SPEC, '#table#', table_name);
buf := REPLACE(TAPI_PACKAGE_SPEC, '#REC_COLS#'
,cols(table_name => table_name
,template => '#col# VARCHAR2(4000)'
,sep => ','));
EXECUTE IMMEDIATE buf;
-- and then similar for TAPI_PACKAGE_BODY (left as exercise for reader)
END create_tapi;
TAPI Generator
71. SUBTYPE rowtype IS xxsps_interaction_vw%ROWTYPE;
PROCEDURE submit_transaction
(hdr IN rowtype
,lines IN XXSPS_FUNDING_TRANS_LINES$TAPI.arraytype ) IS
tran XXSPS_TRANSACTION_PKG.rowtype;
ftls XXSPS_FUNDING_TRANS_LINES$TAPI.arraytype
BEGIN
val (hdr => hdr ,lines => lines);
tran := XXSPS_TRANSACTION_PKG.get_new (tt_id => hdr.tt_related_tt_id);
XAPI example
tran := XXSPS_TRANSACTION_PKG.ins
(ft_si_id => hdr.si_id
,ft_tt_id => hdr.tt_related_tt_id
,ft_description => tran.ft_description
,ft_yr_year => hdr.si_yr_year
,ft_pg_id => NVL(hdr.si_pg_id, tran.ft_pg_id)
,ft_salary_amount => tran.ft_salary_amount
,ft_cash_amount => tran.ft_cash_amount);
-- copy the new ID to the child rows
FOR i IN 1..lines.COUNT LOOP
ftls(i) := lines(i);
ftls(i).ftl_ft_id := tran.ft_id;
END LOOP;
XXSPS_FUNDING_TRANS_LINES$TAPI.bulk_ins(arr => ftls);
END submit_interaction;
72. Steven Feuerstein: “Don’t Repeat SQL Statements”
Call TAPI functions that return record arrays, ref cursors or use a table function
Conversely: Tuning a complex, general-purpose query is more difficult than tuning a complex, single-purpose query.
What about queries?
73. Database Architecture
Debug logging & error handling
Deployment package for DDL
Unit testing framework
Code quality standards
Getting Started
74. Simple – one primary UI (Apex)
Database Architecture
Apex
data
integration
inter- face
schemas
interfaces
75. Shared – multiple UIs
Database Architecture
Apex
Apex
data
x
x
integration
inter- face
schemas
interfaces