Untitled Document
   
 
Indicazioni inerenti le prestazioni per il data tier con JDBC III parte
di Jhon 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 quando il codice in esecuzione è tropo lento.

La seguente serie di indicazioni relative alle prestazioni presenta alcune linee guida di valenza generale per il miglioramento dell'efficienza di un'applicazioni JDBC, la cui stesura è stata realizzata a seguito dell'esame di numerose implementazioni di applicazioni JDBC correntemente rilasciate. Queste 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

Negli ultimi suggerimenti relativi alle prestazioni sono state presentate alcune indicazioni su come ottenere il recupero dei soli dati che effettivamente si sono richiesti. Le seguenti regole generali sulla selezione di funzioni che ottimizzino le prestazioni dovrebbero aiutare a risolvere alcuni problemi comuni di prestazione dei sistemi JDBC.
Le linee guida presentate in questo articolo aiuteranno a selezionare i metodi e gli oggetti JDBC che forniscono le prestazioni migliori.

 

Utilizzo di parametri posizionali come argomenti delle stored procedure
Nelle chiamate a stored procedure, vanno sempre utilizzati parametri posizionali per gli argomenti posizionali invece che argomenti letterali. I driver JDBC possono chiamare stored procedure sul server del database sia eseguendo tali procedure come una qualsiasi altra query SQL, che ottimizzando l'esecuzione mediante chiamata ad una Remote Procedure Call (RCP) direttamente dentro il server del database. Quando la stored procedure viene eseguita come una query SQL, il server del database analizza il comando, abilita i tipi di argomenti e converte questi ultimi nel tipo di dato corretto.

Si ricordi che l'SQL è sempre inviato al server come una stringa di caratteri, per esempio "{call getCustName (12345)}". In questo caso, anche se il programmatore dell'applicazione può assumere che l'unico argomento di getCustName sia un intero, in realtà l'argomento viene passato al server all'interno di una stringa di caratteri. Il server del database analizzerebbe la query SQL, consulterebbe i metadata del database per determinare l'insieme dei tipi dei parametri della procedura, isolerebbe il valore del singolo argomento 1234, poi convertirebbe la stringa '1234' in un valore intero prima di eseguire finalmente la procedura come un evento del linguaggio SQL.

Invocando una RCPC entro il server del database, si evita l'appesantimento dovuto all'utilizzo di una stringa di caratteri SQL. Infatti, diversamente da quanto visto nel caso precedente, un driver JDBC costruirà un pacchetto per la rete contenente i parametri nel formato originari del loro tipi di dato ed eseguirà la procedura in remoto.

Caso 1
In questo esempio la stored procedure non può essere ottimizzata mediante l'utilizzo di una RPC lato server. Il server del database deve trattare la richiesta SQL come l'evento di un linguaggio normale che include l'analisi del comando, l'abilitazione dei tipi d'argomento e la conversione degli argomenti nel corretto tipo di dato, prima di eseguire la procedura.

CallableStatement cstmt = conn.prepareCall ("{call getCustName (12345)}");
ResultSet rs = cstmt.executeQuery ();

Caso 2
In questo esempio, la stored procedure può essere ottimizzata mediante l'utilizzo di una RPC lato server. Poiché l'applicazione evita argomenti letterali e chiama la procedura specificando tutti gli argomenti come parametri, il driver JDBC può ottimizzare l'esecuzione chiamando la stored procedure direttamente dentro il database come una RCP. Viene evitata l'elaborazione di linguaggio SQL sul server del database e il tempo di esecuzione risulta grandemente migliorato.

CallableStatement cstmt = conn.prepareCall("{call getCustName (?)}");
cstmt.setLong (1,12345);
ResultSet rs = cstmt.executeQuery();

 

Uso dell'oggetto Statement anziché dell'oggetto PreparedStatement
I driver JDBC vengono ottimizzati sulla base della percezione di utilizzo delle funzioni che sono in esecuzione. La scelta fra l'oggetto PreparedStatement e l'oggetto Statement dipende dall'uso pianificato. L'oggetto Statement è ottimizzato per un'esecuzione singola di un comando SQL. Al contrario, l'oggetto PreparedStatement è ottimizzato per comandi SQL da eseguire due o più volte.

