JDBC: Introduzione a Java Database Connectivity

Scritto da Matteo Molinaro nella sezione Java

1.INTRODUZIONE

1.1 Java e Database

Negli ultimi anni è nato, si è sviluppato e si sta rapidamente affermando il linguaggio di programmazione Java di Sun. Per guadagnare ulteriori consensi è importante che il linguaggio fornisca al programmatore i mezzi per l'accesso e la gestione delle basi di dati.

Fra le caratteristiche più importanti di Java ricordiamo la semplicità e la portabilità, la possibilità cioè di portare e utilizzare i programmi scritti in Java su qualsiasi piattaforma così come sono, senza la necessità di modificarli o ricompilarli.

Visto il massiccio utilizzo che si fa delle basi di dati per la memorizzazione di informazioni sia nelle grandi aziende che nelle realtà più piccole è abbastanza semplice capire come questi due elementi, Java e i Database, uniti insieme possano fornire al programmatore uno strumento estremamente versatile e al contempo semplice per la realizzazione di applicazioni volte alla gestione di ingenti quantità di informazioni.

Per esempio nel caso di un'azienda che utilizzi più sistemi differenti, magari in località diverse, il risparmio di tempo nella realizzazione del software è sensibile, data anche la predisposizione naturale di Java per la rete.

I progettisti di Java devono aver tenuto in considerazione questi fattori e a tal proposito sono state infatti realizzate le API (Application Programming Interface) JDBC.

L'importanza di JDBC è sottolineata dal fatto di essere inclusa direttamente nella distribuzione standard di Java, e non come libreria separata.

1.2 Caratteristiche delle API JDBC

JDBC è una libreria di Java, quindi un insieme di classi, che fornisce i metodi per l'accesso e la manipolazione di basi di dati di tipo relazionale.

Mantenendo la filosofia di Java, le API JDBC sono state progettate per ottenere l'accesso ai dati indipendentemente dalla piattaforma e dal particolare tipo di database utilizzato; JDBC offre infatti delle interfacce standard e quindi uniche per qualunque DB; per ciascuno di questi deve poi esistere un driver che si occupi di tradurre le chiamate JDBC effettuate dall'applicazione in opportune istruzioni per accedere e manipolare i dati di uno specifico database.

Quando l'applicazione richiede l'accesso ad un DB un particolare componente detto Driver Manager controlla il tipo di DB per cui si è richiesta la connessione e carica il driver appropriato.

In questo modo è possibile cambiare sia piattaforma che DB semplicemente cambiando driver, sempre che ne esista uno.

Un driver per essere considerato pienamente compatibile con JDBC deve implementare tutte le interfacce previste dalle specifiche di JDBC. Se questa condizione è soddisfatta il driver si dice JDBC-Compliant.



1.3 Sull'esempio di ODBC

L'idea che sta alla base di JDBC è la stessa del già collaudato e diffuso ODBC (Open DataBase Connectivity) di Microsoft. Data la numerosa disponibilità di driver compatibili ODBC, i progettisti della Sun hanno ritenuto utile realizzare un cosiddetto driver bridge JDBC - ODBC, un particolare driver che aggiunge uno stadio alla traduzione e converte le chiamate JDBC in chiamate ODBC che quindi le traduce a sua volta in metodi specifici del particolare DB.

Questo driver bridge è stato realizzato principalmente allo scopo di garantire una discreta compatibilità di Java con numerosi DB già dalla sua nascita.

IMPORTANTE notare però che, nel caso ci si appoggi a questo driver, viene in qualche modo a cadere la caratteristica di portabilità, in quanto ODBC è compatibile solo con alcune piattaforme, in particolare è più probabile che funzioni su sistemi Microsoft.

1.4 JDBC e SQL

SQL è il linguaggio standard per la creazione e manipolazione di database relazionali. SQL non è però un linguaggio completo e, oltre al fatto di non essere in grado di eseguire alcune interrogazioni a causa della mancanza di alcuni costrutti tipici della programmazione imperativa, non è assolutamente adatto alla realizzazione di vere e proprie applicazioni.

