Introduzione
Lo sviluppo di applicazioni JDBC mirate all'ottimizzazione
delle prestazioni non è semplice. I driver JDBC
non lanciano alcuna eccezione per segnalare i casi in
cui il codice va in esecuzione in maniera troppo lenta.
Relativamente
alle prestazioni ecco alcune linee guida di portata
generale per il miglioramento dell'efficienza di un'applicazione
JDBC; esse nascono dall'esame di numerose implementazioni
di applicazioni JDBC correntemente rilasciate. Tali
linee guida includono:
-
utilizzo appropriato dei metodi per database che sfruttano
i metadata;
-
recupero dei soli dati richiesti;
-
selezione di funzioni che ottimizzano le prestazioni;
-
gestione delle connessioni e degli aggiornamenti;
-
ideazione ed esecuzione di benchmark per la valutazione
delle prestazioni.
Nel
precedente articolo sono state presentate alcune indicazioni
sulla scelta di funzioni che ottimizzano le prestazioni.
Le seguenti regole generali sulla selezione delle connessioni
e degli aggiornamenti dovrebbero aiutare nella risoluzione
di alcuni problemi comuni di prestazione dei sistemi
JDBC.
Gestione
delle connessioni e aggiornamenti
Le linee guida di questa sessione aiuteranno a gestire
connessioni ed aggiornamenti in modo da migliorare le
prestazioni del sistema per la propria applicazione
JDBC.
Gestione delle connessioni
La gestione delle connessioni è determinante
per le prestazioni dell'applicazione. È consigliabile
ottimizzare la propria applicazione connettendosi una
sola volta e facendo uso di oggetti multipli di tipo
statement, invece di effettuare molteplici connessioni.
Inoltre si eviti di collegarsi a un data source dopo
avere stabilito una connessione iniziale.
Benché
raccogliere informazioni sul driver al momento della
connessione sia una buona consuetudine, è spesso
più efficiente farlo in un unico passaggio anziché
in due. Per esempio, alcune applicazioni stabiliscono
una connessione e poi chiamano un metodo in un componente
separato che si ricollega e raccoglie informazioni sul
driver. Applicazioni che sono ideate come entità
separate dovrebbero passare alla routine di raccolta
dei dati l'oggetto che rappresenta la connessione che
è stata stabilita, invece che stabilire una seconda
connessione.
Un'altra
cattiva abitudine è quella di connettersi e disconnettersi
parecchie volte nell'ambito dell'applicazione per eseguire
comandi SQL. Gli oggetti della connessione possono avere
associati molteplici oggetti di tipo statement. Oggetti
statement, che sono definiti come operazioni di memorizzazione
per informazioni su comandi SQL, possono gestire molteplici
comandi SQL.
Si
possono migliorare significativamente le prestazioni
facendo uso di un connection pool, specialmente per
applicazioni che si connettono su una rete o attraverso
il vasto mondo del web. L'utilizzo di un pool di connessioni
permette il riuso dei collegamenti. La chiusura delle
connessioni non interrompe fisicamente il collegamento
al database. Quando un'applicazione richiede una connessione,
se ne riusa una già attiva, evitando così
l'attività di I/O di rete necessaria per creare
una nuova connessione.
In
aggiunta alle opzioni di regolazione del pool di connessioni,
JDBC 3.0 specifica anche la semantica finalizzata a
fornire un pool di istruzioni. In modo analogo a ciò
che avviene nel connection pool, un pool di struzioni
mantiene in una cache oggetti PreparedStatement così
che possano essere riusati senza l'intervento delle
applicazioni. Per esempio, un'applicazione può
creare un oggetto PreparedStatement simile al seguente
comando SQL:
select
name, address, dept, salary from personnel
where empid = ? or name like ? or address = ?"
Quando
viene creato l'oggetto PreparedStatement, la query SQL
viene analizzata ai fini di una validazione semantica
e viene prodotto un piano di ottimizzazione della query.
Con alcuni sistemi di database quali DB2, il processo
di creazione di uno statement preparato è estremamente
costoso in termini di prestazioni. Una volta che il
prepared statement è stato concluso, un driver
compatibile con JDBC 3.0 lo pone in una cache locale
invece di rilasciarlo. Se più tardi l'applicazione
cerca di creare un prepared statement con la medesima
query SQL, come si verifica comunemente in molte applicazioni,
il driver può semplicemente recuperare lo statement
associato dalla cache locale invece che intraprendere
un viaggio di andata e ritorno verso il server attraverso
la rete e una validazione del database costosa in termini
di tempo.
Le
connessioni e la gestione degli statement dovrebbero
essere scelte prima dell'implementazione. Impiegare
tempo per una gestione ponderata delle connessioni determina
un miglioramento delle prestazioni dell'applicazione
e la rende di più facile manutenzione.
Gestione
dei commit delle transazioni
L'attività di espletamento dei commit delle transazioni
è lenta a causa dei tempi necessari per le operazioni
di I/O da disco e delle eventuali operazioni di I/O
di rete. È consigliabile disattivare l'Autocommit
utilizzando l'impostazione WSConnection.setAutoCommit(false).
Cosa
implica effettivamente un'operazione di commit? Il server
del database deve ritrasferire sul disco ogni pagina
che contenga aggiornamenti o nuovi dati. Ciò
si esplica in una scrittura sequenziale su un file journal,
il che comporta comunque un'operazione di I/O da disco.
In maniera predefinita l'Autocommit è attivo
quando ci si connette a un data source e la modalità
di Autocommit solitamente deteriora le prestazioni a
causa della significativa attività di I/O da
disco necessaria per fare il commit di tutte le operazioni.
Inoltre,
la maggior parte dei server di database non mette a
disposizione una modalità nativa di Autocommit.
Per tale tipo di server, il driver JDBC deve dichiarare
esplicitamente uno statement COMMIT e un BEGIN TRANSACTION
per ogni operazione inviata al server. In aggiunta al
grande quantitativo di richieste di operazioni di input/output
necessarie per supportare la modalità di Autocommit,
le prestazioni vengono ulteriormente compromesse dalle
tre richieste di rete per ciascun statement emesso da
un'applicazione.
Anche
se l'utilizzo di transazioni può incrementare
le prestazioni dell'applicazione, non è consigliabile
adottare questo suggerimento con troppa scioltezza.
Lasciare delle transazioni attive può determinare
una riduzione della capacità computazionale per
il fatto di mantenere delle righe bloccate (lock) per
lunghi periodi, impedendo agli altri utenti di accedere
a quelle righe. È opportuno effettuare commit
di transazioni in intervalli che permettano la massima
concorrenza.
Scelta
del corretto
modello
transazionale
Molti sistemi supportano transazioni distribuite; cioè
transazioni che lanciano connessioni multiple. Le transazioni
distribuite sono come minimo quattro volte più
lente rispetto alle transazioni normali a causa delle
operazioni di collegamento e di I/O attraverso la rete,
necessarie per la comunicazione fra tutti i componenti
coinvolti nella transazione distribuita (il driver JDBC,
il monitor di transazione e il sistema del database).
È consigliabile evitare di usare transazioni
distribuite, a meno che non siano proprio necessarie.
È invece opportuno far uso, quando possibile,
di transazioni locali. Per quanto riguarda le transazioni,
va notato che molti application server Java forniscono
di solito come predefinito un comportamento che fa uso
di transazioni distribuite.
Quindi,
al fine di ottenere per il sistema le migliori prestazioni,
l'applicazione va progettata in modo da essere eseguita
con un singolo oggetto di tipo Connection.
Utilizzo dei metodi di tipo updateXXX
Sebbene gli aggiornamenti mediante chiamate a metodi
Java non si adattino a tutti i tipi di applicazione,
gli sviluppatori dovrebbero cercare il più possibile
di effettuare in tal modo sia gli aggiornamenti che
le cancellazioni. L'utilizzo dei metodi di tipo updateXXX()
permette allo sviluppatore di aggiornare i dati senza
costruire un complesso statement SQL. In tal modo, invece,
lo sviluppatore sostituisce semplicemente nel risultato
la colonna che deve essere aggiornata e i dati che devono
essere modificati. Poi, prima di spostare il cursore
nell'insieme dei risultati, si deve invocare il metodo
updateRow() per aggiornare anche il database.
Nel
seguente frammento di codice, il valore della colonna
Age dell'oggetto rs di tipo ResultSet viene recuperato
facendo uso del metodo getInt(), mentre il metodo updateInt()
è utilizzato per aggiornare la colonna con valore
intero pari a 25. Il metodo updateRow() viene invocato
per aggiornare nel database la riga che contiene il
valore modificato.
int
n = rs.getInt("Age");
// n contains value of Age column in the resultset rs
...
rs.updateInt("Age", 25);
rs.updateRow();
Oltre
a rendere più semplice il mantenimento dell'applicazione,
gli aggiornamenti mediante l'invocazione di questi metodi
solitamente conducono a un incremento delle prestazioni.
Poiché il server del database è già
posizionato relativamente alla Select in esecuzione,
non sono necessarie costose operazioni, in termini di
prestazioni, per individuare le righe da modificare.
Se la riga deve essere individuata, il server solitamente
ha un puntatore interno alla riga disponibile (p.e.
ROWID).
Utilizzo
di getBestRowIdentifier()
Si consiglia l'utilizzo del metodo getBestRowIdentifier()
per la determinazione dell'insieme ottimale di colonne
da usare nella clausola Where per l'aggiornamento dei
dati. L'uso di pseudo-colonne spesso rende disponibile
l'accesso più veloce ai dati e tali colonne possono
essere determinate solo mediante l'utilizzo di getBestRowIdentifier().
Si
possono progettare applicazioni per trarre vantaggio
da aggiornamenti e cancellazioni posizionali. Alcune
applicazioni possono formulare la clausola Where mediante
l'utilizzo di tutte le colonne del risultato che sono
indagabili, invocando getPrimaryKeys() oppure getIndexInfo()
per trovare colonne che possono essere parte di indici
unici. Questi metodi solitamente funzionano, ma possono
portare a query alquanto complesse.
Consideriamo
il seguente esempio:
ResultSet
WSrs = WSs.executeQuery
("SELECT first_name, last_name, ssn, address, city,
state, zip
FROM emp");
// fetch data
...
WSs.executeUpdate ("UPDATE EMP SET ADDRESS = ?
WHERE first_name = ? and last_name = ? and ssn = ?
and address = ? and city = ? and state = ?
and zip = ?");
// fairly complex query
Le
applicazioni dovrebbero invocare getBestRowIdentifier()
per recuperare l'insieme ottimale di colonne (possibilmente
pseudo-colonne) che identificano un record specifico.
Molti database supportano colonne speciali che non sono
esplicitate dall'utilizzatore nella definizione della
tabella, bensì sono colonne "nascoste"
di ogni tabella (p.e. ROWID e TID). Queste pseudo-colonne
generalmente forniscono l'accesso più rapido
ai dati poiché si tratta di solito di puntatori
all'esatta locazione del record. Poiché le pseudo-colonne
non fanno parte della definizione esplicita della tabella,
non sono restituite da getColumns. Per determinare se
esistono pseudo-colonne si invoca il metodo getBestRowIdentifier().
Si
consideri nuovamente il codice precedente:
...
ResultSet WSrowid = getBestRowIdentifier()
(... "emp", ...);
...
WSs.executeUpdate ("UPDATE EMP SET ADDRESS = ?
WHERE ROWID = ?";
// fastest access to the data!
Se
il proprio data source non contiene pseudo-colonne,
allora l'insieme dei risultati di getBestRowIdentifier()
è formato dalle colonne dell'indice unico ottimale
per la specifica tabella (se esiste un indice unico).
Perciò la propria applicazione non necessita
di invocare getIndexInfo per trovare l'indice unico
di minori dimensioni.
L'articolo
è stato
pubblicato originariamente su TheServerSide.com
|