Building Maintainable Applications in Apex
-
Upload
jeffrey-kemp -
Category
Software
-
view
784 -
download
0
Transcript of Building Maintainable Applications in Apex
Building Maintainable Applications in Apex
Jeffrey Kemp
AUSOUG Perth, November 2014
All artifacts including code are presented for illustration
purposes only. Use at your own risk. Test thoroughly in
a non-critical environment before use.
Agenda
Maintainable PL/SQL
Case Study
MVC
TAPIs & XAPIs
Starting your Next App
3 Controversial Statements
2 Interesting Quotes
1 LOTR reference
Also
Is this your application?
Lots of processes and conditions
Complex interactions
Difficult to learn and refactor
Low reuse
Challenges
Readable
Testable
Maintainability
Maintainability is in the eye of the beholder maintainer.
Maintainability
“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 (?)
“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.
DRY
Consistency
Naming
Single-purpose
Assertions
One exit point
Techniques
#include "SDL.h" #define $ for(O=9 #define CX M+=(T%3+2*!(!T*t-6)) #define x ,A=4*!T,O=t,W=h=T<3?u(Q?p:D(A+3),D(A),D(A+1)[i]+D(A+2)*g+):K(t),U=V=K(a),o?U=h,W=V:V, #define C 8*-~L #define Z short #define y a(Z)Y[++O] #define B ),a--||( #define _ ),e--||( #define V(I,D,E)(O=a(I)h[r])&&!(A=(D)(V=(1[E+L]<<16)+*i)/O,A-(I)A)?1[E+L]=V-O*(*E=A):H(0) #define i(B,M)B(o){return M;} #define R(O,M,_)(S=L?a(I Z)O:O,N=L?a(I Z)O M(f=a(I Z)_):(O M(f=a(I n)_))) #define T(_)R(r[u(10,L=4,--)],=,_) #define u(a,r,T)16*i[a]+(I Z)(T i[r]) #define a(_)*(_*)& #define L(_)M(W,_,U) #define M(S,F,T)R(r[S],F,r[T]) #define A(_)(i[L=4]+=2,R(_,=,r[u(10,4,-2+)])) #define c(R,T)(1[u=19,L+T]=(N=a(R)h[r]*(R)*T)>>16,*i=N,G(F(N-(R)N))) #define h(_)(1&(L?a(Z)_:_)>>C-1) #define I unsigned #define n char #define e(_)v(F(40[L(_##=40[E]+),E]&N==S|_ N<_(int)S)) I n t,e,l[80186],*E,m,u,L,a,T,o,r[1<<21],X,*Y,b,Q,R;I Z*i,M,p,q=3;I*localtime(),f,S,kb,h,W,U,c,g,d,V,A;N,O,P=983040,j[5];SDL_Surface*k;i(F,40[E]=!!o)i(z,42[E]=!!o)i(D,r[a(I)E[259+4*o]+O])i(w,i[o]+=~(-2*47[E])*~L)i(v,G(N-S&&1&(40[z((f^=S^N)&16),E]^f>>C-1)))J(){V=61442;$;O--;)V+=40[E+O]<<D(25);}i(H,(46[u=76,J(),T(V),T(9[i]),T(M),M(P+18,=,4*o+2),R(M,=,r[4*o]),E]=0))s(o){$;O--;)40[E+O]=1&&1<<D(25)&o;}i(BP,(*i+=262*o*z(F((*E&15)>9|42[E])),*E&=15))i(SP,(w(7),R&&--1[i]&&o?R++,Q&&Q++,M--:0))DX(){$,O*=27840;O--;)O[(I*)k->pixels]=-!!(1<<7-O%8&r[O/2880*90+O%720/8+(88+952[l]/128*4+O/720%4<<13)]);SDL_Flip(k);}main(BX,nE)n**nE;{9[i=E=r+P]=P>>4;$;q;)j[--q]=*++nE?open(*nE,32898):0;read(2[a(I)*i=*j?lseek(*j,0,2)>>9:0,j],E+(M=256),P);$;Y=r+16*9[i]+M,Y-r;Q|R||kb&46[E]&&KB)--64[T=1[O=32[L=(X=*Y&7)&1,o=X/2&1,l]=0,t=(c=y)&7,a=c/8&7,Y]>>6,g=~-T?y:(n)y,d=BX=y,l],!T*t-6&&T-2?T-1?d=g:0:(d=y),Q&&Q--,R&&R--x(O=*Y,O=u=D(51),e=D(8),m=D(14)_ O=*Y/2&7,M+=(n)c*(L^(D(m)[E]|D(22)[E]|D(23)[E]^D(24)[E]))_ L=*Y&8,R(K(X)[r],=,c)_ L=e+=3,o=0,a=X x a=m _ T(X[i])_ A(X[i])_ a<2?M(U,+=1-2*a+,P+24),v(f=1),G(S+1-a==1<<C-1),u=u&4?19:57:a-6?CX+2,a-3||T(9[i]),a&2&&T(M),a&1&&M(P+18,=,U+2),R(M,=,U[r]),u=67:T(h[r])_(W=U B u=m,M-=~L,R(W[r],&,d)B 0 B L(=~)B L(=-),S=0,u=22,F(N>S)B L?c(I Z,i):c(I n,E)B/**/L?c(Z,i):c(n,E)B L?V(I Z,I,i):V(I n,I Z,E)B L?V(Z,int,i):V(n,Z,E))_++e,h=P,d=c,T=3,a=m,M--_++e,13[W=h,i]=(o|=!L)?(n)d:d,U=P+26,M-=~!o,u=17+(m=a)_(a=m B L(+=),F(N<S)B L(|=)B e(+)B e(-)B L(&=)B L(-=),F(N>S)B L(^=)B L(-),F(N>S)B L(=))_!L?L=a+=8 x L(=):!o?Q=1,R(r[p=m x V],=,h):A(h[r])_ T=a=0,t=6,g=c x M(U,=,W)_(A=h(h[r]),V=m?++M,(n)g:o?31&2[E]:1)&&(a<4?V%=a/2+C,R(A,=,h[r]):0,a&1?R(h[r],>>=,V):R(h[r],<<=,V),a>3?u=19:0,a<5?0:F(S>>V-1&1)B R(h[r],+=,A>>C-V),G(h(N)^F(N&1))B A&=(1<<V)-1,R(h[r],+=,A<<C-V),G(h(N*2)^F(h(N)))B R(h[r],+=(40[E]<<V-1)+,A>>1+C-V),G(h(N)^F(A&1<<C-V))B R(h[r],+=(40[E]<<C-V)+,A<<1+C-V),F(A&1<<V-1),G(h(N)^h(N*2))B G(h(N)^F(h(S<<V-1)))B G(h(S))B 0 B V<C||F(A),G(0),R(h[r],+=,A*=~((1<<C)-1>>V)))_(V=!!--1[a=X,i]B V&=!m[E]B V&=m[E]B 0 B V=!++1[i]),M+=V*(n)c _ M+=3-o,L?0:o?9[M=0,i]=BX:T(M),M+=o*L?(n)c:c _ M(U,&,W)_ L=e+=8,W=P,U=K(X)_!R||1[i]?M(m<2?u(8,7,):P,=,m&1?P:u(Q?p:11,6,)),m&1||w(6),m&2||SP(1):0 _!R||1[i]?M(m?P:u(Q?p:11,6,),-,u(8,7,)),43[u=92,E]=!N,F(N>S),m||w(6),SP(!N==b):0 _ o=L,A(M),m&&A(9[i]),m&2?s(A(V)):o||(4[i]+=c)_ R(U[r],=,d)_ 986[l]^=9,R(*E,=,l[m?2[i]:(n)c])_ R(l[m?2[i]:(n)c],=,*E)_ R=2,b=L,Q&&Q++_ W-U?L(^=),M(U,^=,W),L(^=):0 _ T(m[i])_ A(m[i])_ Q=2,p=m,R&&R++_ L=0,O=*E,F(D(m+=3*42[E]+6*40[E])),z(D(1+m)),N=*E=D(m-1)_ N=BP(m-1)_ 1[E]=-h(*E)_ 2[i]=-h(*i)_ 9[T(9[i]),T(M+5),i]=BX,M=c _ J(),T(V)_ s(A(V))_ J(),s((V&~m)+1[E])_ J(),1[E]=V _ L=o=1 x L(=),M(P+m,=,h+2)_++M,H(3)_ M+=2,H(c&m)_++M,m[E]&&H(4)_(c&=m)?1[E]=*E/c,N=*E%=c:H(0)_*i=N=m&E[L=0]+c*1[E]_*E=-m[E]_*E=r[u(Q?p:m,3,*E+)]_ m[E]^=1 _ E[m/2]=m&1 _ R(*E,&,c)_(a=c B write(1,E,1)B time(j+3),memcpy(r+u(8,3,),localtime(j+3),m)),a<2?*E=~lseek(O=4[E][j],a(I)5[i]<<9,0)?((I(*)())(a?write:read))(O,r+u(8,3,),*i):0:0),O=u,D(16)?v(0):D(17)&&G(F(0)),CX*D(20)+D(18)-D(19)*~!!L,D(15)?O=m=N,41[43[44[E]=h(N),E]=!N,E]=D(50):0,!++q?kb=1,*l?SDL_PumpEvents(),k=k?k:SDL_SetVideoMode(720,348,32,0),DX():k?SDL_Quit(),k=0:0:0;}i(G,48[E]=o)i(K,P+(L?2*o:2*o+o/4&7))
SELECT a FROM t;
instead of
OPEN CURSOR FOR 'select a from t';
Quality PL/SQL
max_lines_per_transaction CONSTANT NUMBER := 1000;
IF line_count > max_lines_per_transaction THEN
instead of
IF line_count > 1000 THEN
Quality PL/SQL
TRANSACTION_PKG.submit (tran_id => r.tran_id ,source_id => NULL ,description => r.description ,approved_by => r.updated_by); instead of TRANSACTION_PKG.submit (r.tran_id, NULL, r.description, r.updated_by);
Quality PL/SQL
Controversial Statement #1
p_this
l_is
v_an
g_awful
cl_way
lol_to
bbq_write
$!#*_code
v_no v_more v_hungarian, v_please!
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
Be consistent.
More importantly…
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
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?
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;
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'));
Package names as context
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
Simplified
MVC Architecture
Process: load
Get PK value
Call TAPI to query record
Set session state for each column
load
Validation
Get values from session state into record
Pass record to TAPI to validate
Return error message
validate
process page request
Get v('REQUEST')
Get values from session state into record
Pass record to TAPI to insert, update or delete
process
Parameters?
v()
APEX_UTIL.set_session_state()
(commit issue)
Apex Logic
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.
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
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;
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
PROCEDURE clear_page_cache IS
BEGIN
APEX_UTIL.clear_page_cache(APEX_APPLICATION.g_flow_id);
END clear_page_cache;
clear_page_cache
It’s ok to call v().
A lot.
Controversial Statement #2
get_row FUNCTION p9_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 p9_get;
set row PROCEDURE p9_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 p9_set;
PKG.proc;
PL/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
Always alias everything
SQL in Apex
Templates, Regions, Items, Reports, etc.
Conditions (simple)
Branches
Authorisation Schemes
Build Options
Keep in Apex
Pros
Fast development
Smaller apex app
Dependency analysis
Procedural control
Refactoring
Modularity
Code re-use
Customisation
Version control
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
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
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
Process: load & process
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
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
Downside: Updateable SQL Reports not easy
Mitigation: exception to the rule?
TAPI cons
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;
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;
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
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
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;
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
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;
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;
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
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;
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;
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;
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
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
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
Transaction-level validation
Special cross-table validation
Not always necessary
Easy to retrofit later
Code re-use made easy
XAPIs
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;
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?
Database Architecture
Debug logging & error handling
Deployment package for DDL
Unit testing framework
Code quality standards
Getting Started
Simple – one primary UI (Apex)
Database Architecture
Apex
data
integration inter- face
schemas interfaces
Shared – multiple UIs
Database Architecture
Apex Apex
data x x
integration inter- face
schemas interfaces
YAGNI.
Controversial Statement #3
YAGNI?
“Ref Codes” Table
Translation Table
System Parameters
Be Consistent
Consider Your Successors
Takeaway
Thank you jeffkemponoracle.com