Untitled Document
   
 
Indicazioni inerenti le prestazioni per il data tier con JDBC IV parte
di John Goodson, traduzione di Antonella Bellettini

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