Per questo motivo è necessario utilizzarlo insieme ad un altro linguaggio di programmazione che si occupi di tutti quegli aspetti che non hanno a che vedere con la manipolazione della base di dati.

Molti linguaggi incorporano SQL e si parla quindi di embedded-SQL. In questi casi il programmatore, nel punto in cui deve accedere al database, "smette" di scrivere nel linguaggio ospite e inserisce le istruzioni da eseguire in SQL direttamente nel codice.

Per ottenere il programma eseguibile il codice deve essere prima esaminato da un particolare traduttore che traduce l'SQL nel linguaggio ospite; solo a questo punto il codice ottenuto può passare al compilatore e quindi al linker.

L'approccio di Java è invece diverso. JDBC mette a disposizione dei metodi ai quali è possibile passare delle stringhe contenenti del codice SQL.

Il codice SQL non viene quindi tradotto prima della compilazione ma viene semplicemente passato al driver JDBC durante l'esecuzione.

Questa soluzione garantisce una maggiore flessibilità, visto che le istruzioni SQL possono essere memorizzate in stringhe variabili, e quindi più facilmente modificabili a run-time rispetto alle soluzioni tradizionali.



2. UTILIZZARE JDBC

2.1 Il package java.sql

Le classi e le interfacce JDBC si trovano nel package java.sql, contenuto nella distribuzione standard del jdk (Java Development Kit).

Le classi e le interfacce più importanti del package java.sql sono:

DriverManager
Connection
Statement
PreparedStatement
CallableStatement
ResultSet
DatabaseMetaData
ResultSetMetaData
Types

La classe DriverManager gestisce i driver JDBC e seleziona il driver apposito per il database in uso.

L'interfaccia Connection rappresenta una sessione di lavoro sul database e permette di ottenere informazioni sulla base di dati. Le operazioni sul database avvengono all'interno di una sessione.

Le interfacce Statement, PreparedStatement e CallableStatement permettono di eseguire interrogazioni e aggiornamenti sulla base di dati.

L'interfaccia ResultSet fornisce l'accesso alle tabelle.

DatabaseMetaData e ResultSetMetaData consentono di ottenere informazioni sullo schema del database.

La classe Types definisce tutta una serie di costanti che identificano i tipi di dati SQL.

Con queste classi e interfacce è possibile avere un controllo pressochè totale della base di dati.

2.2 Creazione di un Database

Le interfacce JDBC permetterebbero di creare un database da zero e modificarne lo schema. In realtà questa possibilità in diversi casi è preclusa dai driver che non supportano operazioni di modifica allo schema.

In molti casi è perciò consigliabile utilizzare gli strumenti forniti dal produttore del database per eseguire queste operazioni.

2.3 Connessione ad un Database

La prima cosa da fare per iniziare a lavorare sul database è caricare il driver JDBC adatto. Per farlo è necessario forzare il caricamento della classe che rappresenta il driver utilizzando il metodo forName della classe Class.

Supponendo di voler utilizzare il driver bridge ODBC, il caricamento si farà tramite l'istruzione:

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
A questo punto si stabilisce la connessione fra l'applicazione Java ed il database chiamando il metodo getConnection della classe DriverManager nel modo seguente:
Connection con = DriverManager.getConnection("jdbc:odbc:database");
Il metodo getConnection richiede come parametro una stringa che contiene il nome della sorgente di dati nella forma:
jdbc:<subprotocol>:<subname>
dove subprotocol indica il driver da utilizzare e subname è il nome della fonte di dati.

2.4 Estrazione e modifica dei dati del Database