Il sovraccarico per l'esecuzione iniziale di un oggetto PreparedStatement è notevole. Il vantaggio viene nelle seguenti riesecuzioni del comando SQL. Per esempio, si supponga di preparare e poi eseguire una query che restituisca informazioni su un dipendente basandosi su un ID. La via più probabile in cui il driver JDBC elaborerà la richiesta preparata sarà l'inoltro, attraverso la rete, di una domanda al server del database per analizzare ed ottimizzare la query. L'esecuzione, poi, si traduce in un'altra richiesta di rete. All'interno dei driver JDBC la riduzione del traffico di rete è l'ultima delle priorità. Se l'applicazione sottopone una tale richiesta una sola volta durante la sua vita, allora va usato l'oggetto Statement. Con un oggetto Statement, l'esecuzione della medesima query si traduce semplicemente in un singolo viaggio di andata e ritorno verso e dal il server del database attraverso la rete.

Queste linee guida vengono complicate dall'uso di raggruppamenti (pool) di prepared statement poiché lo scope dell'esecuzione risulta più lungo. Quando si utilizzano pool di prepared statement, se la query è ad-hoc e probabilmente non sarà più usata, allora va adottato l'oggetto Statement. Se la query non verrà eseguita frequentemente, ma comunque potrà esserlo più volte durante la vita del pool di statement, all'interno di una connection pool, allora va usato un PreparedStatement. Nelle medesime circostanze, qualora non sia presente un pool di statement, va utilizzato un oggetto Statement.

 

Uso di batch anziché prepared statement
La preparazione di un comando di INSERT e la sua esecuzione ripetuta richiede tipicamente l'aggiornamento di una grande quantità di dati, il che si traduce in numerosi viaggi di andata e ritorno attraverso la rete. Per ridurre il numero di chiamate JDBC e migliorare le prestazioni, si possono inviare in una sola volta molteplici query al database facendo uso del metodo addBatch() dell'oggetto PreparedStatement. Per esempio, confrontiamo le seguenti implementazioni, Caso 1 e Caso 2.

Caso 1: Esecuzione ripetuta di prepared statement

