commentiLucidi - Dipartimento di Ingegneria dell`Informazione

Introduzione a JDBC
Carpini Dania, Oliva Giuseppe, Petruzzi Marianna
Commenti ai lucidi
Lucido 1 (premessa)
Dopo avere analizzato in dettaglio, durante il corso di Basi di Dati Distribuite, l'API ODBC, pensata
per consentire la scrittura di applicazioni in maniera indipendente dal DBMS, introduciamo uno
strumento analogo sviluppato successivamente: l'API JDBC. Siamo quindi nell'ambito dei gateway
per basi di dati.
Lucido 2
JDBC è una API scritta intermante in linguaggio Java che implementa, come ODBC, lo standard
SQL CLI (Call Level Interface), il quale definisce l'insieme di funzioni per dialogare con le basi di
dati. La versione attuale dell'API JDBC è la 3.0, inclusa nella libreria standard del J2SE, e le cui
classi sono suddivise in due package: java.sql e javax.sql.
Lucidi 3 e 4
Mentre ODBC è scritta in linguaggio C, per cui una sua implementazione è dipendente dalla
piattaforma per la quale essa viene compilata, JDBC sfrutta le caratteristiche di portabilità proprie
del linguaggio Java, per cui può essere considerata come una versione portabile di ODBC. Inoltre
ODBC è stata concepita per essere interfacciabile con svariati linguaggi di programmazione, ma
questa "eterogeneità di linguaggio" si paga in termini di una maggiore complessità della API,
soprattutto nello scambio dei dati con il DB (vedi conversione dei tipi SQL). JDBC invece, essendo
pensata per integrarsi con un unico linguaggio, semplifica molto l'interazione tra l'applicazione e la
API.
Sia ODBC che JDBC permettono di richiamare funzionalità specifiche di un particolare DBMS. In
generale tale capacità non è una scelta consigliabile, poiché compromette la portabilità
dell'applicazione (dal punto di vista del DBMS utilizzato), andando anche contro la filosofia alla
base di questi strumenti.
Lucido 5
Un'implementazione di JDBC consiste in un DriverManager e un insieme di Driver. Le
applicazioni Java (client) richiamano le funzioni messe a disposizione dall'API, le quali vengono
passate al driver opportuno dal DriverManager. Quest'ultimo ha il compito di creare e gestire le
associazioni tra applicazioni e driver.
Le principali classi presenti nell'API JDBC sono delle "interface", dei cui metodi va fornita
un'implementazione effettiva. I driver realizzano esattamente ciò, e hanno il compito di tradurre le
chiamate JDBC in richieste al particolare DBMS; essi sono solitamente forniti dagli stessi
produttori dei DBMS, ma anche da terze parti.
Lucidi 6 e 7
Esistono 4 tipologie di driver JDBC.
1) Driver nativi: sono scritti in un linguaggio dipendente dalla piattaforma (es. C);
2) JDBC/ODBC Bridge: questo tipo di driver si occupa di tradurre le chiamate JDBC in chiamate
ODBC, ed è particolarmente utile per interfacciarsi con tutti quei DBMS per i quali non sono
ancora disponibili driver JDBC; è inoltre l'unico driver fornito insieme alla libreria Java standard;
3) Middleware-server: serve a tradurre le richieste JDBC in un formato indipendente dal DBMS; le
richieste così tradotte sono poi interpretate da uno strumento di middleware, solitamente
un'applicazione server Java, che le riformula ad un DBMS specifico;
4) Driver Java: è un'implementazione diretta delle interfacce della API; quindi si tratta di un driver
interamente scritto in linguaggio Java;
Per quanto detto si capisce che le soluzioni 3 e 4 sono le uniche a poter garantire la completa
portabilità dell'applicazione.
Lucido 8
Lo schema di utilizzo di JDBC è quello classico previsto dall'SQL CLI, che verrà descritto nei
lucidi seguenti. Bisogna tenere presente che le classi che implementano le funzionalità di base
fanno parte della Core API, per cui è necessario importare il package java.sql.
Lucido 9
Il passo preliminare al collegamento ad un DB è quello di "caricare" il driver. Al momento in cui si
richiede una connessione ad un DBMS il DriverManager cerca il driver all'interno di una lista
contenente quelli disponibili nel sistema. Non è necessario inserire esplicitamente il driver in tale
elenco, in quanto è sufficiente forzare il caricamento della classe corrispondente (la classe Driver)
per far sì che il driver si "auto-registri" al DriverManager. Il metodo da invocare è forName della
classe Class, al quale va passato come argomento il percorso (valido a partire dal CLASSPATH)
della classe che implementa il Driver.
Lucidi 10 e 11
A questo punto è possibile richiedere una connessione, tramite il metodo static getConnection della
classe DriverManager, al quale si devono passare tre argomenti di tipo String: l'URL per la
connessione, la cui sintassi è descritta nel lucido 11 e che varia a seconda del driver; uno UserName
e una Password necessari per l'autenticazione al DBMS (se non necessari possono essere delle
stringhe vuote). Il metodo restituisce un oggetto di tipo Connection, che rappresenta un canale di
comunicazione con il DB, sul quale, una volta terminate le operazioni, è consigliabile richiamare
esplicitamente il metodo close, poiché il Garbage Collector della Java Virtual Machine non ha la
facoltà di rilasciare risorse esterne alla memoria centrale (come quelle occupate sul DBMS).
Lucidi 12 e 13
Per poter inviare comandi al DBMS e ricevere le conseguenti risposte è necessaria la creazione di
un oggetto Statement, tramite il metodo createStatement della classe Connection. Esistono due
differenti metodi da utilizzare, a seconda del tipo di comando che si vuole eseguire: executeUpdate
ed executeQuery, descritti in dettaglio nel lucido. In realtà è presente un ulteriore metodo execute,
che è utile quando non si conosce a priori se si deve effettuare una interrogazione o un comando di
inserimento/modifica: questo metodo restituisce quindi un valore booleano true se è stata eseguita
una query, false altrimenti; è possibile a questo punto richiamare uno dei due metodi getResultSet e
getUpdateCount rispettivamente nel primo e nel secondo caso.
Come si può notare dagli esempi del lucido 13, non è necessario inserire il carattere terminatore per
i comandi SQL che si inviano al DBMS, poiché è il driver ad occuparsi di ciò. Questa particolarità è
dovuta al fatto che DBMS diversi possono usare caratteri terminatori diversi.
Lucidi 14 e 15
Quando si esegue una query (SELECT) viene restituito un oggetto ResultSet, mediante il quale si
possono recuperare le righe ottenute in risposta, nello stesso ordine con cui vengono restituite dal
DBMS. All'insieme dei risultati, che è una tabella composta da varie righe, è associato un cursore
che tiene conto della riga corrente. Inizialmente tale cursore è posizionato su una riga fittizia che
precede la prima riga del risultato. Richiamando il metodo next si fa avanzare il cursore alla riga
successiva: tale metodo restituisce true se tale riga è presente, false altrimenti.
Una volta posizionato il cursore su una riga, si richiamano i metodi getXXX (XXX= Int, String,
Float, ...) per recuperare i valori dei campi selezionati. Questi metodi ricevono come parametro o il
nome della colonna di interesse o la posizione all'interno della tabella risultato (partendo da 1). La
seconda modalità è particolarmente utile quando non si vuole codificare direttamente nel codice
dell'applicazione i nomi delle colonne del DB.
Lucido 16
I metodi getXXX a disposizione sono molteplici e permettono la conversione dei valori SQL
recuperati dal DB in valori JAVA leciti. Quella riportata in questo lucido è un sottoinsieme della
tabella generale presente sulla documentazione ufficiale del J2SE. In tabella sono indicate con una
X più marcata le conversioni consigliate, mentre con quelle meno marcate si indicano conversioni
comunque lecite.
Lucido 17
Un'applicazione Java può mantenere contemporaneamente più connessioni a più database (o
eventualmente allo stesso) e per ciascuna di esse può avere diversi Statement, che a loro volta però
possono tenere aperto al più un oggetto ResultSet. Quest'ultima caratteristica di JDBC è dovuta al
fatto che ad ogni Statement è associato un unico canale di comunicazione con il DBMS, che rimane
aperto fino a quando non sono state recuperate tutte le righe del risultato (oppure si chiude
esplicitamente il ResultSet con il metodo close); se infatti si esegue una nuova query su uno
Statement prima di avere recuperato le tutte le righe di una query precedente, le nuove informazioni
sovrascrivono le vecchie.
Lucido 18
A partire dalla versione 2.0 di JDBC si possono ottenere dei ResultSet con maggiori funzionalità,
richiamando la versione con due parametri del metodo createStatement:
1) scrollableResultSet: la scansione delle righe risultato di una query può essere fatta in entrambe le
direzioni (richiamando i metodi previous e next); consentono inoltre il posizionamento assoluto su
una particolare riga e altre funzionalità;
2) updatableResultSet: mettono a disposizione anche una serie di metodi updateXXX con i quali è
possibile modificare i valori dei campi sulla riga corrente, facendo sì che i cambiamenti si riflettano
automaticamente sulle tabelle del database. Perché un ResultSet sia updatable ci sono delle
restrizioni dovute al tipo di query che si effettua: essa deve fare riferimento ad una sola tabella, non
deve contenere operazioni di join o calusole group by, deve selezionare necessariamente anche la
chiave primaria della tabella, ecc.
Lucido 19
Molti dei metodi presenti nelle classi finora descritte possono sollevare eccezioni di tipo
SQLException: negli oggetti di questo tipo sono memorizzate anche informazioni sul tipo dell'errore
che ha causato l'eccezione, è presente infatti sia il codice previsto dallo standard XOPEN o SQL 99
sia il codice d'errore del particolare DBMS. Come tutte le eccezioni Java, esse vanno gestite
all'interno dell'applicazione con un blocco try-catch.
A volte è utile avere informazioni sul database che si sta utilizzando, come il nome, il numero
massimo di connessioni concorrenti verso di esso, il nome e la versione del driver: queste e altre
informazioni possono essere recuperate da un oggetto di tipo DataBaseMetaData, ottenibile dalla
connessione.
Da un ResultSet è invece possibile recuperare, tramite la creazione di un oggetto
ResultSetMetaData, informazioni come il numero, il nome, il tipo delle colonne restituite da una
query.
Lucidi 20, 21 e 22
Una transazione è una sequenza di comandi logicamente indivisibili, i quali devono essere eseguiti
come un'unità atomica. Per permettere la gestione delle transazioni i sistemi mettono a disposizione
i comandi commit e rollback: con il primo si rendono permanenti gli effetti delle operazioni
eseguite (la transazione è andata a buon fine), con il secondo vengono invece annullate (la
transazione non è andata a buon fine).
Quando si ottiene una connessione al DB mediante le classi del package java.sql, questa è in
modalità autocommit. Ciò significa che il commit viene eseguito automaticamente al
completamento dei comandi. I comandi di inserimento/modifica e quelli DDL sono considerati
completi appena vengono eseguiti, mentre le interrogazioni lo sono soltanto una volta recuperate
tutte le righe del risultato.
Per gestire manualmente il commit dei comandi, si deve innanzitutto disabilitare l'autocommit,
quindi richiamare sulla connessione i metodi commit e rollback al momento opportuno.
È inoltre possibile creare dei checkpoint durante una transazione, cioè dei punti di ancoraggio a cui
ritornare (nel senso delle modifiche al DB) con un'operazione di rollback. Per ottenere ciò si crea un
oggetto SavePoint sulla connessione, il quale può essere passato come parametro al metodo
rollback. Nel caso in cui si richiami la versione di rollback senza parametri vengono disfatte tutte le
operazioni successive all'ultimo commit o rollback.
Impostando inoltre il livello di isolamento delle transazioni si può stabilire il grado di concorrenza
tra esse. La scelta del livello, tra i 5 disponibili (5 costanti definite nella classe Connection), si
effettua in base a quali operazioni "pericolose" si vogliono concedere (dirty reads, phantom reads,
nonrepetable-reads, ...). I livelli sono quelli standard previsti dall'SQL.
Lucido 23
Il package javax.sql fornisce funzionalità più avanzate rispetto a java.sql, come i pool di
connessioni e le transazioni distribuite. Per sfruttare tali potenzialità è necessario ottenere la
connessione al DB mediante l'interfaccia DataSource invece che nel modo precedentemente
descritto.
I pool di connessioni sono un meccanismo per riutilizzare delle connessioni esistenti, piuttosto che
crearne di nuove ad ogni richiesta; poiché la creazione di nuove connessioni è un'operazione
particolarmente costosa, questo meccanismo permette un notevole incremento delle prestazioni.
Con le transazioni distribuite è possibile effettuare transazioni che coinvolgono più database server;
in questo caso le connessioni ai DBMS non sono in modalità autocommit e non è consentito
chiamare i metodi commit e rollback esplicitamente, poiché tutte le operazioni sono gestite da un
Transaction Manager.
Un aspetto particolarmente importante è che l'uso di questi meccanismi avviene in maniera
trasparente all'applicazione, poiché essi sono implementati ad un livello sottostante.