Per eseguire delle interrogazioni o modifiche sulla base di dati si crea per prima cosa un oggetto di tipo Statement utilizzando il metodo createStatement dell'oggetto di tipo Connection ottenuto in precedenza.
Statement st = con.CreateStatement();
L'interfaccia Statement fornisce una serie di metodi come executeQuery ed executeUpdate per eseguire rispettivamente delle operazioni di lettura o modifica sulla base di dati tramite istruzioni SQL. Il metodo executeQuery verrà utilizzato per eseguire delle istruzioni SELECT mentre il metodo executeUpdate per l'esecuzione di istruzioni INSERT, UPDATE e DELETE.

Per esempio, per eseguire una query si farà così:
ResultSet rs = st.executeQuery("SELECT * FROM Tabella1");
Il parametro richiesto dal metodo executeQuery è una stringa contenente il codice SQL da eseguire. Questo metodo non fa altro che passare le istruzioni SQL al driver JDBC e successivamente ricevere da esso il risultato della query salvandolo in un oggetto di tipo ResultSet. L'utilizzo di ResultSet è spiegato in seguito (par 2.5.1).

Se invece si voglionoinserire dei dati in una tabella si utilizzerà un codice dimile al seguente:
st.executeUpdate("INSERT into Tabella1 (campo1, campo2) values(1,2)");
Il metodo executeUpdate non ritorna un oggetto ResultSet, le istruzioni INSERT, UPDATE e DELETE non danno infatti come risultato una tabella.

Il valore ritornato è invece un intero che indica il numero di record modificati.

Un altro esempio potrebbe essere quindi:
int nRows = st.executeUpdate("DELETE * FROM Tabella2 WHERE campo1 > 0");
System.out.println("Sono stati eliminati " + nRows + " record");
Queste due righe di codice eliminano i record della tabella Tabella2 in cui il valore del campo campo1 è maggiore di 0 e successivamente visualizzano il numero di record cancellati.

Oltre all'interfaccia Statement esistono anche le interfacce PreparedStatement e CallableStatement.

L'utilizzo di PreparedStatement è conveniente nel caso di query eseguite ripetutamente, in quanto consente di specificare parzialmente delle query e richiamarle successivamente modificando solamente i nomi dei parametri. In questo modo l'esecuzione è più rapida, in quanto il PreparedStatement conosce già la forma dell'interrogazione e deve solo cambiarne i parametri.

Inizialmente bisogna quindi creare l'interrogazione indicando con il carattere '?' i parametri il cui valore sarà inserito.
PreparedStatement ps = con.prepareStatement("INSERT into Tabella3 (parametro1,parametro2) values (?,?)");
Per inserire i valore mancanti si utilizzano i metodi setXXX (dove XXX è un tipo di dato) a cui vanno passati due parametri: il primo è l'indice del parametro della query che si vuole specificare, il secondo è il valore che gli si vuole dare.
ps.setString(1,"Ciao");
ps.setInt(2,15);
ps.execute();
La prima riga di quest'esempio specifica che il primo parametro dell'interrogazione è una stringa il cui valore è "Ciao", nella seconda riga invece si specifica che il secondo parametro dell'interrogazione è un intero di valore 15.

L'ultima riga esegue l'interrogazione sul database con i parametri specificati.

L'interfaccia CallableStatement consente invece di eseguire delle procedure memorizzate all'interno del database.

Per prima cosa è necessario creare un oggetto CallableStatement indicando, come per PreparedStatement, i parametri con un punto di domanda.

CallableStatement cs = con.prepareCall(" { ? = Procedura1 ? }");
Da notare che il primo '?' non indica un parametro di input ma uno di output. Prima di eseguire la procedura bisogna specificare esplicitamente di che tipo è il valore di ritorno della procedura tramite il metodo RegisterOutParameter.
cs.RegisterOutParameter(1,Types.INTEGER);
Questa istruzione specifica che il primo parametro del metodo prepareCall chiamato in precedenza è un parametro di output ed è un intero.

Per eseguire la procedura:
cs.setInt(2,10);
cs.execute();
La prima istruzione specifica che il parametro passato alla procedura (quindi il secondo '?' nella prepareCall) è un intero di valore 10.

La seconda istruzione esegue la procedura con i parametri specificati.

