Introduzione
JDBC è l’acronimo per Java DataBase Connectivity ed identifica un set di API standardizzate per l’accesso e la gestione della persistenza dei dati su database indipendentemente dal vendor del DBMS utilizzato.
L’architettura di JDBC prevede due componenti fondamentali: il JDBC Driver Manager ed uno o più JDBC Driver specifico per ogni DBMS. Il Driver Manager fornisce il layer di astrazione che consente alle applicazioni di interagire con la Base Dati attraverso un set di API standardizzate (JDBC API). Esso inoltre tiene traccia dei JDBC Driver disponibili per la comunicazione con i DBMS con i quali dialoga utilizzando le interfacce standard JDBC Driver API. L’immagine seguente mostra l’archetettura appena descritta.
La versione attuale ella specifica JDBC è la 4 ed è inclusa nel JDK nei package java.sql
e javax.sql
,
il secondo dei quali è opzionale in quanto utilizzato per l’integrazione con la piattaforma J2EE. Le principali componenti (classi o interfacce) definite nel package java.sql
sono:
- DriverManager: classe utilizzata per ottenere la connessione verso il database specifico;
- Connection: interfaccia di comunicazione con la Base Dati una volta stabilita la connessione;
- Statement: gli oggetti che implementano tale interfaccia sono utilizzati per sottomettere uno statement SQL al database;
- ResultSet: oggetti utilizzati per memorizzare i dati restituiti da una query SQL eseguita per mezzo di uno Statement.
A queste classi/interfacce se ne aggiungono altre utilizzate per diversi scopi, come ad esempio DatabaseMetaData, ResultSetMetaData, SQLException, etc.
Le fasi operative per l’implementazione di una applicazione che utilizza JDBC per la connessione al DB sono i seguenti:
- Registrazione driver JDBC;
- Apertura connessione al DB (Connection);
- Creazione oggetto Statement;
- Esecuzione query e restituzione oggetto ResultSet;
- Utilizzo dei risultati;
- Rilascio delle risorse.
Registrazione Driver JDBC
Per rendere disponibile il driver al Driver Manager è possibile procedere con diverse modalità operative differenti. La prima prevede di caricarlo in modo esplicito attraverso l’inizializzazione della classe che lo implementa e la sua conseguente registrazione nel Driver Manager. Tale inizializzazione si ottiene attraverso l’invocazione seguente, dove la stringa oracle.jdbc.driver.OracleDriver
identifica la classe driver richiesta:
1 |
Class.forName("oracle.jdbc.driver.OracleDriver"); |
Altra possibilità è quella di lasciare che il Driver Manager si inizializzi utilizzando la proprietà di sistema jdbc.drivers
da cui legge l’elenco di tutti i driver da caricare. In questo caso è quindi sufficiente fare:
1 |
System.setProperty("jdbc.drivers", "oracle.jdbc.driver.OracleDriver"); |
1 |
jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.taste.ourDriver |
Infine, con la versione 4 della specifica JDBC il DriverManager supporta il meccanismo dei Service Provider di Java SE. Quindi i driver per poter essere registrati dal Database manager non dovranno fare altro che includere il file java.sql.Driver
nella cartella META-INF/services/
popolandolo col nome del driver JDBC. Ad esempio nell’archivio ojdbc7.jar contenente il diver oracle il file indicato è valorizzato con la stringa: oracle.jdbc.OracleDriver
.
Apertura della Connessione
Per l’apertura della connessione verso il database il Driver Manager fornisce un metodo statico generico che richiede la conoscenza della stringa di connessione e delle credenziali di accesso al DB.
1 |
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@ORCL:1025", "scott", "tiger"); |
Sarà il Driver Manager a recuperare il driver giusto attraverso un meccanismo a cui devono conformarsi tutti i driver JDBC. Il particolare driver da usare viene selezionato attraverso l’URL che ha il seguente formato:
1 |
jdbc:<subprotocol>:<subname> |
- <subprotocol> rappresenta il sottoprotocollo ovvero il driver che si intende utilizzare come interfaccia al DB verso il DBMS;
- <subname> rappresenta il nome della risorsa che verrà elaborata dal driver in relazione a delle regole caratteristiche del driver stesso (varie modalità di specifica, in dipendenza dal DBMS).
Una volta selezionato, il driver sarà responsabile di tutte le operazioni per quella connessione.
Creazione dello Statement
Una volta ottenuta la connessione al DB, attraverso l’oggetto Connection
è possibile ottenere tre diversi tipi di oggetti che permettono di creare istruzioni SQL. Questi sono Statement
, PreparedStatement
e CallableStatement
(alcuni DBMS potrebbero non avere tutte e tre le implementazioni).
Statement |
E’ utilizzato per l’esecuzione di uno statement SQL statico, ovvero senza parametri, ed il recupero dei risultati prodotti. |
PreparedStatement |
E’ utilizzato per l’esecuzione di uno statement SQL che può avere parametri di input. |
CallableStatement |
E’ utilizzato per l’esecuzione di stored procedure che possono avere sia parametri di input che di output. |
Esecuzione della Query
Una volta ottenuto uno degli oggetti citati precedentemente non resta che eseguire la query. Vediamo come farlo nel caso di oggetto Statement
:
1 2 |
Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM utenti WHERE ID>100"); |
Si può notare che l’oggetto Statement così ottenuto non dispone di metodi per la valorizzazione di eventuali parametri presenti nella query. Lo stesso risultato si può ottenere con uno oggetto PreparedStatement
nel modo seguente:
1 2 3 |
PreparedStatement ps = conn.prepareStatement(SELECT * FROM utenti WHERE ID>?"); ps.setInt( 1, 100 ); ResultSet rs = ps.executeQuery(); |
dove il parametro di input è valorizzato utilizzando uno specifico metodo dell’oggetto che ne identifica anche il tipo.
Utilizzo dei Risultati
In entrambe i casi visti sopra, una volta eseguita la query si ottiene un oggetto di tipo ResultSet
che contiene i record risultanti dalla query e che consente, attraverso il metodo next(),
di scorrerli ed accedere ai campi dei record mediante opportuni metodi getter.
Si noti che l’oggetto ResultSet
non contiene al suo interno tutti i dati recuperati dalla query, ma in realtà fornisce un cursore che inizialmente punta alla prima riga del set. L’oggetto infatti dispone dei metodi per manipolare il cursore, come ad esempio:
boolean next()
– avanza al record successivo;boolean previous()
– torna al record precedente;boolean absolute(int n)
– salta al record della riga nboolean relative(int n)
– avanza di n righe più avanti;
Si noti che i termini record e riga sono utilizzati in modo intercambiabili poiché il risultato della query può essere visto come una tabella contenente una riga per ogni record recuperato.
Come detto precedentemente per ottenere i dati dei campi di ciascun recordset si utilizzano i metodi getXXX(String attr)
o getXXX(int index)
in cui XXX corrisponde al tipo di dato restituito. L’argomento nelle due versioni del metodo corrisponde rispettivamente al nome dell’attributo desiderato o alla sua posizione (1 equivale alla prima colonna).
Di seguito si riporta un codice di esempio in cui si scorrono i record ottenuti e si stampano i nomi ed i valori associati alle colonne dei record:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Legge i metadati associati alla query ResultSetMetaData md = rs.getMetaData(); int columnsNumber = md.getColumnCount(); // Scorre i record while ( rs.next() ) { System.out.println( "Record: " + rs.getRow() ); for (int i = 1; i <= columnsNumber; i++) { System.out.println( md.getColumnName(i) + ": " + rs.getString(i)); } System.out.println( "---" ); } |
Rilascio delle Risorse
Una volta eseguite le operazioni volute devono essere rilasciate le risorse acquisite e chiusa la connessione. Ciò comporta la chiusura degli oggetti ResultSet
, Statement
e Connection
, nell’ordine inverso alla loro apertura. E’ inoltre disponibile su tali oggetti un metodo per verificare se l’operazione è già stata effettuata: isClose()
, che però indica solo che il metodo close()
sull’oggetto è stato invocato, e non se la connessione è attiva o meno.
Un Esempio Completo
Nell’esempio completo sostituire i parametri tra parentesi quadre [] per adattarlo ad un caso reale.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public class JdbcExample { // Database URL private static final String DB_URL = "jdbc:oracle:thin:@//[HOST]:1521/[SERVICE_NAME]"; // Database credentials private static final String USER = "[USER]"; private static final String PASS = "[PASS]"; // Query private static final String QUERY = "SELECT * FROM [TABLE_NAME] WHERE ID = ?"; public static void main(String[] args) throws SQLException { Connection conn = null; Statement stmt = null; ResultSet rs = null; PreparedStatement ps = null; try { conn = DriverManager.getConnection( DB_URL, USER, PASS ); ps = conn.prepareStatement(QUERY); ps.setLong( 1, 1 ); // Set the ID rs = ps.executeQuery(); ResultSetMetaData md = rs.getMetaData(); int columnsNumber = md.getColumnCount(); while ( rs.next() ) { System.out.println( "Record: " + rs.getRow() ); for (int i = 1; i <= columnsNumber; i++) { System.out.println( md.getColumnName(i) + ": " + rs.getString(i)); } System.out.println( "---" ); } } catch( Exception ex ) { ex.printStackTrace(); } finally { if ( ps != null ) { ps.close(); } if ( rs != null ) { rs.close(); } if ( conn != null ) { conn.close(); } } } } |
Query di Aggiornamento
Oltre alle query di selezione viste sopra è possibile utilizzare JDBC per l’esecuzione dei comandi SQL di INSERT, UPDATE e DELETE. Per farlo è sufficiente invocare il metodo executeUpdate()
dell’oggetto Statement
il quale restituisce il numero di righe coinvolte nell’esecuzione del comando.
1 |
int affecterRow = statement.executeUpdate( "UPDATE user SET password = '...' WHERE id = 100" ); |
I Metadati
JDBC supporta l’accesso dinamico ai database, ovvero la possibilità di accedere a un database e ricavarne informazioni sulla sua struttura interna (tabelle, relazioni, sinonimi, link, trigger, ecc.) senza saperne nulla a priori. In questo modo posso, per esempio, interrogare una tabella senza sapere in anticipo quali e quante sono le colonne che la compongono, come fatto nell’esempio presentato sopra.
Questo grazie al fatto che tutti i DBMS hanno delle tabelle interne dette dizionari o cataloghi che contengono metainformazioni circa la struttura interna dai database. Se si hanno i privilegi è anche possibile interrogare tali tabelle con query SQL. JDBC sfrutta questa possibilità e fornisce un paio di interfacce per fornire al programmatore un modo per accedere a tali meta-informazioni.