MokaByte 57 - 9mbre 2001 
Applicazioni web
II parte
Accedere Database con Java
di
Antonio Cisternino
Eccoci al secondo appuntamento della miniserie dedicata alle applicazioni n-tier, in questa puntata  faremo una breve panoramica sull'interfaccia ai database JDBC di Java. Scriveremo quindi un piccolo server che manipola dati contenuti in un database

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.


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it