Per leggere il valore ritornato dalla procedura si utilizza il metodo getXXX (dove XXX indica il tipo di dato).
int val = cs.getInt(1);
Si era specificato in precedenza che il valore ritornato dalla procedura era di tipo intero, utilizziamo quindi il metodo getInt per leggerlo e lo salviamo in una variabile di tipo int.

2.5 Elaborare i dati

Oltre ad eseguire delle interrogazioni sulla base di dati, molto probabilmente si vorranno anche visualizzare i risultati ottenuti. A differenza di SQL però, un linguaggio procedurale come Java non è pensato per operare su tabelle.

A questo scopo sono quindi presenti le interfacce ResultSet, ResultSetMetaData e DatabaseMetaData.

2.5.1 ResultSet e cursori

Un oggetto di tipo ResultSet fornisce l'accesso ad una tabella.

Poichè una tabella può essere composta da due o più colonne (campi) e righe (record), per accedere ai dati si utilizza un cursore.

Un cursore è simile ad un puntatore che indica un determinato record di una tabella; è possibile quindi leggere i valori dei campi del record selezionato. In ogni momento è possibile spostare il cursore sul record successivo o precedente a quello corrente oppure è possibile posizionarlo su un qualunque altro record specificandone l'indice.

Per esempio per visualizzare i dati di una tabella è possibile procedere come segue:
while(rs.next())
{
    String var1 = rs.getString("campo1")
    System.out.println(var1);
}
Quando l'oggetto ResultSet viene creato il cursore punta al record "precedente al primo", quindi in realtà non punta a niente. In questo modo però è possibile scrivere un codice più elegante per scorrere tutta la tabella utilizzando un ciclo while.

Il metodo next sposta il cursore sul record successivo a quello corrente e ritorna vero se il nuovo record selezionato è valido, falso se non ci sono altri record nella tabella.

Il metodo getString richiede come parametro il nome di un campo della tabella e ritorna il valore di quel campo nel record corrente. Questo metodo legge il valore del campo come se fosse un campo di testo ma esiste un metodo per ogni tipo di dato (getInt, getFloat, getDate, getObject...).

2.5.2 I metadati

Negli esempi precedenti si è lavorato con il presupposto di conoscere a priori lo schema del DB. In alcuni casi comunque questo non avviene e, per esempio, si ha la necessità di visualizzare il contenuto di una tabella senza sapere quanti campia ha, quali sono i loro nomi e che tipo di dati contengono.

Per risolvere il problema si possono utilizzare le interfacce ResultSetMetaData e DatabaseMetaData che consentono di ottenere informazioni sullo schema del database.

Ad esempio, se volessimo visualizzare l'intestazione di una tabella che non conosciamo potremmo utilizzare il seguente codice:
ResultSetMetaData rsMeta = rs.getMetaData();
int nColumn = rsMeta.getColumnCount();

for(int i=1; i <= nColumn; i++)
{
    System.out.println(rsMeta,getColumnName(i) + " ");
}
Con il metodo getMetaData creiamo un oggetto di tipo ResultSetMetaData che ci fornisce i metadati della nostra tabella.

Il metodo getColumnCount ritorna il numero di campi della tabella; il metodo getColumnName ritorna il nome del campo nella posizione i-esima.

Questo frammento di codice, quindi, prima legge quanti campi contiene la tabella e poi li scorre uno a uno visualizzandone il nome.

L'interfaccia ResultSetMetaData consente di ottenere molti altri dati sullo schema di una tabella mentre l'interfaccia DatabaseMetaData fornisce dati generici sulla base di dati come tipo di database, version, grammatiche SQL supportate, istruzioni supportate (ad esempio se è supportata o meno l'istruzione UNION) e cosi via.

I più cliccati della sezione Java

:: Generare numeri Random in Java (171.176)

:: Introduzione a Java (8.167)

:: Si può spedire o ricevere email da un'Applet Java? (7.388)

:: Accedere ad una classe Java con Asp (5.101)

IN EVIDENZA
DOWNLOAD