building maintainable applications in apex
TRANSCRIPT
![Page 1: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/1.jpg)
Building Maintainable Applications in Apex
Jeffrey Kemp
AUSOUG Perth, November 2014
![Page 2: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/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: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/3.jpg)
Agenda
Maintainable PL/SQL
Case Study
MVC
TAPIs & XAPIs
Starting your Next App
![Page 4: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/4.jpg)
3 Controversial Statements
2 Interesting Quotes
1 LOTR reference
Also
![Page 5: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/5.jpg)
Is this your application?
![Page 6: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/6.jpg)
Lots of processes and conditions
Complex interactions
Difficult to learn and refactor
Low reuse
Challenges
![Page 7: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/7.jpg)
Readable
Testable
Maintainability
![Page 8: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/8.jpg)
Maintainability is in the eye of the beholder maintainer.
Maintainability
![Page 9: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/9.jpg)
“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 (?)
![Page 10: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/10.jpg)
“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.
![Page 11: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/11.jpg)
DRY
Consistency
Naming
Single-purpose
Assertions
One exit point
Techniques
![Page 12: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/12.jpg)
#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))
![Page 13: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/13.jpg)
![Page 14: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/14.jpg)
SELECT a FROM t;
instead of
OPEN CURSOR FOR 'select a from t';
Quality PL/SQL
![Page 15: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/15.jpg)
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
![Page 16: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/16.jpg)
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
![Page 17: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/17.jpg)
Controversial Statement #1
![Page 18: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/18.jpg)
p_this
l_is
v_an
g_awful
cl_way
lol_to
bbq_write
$!#*_code
v_no v_more v_hungarian, v_please!
![Page 19: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/19.jpg)
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
![Page 20: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/20.jpg)
Be consistent.
More importantly…
![Page 21: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/21.jpg)
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
![Page 22: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/22.jpg)
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?
![Page 23: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/23.jpg)
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;
![Page 24: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/24.jpg)
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
![Page 25: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/25.jpg)
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
![Page 26: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/26.jpg)
![Page 27: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/27.jpg)
Simplified
![Page 28: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/28.jpg)
MVC Architecture
![Page 29: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/29.jpg)
Process: load
![Page 30: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/30.jpg)
Get PK value
Call TAPI to query record
Set session state for each column
load
![Page 31: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/31.jpg)
Validation
![Page 32: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/32.jpg)
Get values from session state into record
Pass record to TAPI to validate
Return error message
validate
![Page 33: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/33.jpg)
process page request
![Page 34: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/34.jpg)
Get v('REQUEST')
Get values from session state into record
Pass record to TAPI to insert, update or delete
process
![Page 35: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/35.jpg)
Parameters?
v()
APEX_UTIL.set_session_state()
(commit issue)
Apex Logic
![Page 36: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/36.jpg)
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.
![Page 37: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/37.jpg)
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
![Page 38: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/38.jpg)
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;
![Page 39: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/39.jpg)
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
![Page 40: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/40.jpg)
PROCEDURE clear_page_cache IS
BEGIN
APEX_UTIL.clear_page_cache(APEX_APPLICATION.g_flow_id);
END clear_page_cache;
clear_page_cache
![Page 41: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/41.jpg)
It’s ok to call v().
A lot.
Controversial Statement #2
![Page 42: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/42.jpg)
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;
![Page 43: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/43.jpg)
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;
![Page 44: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/44.jpg)
PKG.proc;
PL/SQL in Apex
![Page 45: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/45.jpg)
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
![Page 46: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/46.jpg)
Templates, Regions, Items, Reports, etc.
Conditions (simple)
Branches
Authorisation Schemes
Build Options
Keep in Apex
![Page 47: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/47.jpg)
Pros
Fast development
Smaller apex app
Dependency analysis
Procedural control
Refactoring
Modularity
Code re-use
Customisation
Version control
![Page 48: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/48.jpg)
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
![Page 49: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/49.jpg)
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
![Page 50: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/50.jpg)
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
![Page 51: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/51.jpg)
Process: load & process
![Page 52: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/52.jpg)
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
![Page 53: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/53.jpg)
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
![Page 54: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/54.jpg)
Downside: Updateable SQL Reports not easy
Mitigation: exception to the rule?
TAPI cons
![Page 55: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/55.jpg)
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;
![Page 56: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/56.jpg)
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;
![Page 57: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/57.jpg)
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
![Page 58: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/58.jpg)
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
![Page 59: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/59.jpg)
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;
![Page 60: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/60.jpg)
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
![Page 61: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/61.jpg)
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;
![Page 62: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/62.jpg)
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;
![Page 63: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/63.jpg)
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
![Page 64: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/64.jpg)
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;
![Page 65: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/65.jpg)
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;
![Page 66: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/66.jpg)
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;
![Page 67: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/67.jpg)
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
![Page 68: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/68.jpg)
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
![Page 69: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/69.jpg)
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
![Page 70: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/70.jpg)
Transaction-level validation
Special cross-table validation
Not always necessary
Easy to retrofit later
Code re-use made easy
XAPIs
![Page 71: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/71.jpg)
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;
![Page 72: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/72.jpg)
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?
![Page 73: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/73.jpg)
Database Architecture
Debug logging & error handling
Deployment package for DDL
Unit testing framework
Code quality standards
Getting Started
![Page 74: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/74.jpg)
Simple – one primary UI (Apex)
Database Architecture
Apex
data
integration inter- face
schemas interfaces
![Page 75: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/75.jpg)
Shared – multiple UIs
Database Architecture
Apex Apex
data x x
integration inter- face
schemas interfaces
![Page 76: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/76.jpg)
YAGNI.
Controversial Statement #3
![Page 77: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/77.jpg)
YAGNI?
“Ref Codes” Table
Translation Table
System Parameters
![Page 78: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/78.jpg)
Be Consistent
Consider Your Successors
Takeaway
![Page 79: Building Maintainable Applications in Apex](https://reader034.vdocuments.mx/reader034/viewer/2022052508/559f72491a28ab83638b45ab/html5/thumbnails/79.jpg)
Thank you jeffkemponoracle.com