PreparedStatement ps = conn.prepareStatement("INSERT into employees
                                             values (?, ?, ?)");

for (n = 0; n < 100; n++) {
  ps.setString(name[n]);
  ps.setLong(id[n]);
  ps.setInt(salary[n]);
  ps.executeUpdate();
}

Caso 2: Utilizzo di un batch

PreparedStatement ps = conn.prepareStatement("INSERT into employees
                                             values (?, ?, ?)");

for (n = 0; n < 100; n++) {  
  ps.setString(name[n]);
  ps.setLong(id[n]);
  ps.setInt(salary[n]);
  ps.addBatch();
}
ps.executeBatch();

Nel Caso 1, viene utilizzato un prepared statement per eseguire molteplici volte un comando INSERT. In questo caso sono necessari 101 viaggi di andata e ritorno sulla rete per eseguire 100 operazioni d'nserimento: 1 andata e ritorno per preparare il comando e 100 andate e ritorno per eseguire le iterazioni (una per ogni iterazione). Se invece si utilizza il metodo addBatch() per compattare 100 operazioni di INSERT, come dimostrato nel Caso 2, sono necessari solo due andate e ritorno attraverso la rete: una per preparare il comando ed un'altra per eseguire il batch. Anche se con l'utilizzo di batch sono richiesti più cicli di CPU sul database, vi è comunque un guadagno in prestazioni grazie alla riduzione di viaggi di andata e ritorno sulla rete. Si ricordi che con i driver JDBC il maggior guadagno in prestazioni si trova mediante la riduzione dello scambio di comunicazioni sulla rete fra il driver JDBC e il server del database.

 

Scelta del giusto cursore
Un'appropriata scelta del tipo di cursore permette di massimizzare la flessibilità dell'applicazione. Questa sessione riassume le caratteristiche relative alle prestazioni di tre tipi di cursore.
Un cursore di tipo forward-only fornisce ottime prestazioni nel caso di letture sequenziali di tutte le righe di una tabella. Per il recupero dei dati da tabelle non vi è metodo più veloce, per ottenere le righe dei risultati, che quello di far uso di un cursore di tipo forward-only. Comunque, tale cursore non può essere adottato quando l'applicazione deve elaborare le righe in modo non sequenziale.

L'utilizzo dei cursori di tipo insensitive da parte dei driver JDBC è ideale per applicazioni che richiedono un alto livello di concorrenza sul server del database e l'abilità di scorrere sia avanti che in dietro sui set dei risultati. La prima richiesta rivolta a un cursore di tipo insensitive fa il fetch di tutte le righe (o di molte, ma non tutte, quando il driver JDBC fa uso di un fetch "pigro") e le memorizza sul client. Perciò la prima richiesta è molto lenta, soprattutto quando si ricercano dati lunghi. Le richieste seguenti non comportano alcun traffico di rete (o un traffico limitato quando un driver utilizza un fetch "pigro") e sono elaborate velocemente. Vista la lentezza della prima elaborazione, l'uso di un cursore di tipo insensitive non è adeguato nel caso di richiesta singola di un'unica riga. Gli sviluppatori dovrebbero evitare l'utilizzo di cursori di tale tipo quando i dati che vengono restituiti sono lunghi, perché si potrebbe esaurire lo spazio di memoria disponibile. Alcune implementazioni di cursori di tipo insensitive immagazzinano i dati in tabelle temporanee sul server del database, evitando così tale problema, ma la maggior parte di questo tipo di cursori memorizza le informazioni localmente all'applicazione.

Alcuni cursori di tipo sensitive, talvolta indicati come "keyset-driven", utilizzano identificatori, quali un ROWID, che esistono già nel database. Quando si scorre il set dei risultati vengono reperiti i dati inerenti tali identificatori. Poiché ogni richiesta genera traffico di rete, l'esecuzione può essere molto lenta. Comunque, la restituzione di righe non consecutive non influenza ulteriormente le prestazioni.

Per illustrare ciò più estesamente, si consideri un'applicazione che ritornerebbe normalmente 1000 righe. Durante l'esecuzione o alla prima richiesta di una riga, un driver JDBC non eseguirebbe il comando SELECT fornito dall'applicazione. Bensì il driver JDBC sostituirebbe la lista SELECT della query con l'dentificatore di chiave, per esempio, ROWID. La query modificata sarebbe poi eseguita dal driver e tutti i valori delle 1000 chiavi verrebbero recuperati dal server del database e tenuti per essere utilizzate dal driver. Ogni richiesta, da parte dell'applicazione, di una riga del risultato, conduce il driver JDBC a cercare il valore della chiave relativo alla riga appropriata nella propria memoria locale, a costruire una query ottimizzata che contiene un'istruzione condizionale WHERE simile a 'WHERE ROWID = ?', a eseguire la query modificata e quindi a recuperare dal server la singola riga del risultato.

I cursori di tipo sensitive sono il modello preferito di cursore dinamico quando l'applicazione non può sostenere la memorizzazione localmente dei dati forniti da un cursore insensitive.

 

Utilizzo efficiente dei metodi 'get'
JDBC fornisce una varietà di metodi per il recupero di dati dal set dei risultati, quali getInt(), getString() e getObject(). Il metodo getObject() è il più generico e fornisce le prestazioni peggiori qualora vengano specificate mappature non di default. Ciò perché il driver JDBC deve fare elaborazioni extra per determinare il tipo di valore ricercato e generare la mappatura appropriata. Va utilizzato sempre il metodo specifico per il tipo di dati.
Per migliorare ulteriormente le prestazioni, si fornisce il numero della colonna ricercata, per esempio, getString(1), getLong(2) e getInt(3), invece del nome della colonna. Se il numero di colonna non è specificato, il traffico di rete non ne risente, ma aumentano le conversioni e le ricerche costose. Per esempio, si supponga di usare getString("foo") ... Un driver si trova a dover convertire il delimitatore foo in lettere maiuscole, se necessario, e poi a dover comparare "foo" con i nomi delle colonne nella loro lista. Se invece il driver andasse direttamente alla colonna 23 deli risultati, verrebbe risparmiato un quantitativo significativo di elaborazioni.
Per esempio, si supponga di avere un set di risultati con 15 colonne e 100 righe, e che i nomi delle colonne non sia incluso in tale set. Si sia interessati a tre colonne: EMPLOYEENAME (di tipo stringa), EMPLOYEENUMBER (di tipo intero lungo) e SALARY (di tipo intero). Se si specifica getString("EmployeeName"), getLong("EmployeeNunmber") e getInt("Salary"), ogni nome di colonna deve essere convertito nell'appropriato formato della colonna, nei metadata del database, e le ricerche crescono considerevolmente. Le prestazioni migliorerebbero significativamente se si specificasse getString(1), getLong(2) e getInt(15).

 

Recupero di chiavi auto-generate
Molti database hanno colonne nascoste (chiamate pseudo-colonne) che rappresentano chiavi uniche relativamente ad ogni riga in una tabella. Tipicamente l'utilizzo di questi tipi di colonne in una query costituisce la via più veloce di accesso a una riga poiché le pseudo-colonne usualmente rappresentano l'indirizzo fisico del dato sul disco. Prima della versione 3.0 di JDBC, un'applicazione poteva recuperare i valori delle pseudo-colonne solo mediante l'esecuzione di un comando SELECT immediatamente dopo l'inserimento dei dati.

//insert row
int rowcount = stmt.executeUpdate ("insert into LocalGeniusList (name)
                                   values ('Karen')");

// now get the disk address - rowid - for the newly inserted row
ResultSet rs = stmt.executeQuery ("select rowid from LocalGeniusList where
                                   name = 'Karen'");

Questo metodo di recupero delle pseudo-colonne ha due pecche principalmente. La prima è che il reperimento di pseudo-colonne richiede l'invio attraverso la rete e l'esecuzione sul server di una query separata. La seconda è che, poiché può non esservi una chiave primaria sulla tabella, la condizione di ricerca della query può non essere in grado di identificare in modo univoco la riga. In quest'ultimo caso possono essere restituiti molteplici valori di pseudo-colonna e l'applicazione può non essere in grado di determinare quale sia effettivamente quello relativo alla riga di più recente inserzione.

Una prerogativa opzionale delle specifiche di JDBC 3.0 è l'abilità di recuperare informazioni su chiavi auto-generate relative ad una riga, quando essa viene inserita in una tabella.

int rowcount = stmt.executeUpdate ("insert into LocalGeniusList (name)
                                   values ('Karen')",
// insert row AND return key
Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stmt.getGeneratedKeys ();
// key is automatically available

Ora l'applicazione contiene un valore che può essere utilizzato in una condizione di ricerca per permettere l'accesso più rapido possibile alla riga e un valore che identifica in modo univoco la riga, anche quando sulla tabella non esiste una chiave primaria.

L'abilità di recuperare chiavi auto-generate fornisce flessibilità per lo sviluppatore JDBC e determina aumenti di prestazione quando si accede ai dati.

Scelta del giusto tipo di dati
Il recupero e l'invio di certi tipi di dati può essere costoso. Quando si disegna uno schema, va selezionato il tipo di dati che può essere elaborato più efficientemente. Per esempio i dati di tipo intero sono di più facile elaborazione rispetto a quelli in virgola mobile e ai decimali. I dati in virgola mobile sono definiti secondo formati interni specifici del database, solitamente in forma compressa. I dati devono essere decompressi e convertiti in un diverso formato così da potere essere elaborati dal protocollo di basso livello del database.

Recupero dei set di risultati
La maggior parte dei driver JDBC non possono implementare cursori dinamici, a causa del supporto limitato fornito per tale tipo di cursori dal database. A meno che non si sia certi che il database supporti set di risultati scorribili (per esempio, rs), per determinare quante siano le righe presenti nel set dei risultati non si devono chiamare i metodi rs.last() e rs.getRow(). Per i driver JDBC che emulano i cursori dinamici, una chiamata ad rs.last() si traduce nel recupero, da parte loro, di tutti i risultati attraverso la rete per raggiungere l'ultima riga. Al posto di ciò si può sia contare le righe iterando sul il set dei risultati che ottenere il numero delle righe mediante l'inoltro di una query con un colonna COUNT nell'istruzione SELECT.

In generale è buona norma non scrivere codice che si appoggi sul numero di righe del risultato di una query, poiché in tal caso i driver si trovano costretti a fare il fetch di tutte le righe in esso presenti per sapere quante ne restituirà la query.

L'articolo è stato originariamente pubblicato su theserverside.com