Per
poter realizzare applicazioni n-tier abbiamo
detto che dobbiamo individuare n livelli in
cui un'elaborazione può essere scomposta per
poterla distribuire su altrettanti sistemi. Per capire
meglio in concreto la struttura 3-tier su cui ci concentreremo
abbiamo bisogno di un problema concreto da affrontare.
Realizzeremo
quindi una struttura 3-tier in cui ci sarà
un database, un server che interagisce le connessioni
e un cliente. Cominceremo con una prima struttura
in cui un server scritto da noi interagirà
con un cliente e con un database, successivamente
cercheremo di utilizzare soluzioni alternative.
In
questo appuntamento cercheremo di capire come accedere
un database. Questo costituisce il primo livello della
nostra architettura. Il DBMS Successivamente scriveremo
il secondo livello costituito dal server.
Java
e JDBC
Java
offre un'interfaccia standard per l'accesso ai database
chiamata JDBC. In questo paragrafo facciamo un breve
riepilogo della sua struttura e di come vada usata
per accedere un database.
L'interfaccia
JDBC sfrutta in modo magistrale il meccanismo di loading
dinamico delle classi di Java. Il package java.sql
contiene infatti quasi solo interfacce che modellano
un'interfaccia ragionvevole verso un database relazionale.
La domanda è: chi si occupa di fare il lavoro?
La risposta, in un certo senso ovvia, è il
driver JDBC del database. In effetti studiando
con più attenzione le classi appartenenti al
package java.sql si scopre una classe! Si tratta della
classe java.sql.DriverManager che è responsabile
del caricamento dei driver che consentono l'accesso
ad un database.
Questa
classe offre solo metodi statici nella propria interfaccia:
esiste un solo driver manager. Per poter aprire una
connessione ad un database è quindi necessario
utilizzare il metodo getConnection specificando il
nome del database sotto forma di URL. E il driver?
In effetti la documentazione del package riporta un
piccolo dettaglio: prima di usare la chiamata a getConnection
è necessario aver eseguito la seguente istruzione:
try
{
Class.forName("drivername");
}
catch
(Exception e) {
System.err.println("Class not found!");
}
I
più curiosi si saranno chiesti: perché
è mai necessario? Perché non ci pensa
il DriverManager? E come si dice quale driver usare
al DriverManager?
La
risposta ad entrambe queste domande risiede in un
meccanismo potentissimo di Java che è il loading
dinamico di una classe. Contrariamente a quello che
accade nei tradizionali modelli di esecuzione, in
cui un file eseguibile viene caricato ed eseguito
e non può caricare pezzi durante l'esecuzione
(eccezion fatta per le librerie dinamiche ovviamente),
la Java Virtual Machine carica una classe solo quando
si cerca di crearne un'istanza o di accedere un suo
membro statico.
L'altro
ingrediente fondamentale è il supporto che
Java offre alla reflection: meccanismo che
espone il sistema dei tipi al linguaggio consentendo
di vedere e modificare dinamicamente i membri di una
classe utilizzando l'oggetto Class (la classe Object
ha un metodo getClass() che consente di accedere alla
descrizione della classe a cui appartiene un oggetto).
Quando
per la prima volta si richiede il caricamento di una
classe la JVM usa il ClassLoader per caricare il bytecode
e trasformarlo in una classe utilizzabile dal runtime.
Durante questo processo viene creato un oggetto appartenente
alla classe Class e che rappresenta il tipo appena
caricato. Durante questo processo (a partire dalla
versione 1.1 del linguaggio) è possibile far
sí che sia eseguito un blocco di codice responsabile
dell'eventuale inizializzazione dei metodi della classe.
Questo blocco viene specificato all'interno della
classe. Ad esempio:
class
Foo {
public static int count;
{ // Questo è il blocco che sara`
invocato
count = 1;
}
}
Nella
classe Foo il blocco di codice viene invocato al caricamento
nella JVM del caricamento della classe Foo.
Rimane
un ultimo passo per comprendere il meccanismo del
DriverManager: il metodo forName consente di ottenere
un oggetto della classe Class che descrive una classe
il cui nome è fornito come stringa. Ad esempio:
Class
s = Class.forName("java.lang.String");
Ovviamente
la classe potrebbe non esistere e quindi bisogna prepararsi
a raccogliere un'eccezione con un blocco try...catch.
Detto
questo: perché mai chiamare il metodo forName
per caricare la classe che implementa il driver JDBC
se non si memorizza neanche in una variabile? Semplicemente
perché il codice che inizializza la classe
provvede a registrare il driver presso il DriverManager!
In particolare associa alla classe del driver la base
di URL che verranno gestite da quel driver.
A
questo punto siamo pronti per creare la prima connessione
al database utilizzando il metodo getConnection:
try
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
}
catch(ClassNotFoundException e) {
}
String
url = "jdbc:odbc:dbname";
Connection
db = DriverManager.getConnection(url);
Nell'esempio
carichiamo il driver JdbcOdbc compreso nella distribuzione
del linguaggio. Questo driver si occupa di tradurre
le richieste JDBC ad un driver ODBC. Visto che la
maggior parte dei DBMS offrono il driver ODBC l'interfaccia
è utilizzabile con un grande numero di DBMS.
Ovviamente se un DBMS offre il proprio driver JDBC
è da preferirsi a quello che passa per ODBC.
La
URL consente al DriverManager di capire che il driver
da usare è il bridge JDBC/ODBC e quindi crea
una connessione al database e restituisce un oggetto
che implementa l'interfaccia Connection.
Qui
avviene la fine di una magia che spesso passa inosservata:
un programma è in grado di utilizzare un oggetto
di una classe che neanche conosce (in questo caso
il driver JDBC-ODBC). Questo è possibile grazie
al fatto che l'oggetto implementa un'interfaccia conosciuta
al programma.
Per
ovvi motivi nei nostri esempi ci limeteremo ad usare
il driver JDBC-ODBC. Questo non implica che il DB
risieda sulla macchina su cui è definita la
connessione. Nel caso di connessioni a DB SQLServer
e Oracle, ad esempio, la connessione ODBC consente
di accedere database remoti. In ambiente Linux se
si fa uso del database Postgres è disponibile
un driver JDBC.
Per
configurare una connessione ODBC su Windows è
necessario andare nel pannello di controllo e lanciare
l'applet ODBC. A questo punto di crea una nuova connessione
in cui, una volta specificato il driver, bisogna inserire
i parametri di connessione (nel caso di Access un
file, nel caso di Oracle o SQLServer un host) e un
nome. Questo nome è quello che va usato al
posto di dbname nella URL di connessione JDBC.
Accediamo
al database
Siamo
finalmente giunti al punto cruciale: possiamo vedere
i nostri dati! La connessione ci fornisce uno strumento
fondamentale per impartire un comando SQL al database:
un oggetto che implementi l'interfaccia java.sql.Statement.
Questo è possibile utilizzando la nostra connessione
ed in particolare il metodo createStatement:
Statement
s = db.createStatement();
Utilizzando
poi questo oggetto è possibile fare un'interrogazione
al database utilizzando il metodo executeQuery e fornendo
l'interrogazione SQL da effettuare.
Supponendo
di avere una tabella chiamata Studenti con due attributi,
nome e matricola, è possibile ottenere tutti
record contenuti in essa con il seguente codice:
Statement
s = db.createStatement();
ResultSet
r = s.executeQuery("Select * from Studenti");
Il
risultato dell'esecuzione della query è rappresentato
da un oggetto che implementa l'interfaccia java.sql.ResultSet.
Prima di cercare di capire cosa sia un ResultSet è
bene ricordare che SQL è lo standard per interrogare
le basi di dati relazionali. Esistono numerosi
testi che descrivono la sua struttura e il suo uso
e non abbiamo qui sufficiente spazio per trattare
un tale argomento. Per chi comincia quindi non posso
che suggerire di utilizzare un DBMS tipo Access che
ha un sistema per disegnare le query. Una volta disegnata
si passa alla visualizzazione SQL della query e la
si copia nel programma!
Un
ResultSet è quello che nel modo database si
chiama un cursore. Poiché il risultato
di una interrogazione è una tabella il ResultSet
consente di scorrere le righe della tabella una alla
volta.
Il
tipico codice che si usa per scorrere la tabella è
il seguente:
while
(r.next()) {
// Accede ai dati della riga.
}
L'accesso
ai dati di una riga avviene utilizzando i vari metodi
getXXX dove XXX indica il tipo di dato contenuto nella
colonna. Nel nostro esempio supponiamo che l'attributo
nome sia di tipo stringa e la matricola di tipo intero:
while
(r.next()) {
String nome = r.getString("nome");
int mat = r.getInt("matricola");
// Usa i dati...
}
Cosa
accade se vogliamo modificare i dati contenuti nella
tabella? In questo caso dobbiamo fare ricorso al metodo
executeUpdate che ci consente di specificare un comando
INSERT o UPDATE per modificare una tabella. Ad esempio:
s.executeUpdate("Insert
into studenti (nome, matricola)
values
('antonio', 164795);");
Il
metodo executeUpdate restituisce il numero di righe
della tabella coinvolte nella modifica. Nel nostro
caso 1.
Conclusioni
Sappiamo
quindi ora tutto quello che serve sul primo tier della
nostra architettura 3-tier. Se vi state chiedendo
che c'è di diverso da un articolo che parla
solo di accedere DBMS da Java la risposta è:
assolutamente niente. Le architetture n-tier riguardano
la struttura di un sistema che come è ovvio
è composto da elementi che nel loro piccolo
sono del tutto standard. La ricchezza che deriva dalla
struttura è principalmente una migliore ingegnerizzazione
del sistema che porta ad una sua maggione manutenibilità.
Inoltre la conoscenza delle interfacce tra un livello
e l'altro aiuta a rendere più flessibile il
sistema, ad esempio accedendo più DB invece
che uno solo per distribuire il carico senza che il
livello intermedio si accorga del cambiamento.
Spero
di non avervi annoiato troppo e che la piccola digressione
sull'architettura del class loading dinamico vi abbia
fatto scoprire un aspetto poco conosciuto del linguaggio.
Nella
prossima puntata svilupperemo il secondo livello dell'architettura:
il server che consentirà ai clienti di accedere
il DB. Sarà una buona occasione per ripassare
la struttura di un server multithread in Java.
|