Tipi di driver JDBC

annuncio pubblicitario
Dispense su JDBC
di Marco Baioletti
Introduzione
In queste note viene fornita una breve introduzione alla tecnologia JDBC che consente ad
un'applicazione Java di connettersi ad un database e di eseguire operazioni di interrogazione e
manipolazione dati. Questa tipologia di applicazione e' estremamente importante perché consente di
utilizzare in modo semplice tutti i servizi offerti dal DBMS direttamente da programma.
Tipi di driver JDBC
La comunicazione tra l'applicazione Java e il DBMS è svolta da una componente software chiamata
driver. Esistono 4 tipi di driver JDBC :
1. JDBC-ODBC. Il driver utilizza un "ponte" creato dalla Sun, che permette l'utilizzo di driver
di tipo ODBC, all'interno di applicazioni Java.
2. JDBC basato su funzioni scritte in un linguaggio "nativo" (cioè non in Java) e poi richiamate
da Java.
3. JDBC-Net, driver nativo, pure Java, che non comunica direttamente con il DBMS, ma con
un middleware, in grado di prendere le chiamate e convertirle in qualcosa che il database è
in grado di capire, rispondendo poi di conseguenza.
4. JDBC, pure Java, con accesso diretto. In questo caso non occorre nulla, se non il driver e il
DBMS al quale collegarsi.
In queste note si farà riferimento al driver di tipo 1 JBDC-ODBC che sarà utilizzato per connettersi,
via ODBC, a database MS Access (per questo tipo di database non esistono a tutt'oggi altri driver
ufficiali) e un cenno alla connessione con database MySQL con il driver Connector/J di tipo .
Operazioni preliminari
Creazione della sorgente dati ODBC
Questa operazione va fatta la prima volta che si crea l'applicazione. Per poter utilizzare un DB
Access tramite JDBC-ODBC è indispensabile definire una sorgente dati ODBC associata al DB in
questione.
Le seguenti istruzioni sono valide per Windows XP. Per altri sistemi operativi i passi da compiere
sono simili.
1. Andare su Menù Start
2. Selezionare Pannello di controllo
3. Selezionare Strumenti di amministrazione
4. Selezionare Origine dati (ODBC)
5. su Origine dati utente selezionare "Database di Microsoft Access" e cliccare su Aggiungi
6. Selezionare come driver per l'origine dati "Driver di Microsoft Access" e cliccare su Fine
7. Digitare il nome da dare all'origine dati (questo nome sarà la terza componente dell'URL del
database)
8. Sul riquadro Database cliccare Seleziona (per scegliere un DB già esistente) o Crea (per
creare un nuovo DB)
9. In ogni caso bisogna fornire il pathname del DB
10. Alla fine cliccare su OK.
Caricamento del driver
La prima operazione che deve essere svolta in un'applicazione che usa JDBC e' il caricamento del
driver.
Il driver va caricato con un'istruzione particolare Class.forName specificando tra parentesi il
"percorso" in cui si trova il driver.
Per utilizzare il driver che verrà usato negli esempi l'istruzione e'
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Questa istruzione va eseguita prima di ogni altra operazione sul database.
E' obbligatorio gestire con il costrutto try/catch l'eventuale eccezione generata.
Creazione dell'oggetto connessione
L'oggetto connessione e' un oggetto mediante il quale si specifica a quale database far eseguire le
operazioni richieste dal programma. Nel caso in cui un programma utilizzi più database, ci devono
essere tanti oggetti connessione quanti sono i database utilizzati contemporaneamente.
Un oggetto connessione si dichiara con
Connection c;
e si crea con
c=DriverManager.getConnection(URL_DB);
in cui URL_DB e' una stringa divisa in tre parti separate dal carattere di due punti. Le prime due
parti sono obbligatoriamente jdbc e odbc. La terza parte è il nome dell'origine dati ODBC (fornita
nel punto 7 delle istruzioni precedenti).
Ad esempio per connettersi ad un database avente come origine dati ODBC il nome prova_db1
l'URL del DB è jdbc:odbc:prova_db1.
E' obbligatorio gestire con il costrutto try/catch l'eventuale eccezione generata.
L'oggetto connessione va creato prima di operare con un database. Poiché ogni operazione fatta sul
database deve utilizzarlo, esso deve essere memorizzato in una variabile visibile all'intero
programma. Se il programma è composto da un'unica classe la variabile c può essere definita
all'interno della classe. Se invece il programma ha più classi, la variabile c può essere messa in una
classe statica.
Connessione con un database MySQL
Per utilizzare il driver che verrà usato per connettersi ad un database MySQL si usa la classe
Class.forName("com.mysql.jdbc.Driver");
Per connettersi ad un database di nome prova_db2 all'indirizzo www.azienda.it tramite l'utente rossi
e password segreto l'URL del DB è
jdbc:mysql://www.azienda.it:3306/prova_db2?user=rossi&password=segreto
in cui 3306 è la porta TCP/IP su cui per default lavora MySQL. Per connessioni in locale usare
l'indirizzo localhost.
Operazioni sul database
L'oggetto Statement
L'oggetto Statement serve a eseguire istruzioni SQL sul database. Esso può essere utilizzato sia per
eseguire comandi veri e propri (inserimento/modifica/cancellazione record, ecc.), sia interrogazioni
sul DB.
Creazione
L'oggetto Statement si dichiara con
Statement st;
e si crea con
st=c.createStatement( );
in cui c e' l'oggetto connessione da usare.
Utilizzo per eseguire comandi
Per utilizzare l'oggetto Statement bisogna specificare una stringa contenente il comando SQL da
eseguire. La cosa più semplice, anche per trovare eventuali errori di programmazione, è quella di
utilizzare una variabile stringa, ad esempio dichiarata con
String SQL;
su cui si memorizza il comando SQL.
A questo punto, se il comando SQL da eseguire non è un interrogazione (quindi può essere
un'operazione di INSERT, UPDATE, DELETE o addirittura un comando del DDL), si usa il
metodo
st.executeUpdate(SQL);
e il comando verrà eseguito.
Il metodo e' una funzione e restituisce come risultato il numero di record coinvolti nel comando. Se
però non serve, si può ignorare questo risultato.
Ad esempio
int nr; String SQL;
SQL="insert into prodotti values(3,'penne',10)";
nr=st.executeUpdate(SQL);
Nella variabile nr restituirà 1, perché solo un record è stato toccato dal comando.
Se non mi serve sapere quanti record sono stati coinvolti basterà usare
SQL="insert into prodotti values(3,'penne',10)";
st.executeUpdate(SQL);
Si noti che uno stesso oggetto Statement può essere utilizzato senza problemi per inviare più
comandi al DB
SQL="insert into prodotti values(3,'penne',10)";
st.executeUpdate(SQL);
SQL="insert into prodotti values(4,'matite',10)";
st.executeUpdate(SQL);
Utilizzo per eseguire interrogazioni
Per far eseguire un'interrogazione si deve usare il metodo executeQuery che restituisce come
risultato un oggetto di tipo ResultSet con il quale è possibile consultare e, in certi casi, modificare il
risultato dell'interrogazione.
ResultSet rs;
...
rs=st.executeQuery(SQL);
E' possibile stabilire il massimo numero di righe che possono essere gestite in un resultset
utilizzando il comando
st.setMaxRows(nr);
Inoltre è possibile indicare un tempo massimo di esecuzione con il comando
st.setQueryTimeout(sec);
Comandi parametrici
E' molto frequente che l'istruzione SQL da eseguire, sia essa un comando o un'interrogazione,
contengano delle parti, chiamate parametri, note solo a tempo di esecuzione.
Il classico esempio è quello di estrarre tutte le informazioni di una persona, a partire dal cognome,
che l'utente fornisce durante l'esecuzione del programma, ad esempio tramite una TextField.
String SQL;
String cognome=jTextField1.getText( );
SQL="select * from persona where cognome='" + cognome + "'";
Sono indispensabili i due apici singoli. Per capire a cosa servono si supponga che il cognome
digitato sia Rossi. La stringa memorizzata nella variabile SQL sarà select * from persona where
cognome='Rossi'. Gli apici infatti servono in SQL a delimitare le costanti stringa.
Se per errore si omettono gli apici, si avrà un'eccezion SQL in quanto il comando inviato al DBMS
sarebbe select * from persona where cognome=Rossi, che è sbagliato poiché si tenterebbe di
confrontare il campo cognome con il campo rossi (che ovviamente non esiste).
In un altro esempio si vogliono trovare le informazioni di un prodotto a partire dal codice, che è di
tipo numerico. In tal caso gli apici non si devono mettere, in quanto in SQL le costanti numeriche
non devono essere delimitate da apici o virgolette.
Ad esempio
String codice=jTextField2.getText( );
SQL="select * from prodotto where codice=" + codice;
Si noti che il codice è memorizzato in una variabile di tipo stringa, ma è inserito nella query senza
apici e come tale verrà trattato in SQL come se fosse un intero.
Ad esempio se l'utente ha digitato il codice 10, la stringa SQL conterrà il comando select * from
prodotto where codice=10.
Ovviamente se l'utente ha inserito nel TextField un dato non numerico, si avrà comunque
un'eccezione SQL. Ad esempio se l'utente ha inserito la parola due, la stringa inviata al DBMS sarà
select * from prodotto where codice=due, che produce lo stesso errore dell'esempio precedente.
Oggetto di tipo ResultSet
L'oggetto ResultSet puo' essere utilizzato in due modi.
Nel primo modo (l'unico possibile con il JDBC 1.0) il ResultSet serve ad accedere in sola lettura al
risultato di un'interrogazione.
Un'interrogazione quasi sempre restituisce un insieme di righe e il resultset consente di vedere una
riga per volta e di muoversi all'interno di queste righe (solo di una riga in avanti nel JDBC 1.0, in
altre direzione o modalità con il JDBC 2.0).
Nel secondo modo il ResultSet può servire a modificare il contenuto di una tabella del database,
potendo cancellare, inserire e modificare le righe
Opzioni di creazione
Quando si crea un oggetto Statement, si possono specificare due parametri tipo_scroll e
tipo_concorrenza che specificano in modo più accurato il tipo di ResulSet che si vuole ottenere.
Quindi si usa
Statement st=c.createStatement(tipo_scroll, tipo_concorrenza)
in cui tipo_scroll puo' essere
 ResultSet.TYPE_FORWARD_ONLY: il resultset può essere scorso solo in avanti;


ResultSet.TYPE_SCROLL_INSENSITIVE: il resultset può essere scorso sia in avanti che
all'indietro, ma non viene aggiornato se nel frattempo la sorgente dei dati viene modificata;
ResultSet.TYPE_SCROLL_SENSITIVE: il resultset può essere scorso sia in avanti che
all'indietro, inoltre il resultset è sempre tenuto aggiornato rispetto alla sorgente dei dati.
Invece tipo_concorrenza puo' essere
 ResultSet.CONCUR_READ_ONLY: il resultset serve solo per leggere i dati della tabella o
della query
 ResultSet.CONCUR_UPDATABLE: è possibile modificare i dati del resultset aggiornando la
tabella di origine
Per default, cioè se non si indica niente, tipo_scroll è ResultSet.TYPE_FORWARD_ONLY e
tipo_concorrenza è ResultSet.CONCUR_READ_ONLY.
Movimento
Il ResultSet consente l'accesso ad una sola riga per volta, chiamata la riga corrente del ResultSet.
Appena creato il ResultSet, la riga corrente non è la prima, ma una riga fittizia precedente la prima
riga. Analogamente esiste anche una riga fittizia successiva all'ultima riga.
Il ResultSet fornisce, a partire dal JDBC 2.0, una serie di metodi per "navigare" all'interno delle
righe che permettono di passare ad una nuova riga corrente:
 rs.next( ), va alla riga successiva
 rs.previous( ), va alla riga precedente
 rs.first( ), va alla prima riga
 rs.last( ), va all'ultima riga
 rs.absolute(n), va alla riga numero n (a partire dall'inizio se n>0, a partire dalla fine se n<0)
 rs.relative(n), si sposta di n righe rispetto alla posizione attuale (in avanti se n>0, all'indietro
se n<0)
 rs.afterLast( ), va alla riga fittizia dopo l'ultima riga
 rs.beforeFirst( ), va alla riga fittizia prima della prima riga
Se il tipo_scroll e' FORWARD_ONLY l'unico movimento possibile è next.
Per usare gli altri movimenti, il tipo_scroll deve essere uno degli altri due tipi.
Tutti i metodi, tranne gli ultimi due, restituiscono true se la posizione di arrivo è una posizione
valida, false altrimenti. Ad esempio il metodo next dà false quando è eseguito sull'ultima riga, true
se eseguito su una qualsiasi altra riga; il metodo previous dà false quando è eseguito sulla prima
riga, true se eseguito su una qualsiasi altra riga.
Controllo della posizione
Il ResultSet fornisce una serie di metodi funzione a valore booleano per controllare la posizione
della riga corrente all'interno del ResultSet:
 rs.isFirst( ), restituisce true se la riga corrente è la prima riga
 rs.isBeforeFirst( ), restituisce true se la riga corrente è la riga fittizia prima della prima riga
 rs.isLast( ), restituisce true se la riga corrente è l'ultima riga

rs.isAfterLast( ), restituisce true se la riga corrente è la riga fittizia dopo l'ultima riga
Lettura campi
E' possibile ottenere il valore di un campo della riga corrente con i due metodi
rs.getXXX(nome_campo)
rs.getXXX(indice_campo)
XXX puo' essere String, Int, Float, Double, Date, Boolean, ecc.
Il nome del campo deve essere quello indicato nella parte Select del comando SQL o quello
utilizzato nel DB (se nella Select si è usato *).
L'indice del campo deve essere un numero intero, a partire da 1, che rappresenta la posizione del
campo nella lista dei campi della Select o nella lista dei campi della tabella del DB.
In questo e negli esempi successivi si utilizzerà una tabella prodotti avente i seguenti campi
 codice, di tipo numerico intero
 descrizione, di tipo stringa
 prezzo, di tipo numerico reale a doppia precisione.
In questo primo esempio si vogliono trovare la descrizione e il prezzo dei prodotti che costano più
di 10 euro.
SQL="select codice, descrizione from prodotti where prezzo>10";
rs=st.executeQuery(SQL);
while(rs.next()) {
String d;
double p;
d=rs.getString("descrizione");
p=rs.getDouble("prezzo");
System.out.println(d+" "+p);
}
In questo secondo esempio si vuole riempire un ComboBox con i prodotti presenti nella tabella
prodotti, in particolare si vuole che nella ComboBox appaiano solo le descrizioni dei prodotti.
Innanzitutto bisogna definire una classe prodotto
class prodotto {
public int codice;
public String descrizione;
public double prezzo;
public String toString( ) { return descrizione; }
}
in cui si definisce nel metodo toString cosa si vuole visualizzare. Poi si riempe il ComboBox
SQL="select * from prodotti";
rs=st.executeQuery(SQL);
while(rs.next()) {
prodotto p=new prodotto;
p.codice=rs.getInt("codice");
p.descrizione=rs.getString("descrizione");
p.prezzo=rs.getDouble("prezzo");
jComboBox1.addItem(p);
}
Aggiornamento campi
Si può modificare un campo della riga corrente con i due metodi
rs.updateXXX(nome_campo, nuovo_valore)
rs.updateXXX(indice_campo, nuovo_valore)
XXX puo' essere String, Int, Float, Double, Date, Boolean, ecc.
Negli esempi seguenti si suppone che rs sia un resultset aggiornabile.
Ad esempio, se vogliamo cambiare la descrizione del prodotto della riga corrente
rs.updateString("descrizione","patate");
Una volta che la riga è stata cambiato, il DB può essere aggiornato con il comando
rs.updateRow( )
Cancellazione record
Con il metodo rs.DeleteRow( ) si cancella, sia dal ResultSet che dalla tabella sorgente nel DB, la
riga corrente.
Inserimento record
L'inserimento di un nuovo record avviene con un procedimento un po' complesso.
L'ordine da seguire è il seguente
1. rs.moveToInsertRow( ): con questo primo metodo si crea una nuova riga vuota in una
posizione "indefinita" all'interno del ResultSet
2. rs.updateXXX(...): con questi metodi si inseriscono i valori nei campi della nuova riga
3. rs.insertRow( ): la nuova riga viene accettata sia nel ResultSet che nel DB
4. rs.moveToCurrentRow( ): ci si posiziona sulla quella che era la riga corrente prima
dell'operazione di inserimento.
Ad esempio
rs. moveToInsertRow( );
rs.updateInt("codice",8);
rs.updateString("descrizione","gomme");
rs.updateDouble("prezzo",1.2);
rs.insertRow( );
rs.moveToCurrentRow( );
Gestione delle transazioni
Normalmente il JDBC considera ogni istruzione SQL inviata al DBMS come se fosse una
transazione a sé, cioè chiede al DBMS di fare il commit ad ogni comando. Questa modalità di
lavoro si chiama auto-commit.
Per creare una transazione è necessario disabilitare questa opzione. Il comando per ottenere ciò è
c.setAutoCommit(false);
A questo punto tutti i comandi SQL successivi faranno parte della transazione.
La transazione, come è noto, può avere due esiti: successo o fallimento. Nel primo caso il DBMS
deve fare il commit, cioè deve registrare in modo definitivo le modifiche sul database. Nel secondo
caso il DBMS deve annullare le eventuali modifiche apportate.
L'esito con successo si indica con l'istruzione
c.commit( )
mentre l'esito con fallimento si indica con
c.rollback( )
I due comandi sono indispensabili: senza di essi la transazione non andrebbe né al commit, né al
rollback.
I due esiti devono essere raggiunti in due diramazioni diverse del programma, ad esempio attraverso
una if si stabilisce se la transazione fallisce oppure no. La if è però in grado di gestire solo
transazioni per cui l'esito è determinato da una condizione (ad esempio su richiesta dell'utente).
Un motivo più frequente in grado di influenzare l'esito di una transazione è il verificarsi di un errore
in fase di esecuzione (istruzioni SQL errate, vincoli del DB non rispettati, errori di sistema,
problemi hardware, ecc.). In tali casi è indispensabile usare il costrutto try-catch.
Lo schema è il seguente:
c.setAutoCommit(false);
try {
istruzione
istruzione
...
istruzione
c.commit( );
} catch(SQLException e) {
// gestione dell'errore
...
c.rollback( );
}
Se non accadono errori, la transazione termina con successo e va al commit. Se invece si verifica un
errore, l'esecuzione passa al blocco catch, la transazione viene annullata tramite il comando di
rollback. Ci possono essere più blocchi catch relativi a tipi di eccezioni diversi; inoltre non tutti
necessariamente causano un esito negativo della transazione (in tal caso devono terminare con
c.commit( )). Si noti che senza il comando rollback del blocco catch, la transazione non viene
annullata dal DBMS.
Ecco un esempio di transazione che non va mai al commit
c.setAutoCommit(false);
try {
Statement c=c.createStatement( );
st.executeUpdate("insert into prodotti values(8,'pane',1.10)");
st.executeUpdate("insert into prodotti values(8,'carne',21)");
c.commit( );
} catch(SQLException e) {
c.rollback( );
}
Il motivo è che codice è una chiave della tabella prodotti e quindi non è possibile inserire due
record con la stessa chiave. La transazione viene eseguita fino alla seconda executeUpdate, viene
sollevata un'eccezione e la transazione è annullata: anche l'effetto della prima executeUpdate viene
annullato dal comando rollback (la transazione deve essere sempre atomica).
Isolamento delle transazioni
E' possibile indicare il livello di isolamento delle transazione da utilizzare.
Le anomalie prese in considerazione sono
S = lettura sporca: una transazione T1 legge un dato prodotto da una transazione T2 ancora in corso
e che poi fallirà
R = lettura non ripetibile: una transazione T1 legge una riga una prima volta, poi rileggendola una
seconda la trova diversa perché nel frattempo è stata modificata da una transazione T2 che è arrivata
al commit
F = righe fantasma: una transazione T1 legge o opera, attraverso una query, delle righe di una
tabella una prima volta, una transazione aggiunge o toglie alcune righe da quella tabella, poi T1
rilegge le righe con la stessa query e ne trova un numero diverso.
La differenza tra le anomalie R e F è molto sottile: nella seconda lettura con R si possono vedere
cambiamenti dei valori di una o più righe, invece con F si possono vedere altre righe o non vedere
più alcune. Si noti che bloccando l'anomalia R non si bloccano, generalmente, le anomalie F,
soprattutto se si aggiungono nuove righe.
Il livello è definito con il comando
c.setTransactionIsolation(livello)
in cui livello è un numero intero che puo' assumere i seguenti valori (definiti come costanti)
 Connection.TRANSACTION_READ_UNCOMMITTED: una transazione può leggere dati
prodotti da transazioni ancora in corso, anomalie ammesse S, R, F
 Connection.TRANSACTION_READ_COMMITTED: una transazione può leggere solo dati
prodotti da transazioni terminate con successo, anomalie ammesse R, F
 Connection.TRANSACTION_REPEATABLE_READ: anomalia ammessa F
 Connection.TRANSACTION_SERIALIZABLE: nessuna anomalia ammessa
Scarica