MokaByte Numero  38  - Febbraio  2000
 
JDBC e Web
II parte
di 
Mauro Molino
Come devo strutturare la mia applicazione se voglio 
interfacciare una base di dati con il web? 
In particolare cosa devo fare per permettere ad una applet di leggere i dati contenuti in tale db?

In questa seconda parte del discorso vedremo l’implementazione del server per l’esecuzione remota di query, lasciando al prossimo e ultimo articolo il client ed alcune ottimizzazioni

Il Server
Il nostro server è una sorta di servizio che sta in ascolto su una porta TCP/IP in attesa di connessioni. Una volta stabilita la connessione ed attivata la comunicazione bidirezionale, è possibile scambiare messaggi con il client. Nel nostro caso, il client darà dei comandi da eseguire su un database, e il server restituirà eventuali dati o messaggi di errore.
Intanto di che librerie avremo bisogno ? Sicuramente  java.net per la gestione dei socket, java.io per i flussi di dati in input/output e poi la java.sql per la connessione al database. Quindi:
 
import java.net.*;
import java.io.*;
import java.sql.*;
A questo punto possiamo creare la nostra classe:
 
public class JDBCServer {
    static ServerSocket ssock;
    static BufferedReader in;
    static PrintWriter out;
    static Connection con; 
    static Statement stmt ;
    static ResultSet rs ;
    static ResultSetMetaData rsmd;
Vediamo a cosa ci serviranno gli oggetti dichiarati:
il ServerSocket è per l’appunto il socket in ascolto su una porta in attesa di connessioni. Come vedremo, all’arrivo di una richiesta di connessione verrà creato un socket “normale” per la comunicazione con il client.
BufferedReader e PrintWriter sono gli oggetti implicati nello “stream” dei dati, rispettivamente in ingresso ed uscita.
Connection gestisce la connessione al database,Statement rappresenta un oggetto per le query, ResultSet conterrà i risultati delle query effettuate e ResultSetMetaData conterrà informazioni sulla struttura dei dati presenti nel recordset ( che utilizzeremo nel prossimo articolo).
 
10 public static void main(String[] args){
20  String comando;
30 String data,inputLine;
40 try {
50 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
60  String dburl = "jdbc:odbc:nomedb";
70  con = DriverManager.getConnection(dburl, "", "");
80  stmt = con.createStatement();
90  ServerSocket ssock=new ServerSocket(3000);
100               Socket sock=null;


In questa parte viene effettuata la connessione al database. Come detto nel precedente articolo, stiamo supponendo una connessione ad un database odbc tramite bridge jdbc:odbc. Alla riga 60 si costruisce la stringa di connessione, dove nomedb è il nome odbc del nostro database.Dopo avere inizializzato la connessione al database creiamo(riga 80) l’oggetto Statement che ci servirà per costruire le query.
Le righe 90 e 100 si occupano invece di dichiarare 2 socket: uno server sulla porta 3000, e uno client per la comunicazione con il client.

A questo punto siamo pronti per entrare nel loop principale del nostro server:
 

110 while (true){
120 sock=ssock.accept();
130 in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
140 out = new PrintWriter(sock.getOutputStream(),true);


Alla riga 120 viene lanciato il metodo “accept” del ServerSocket. Questo metodo, sincrono, mette il ServerSocket in attesa di una connessione da parte di un client, dopo di che crea un socket per la comunicazione bidirezionale con il client stesso.
Le 2 righe seguenti si occupano invece di inizializzare lo “streaming” in ingresso e in uscita lungo il socket.
Una volta creato il socket, e quindi instaurato un canale di trasmissione fra client e server, possiamo far partire il secondo loop, quello che si occupa di controllare se arrivino richieste dal client. Nel nostro caso questo meccanismo è sufficiente in quanto è sempre il client a dare disposizioni sul da farsi, quindi il server si limita ad attendere istruzioni, eseguirle , ed eventualmente restituire informazioni.

150 while (true) {
160   inputLine = in.readLine();
Alla riga 160 , il metodo redLine() sullo stream di input attende in maniera sincrona l’arrivo di una riga di dati.
Le righe successive compongono il parser, quel modulo che si occupa di interpretare i dati in arrivo. In questo caso, abbiamo utilizzato la seguente convenzione: la riga è composta da un comando, seguito da un duepunti( : ), seguito dalla stringa di dati da utilizzare. La riga 170 restituisce la posizione nella stringa del segno di duepunti. Se viene trovato, il valore sarà l’indice di posizione, altrimenti otterremo uno zero. Ed è questo il controllo che viene effettuato alla riga 180. In pratica, la stringa verrà processata solo se è presente il segno di due punti e se viene riconosciuto un comando valido.
 
170    int p = inputLine.indexOf(":");
180    if (p > 0) {
190     comando = inputLine.substring(0,p);
200     data = inputLine.substring(p+1,inputLine.length());


La variabile “comando” contiene il “metodo” richiesto dal client, che nella stringa ricevuta è la prima parte prima del segno i duepunti. Nelle righe successive verranno eseguite varie operazioni a seconda del comando ricevuto, ed ora le vedremo una ad una. Notiamo che il metodo utilizzato, cioè una serie di if per controllare il comando, non è molto elegante, ma a fini didattici è sopportabile la mancanza di stile.
Ci siamo limitati ad implementare 4 “comandi”, da un lato sufficienti per vedere il nostro server in azione, dall’altro abbastanza esplicativi da permettere l’implementazione di qualsiasi altro tipo di comando .
Il comando “Query”, esegue appunto una query. Supponiamo che la stringa ricevuta sia la seguente:
“Query:select * from tabella where ID=13”. Il parser troverà un segno di due punti in posizione 6 e dividerà la stringa di conseguenze. Avremo così il comando “Query” e la stringa di dati “select * from tabella where ID=13”. Osservando le righe dalla 210 alla 280 è piuttosto semplice capire ciò che accade; la stringa di dati viene passata come argomento alla funzione ( termine orrendo per definire un metodo della nostra classe) ExecuteQuery ( righe 510-600), che lancia la query ed eventualmente restituisce un messaggio di errore.
 

210     if (comando.equals("Query")) {
220      String esegui=ExecuteStatement(data);
230      out.println("Query:" + esegui); 
240      if (esegui !="OK")
250      {
260       out.println("Errore:esecuzione della query non riuscita");
270      }
280     }
Il comando getString lancia il metodo getString interno alla classe ( righe 610-680), che legge un campo di tipo stringa  dato il nome del campo, e ne restituisce il valore.
 
290     if (comando.equals("getString")) {
300      out.println("getString:" + getString(data));
310 }


Stesso meccanismo per il comando moveNext, che in caso di EOF restituisce appunto la stringa “EOF” 
 

320     if (comando.equals("moveNext")) {
330      out.println("moveNext:" + moveNext());
340     }


Il comando “Esci” serve per chiudere la connessione fra client e server e liberare il socket. Infatti, il comando break che viene lanciato, fa uscire dal loop interno portando l’esecuzione alla riga 400, con in seguito la chiusura del socket e il ritorno alla riga 120, dove il server si mette in ascolto per una successiva connessione.
 

350     if (comando.equals("Esci")) {
360      break;
370     }
380    }
390               }
400                  try {
410    sock.close();
420   } 
430   catch(Throwable ee){
440   System.out.println(ee.toString()); 
450       } 
460  }
470         } catch (Throwable e) {
480             System.out.println(e.toString());
490         }
500    }

510 public static String ExecuteStatement(String TestoQuery){
520  try{
530   rs = stmt.executeQuery(TestoQuery);
540   rsmd = rs.getMetaData();
550   return "OK";
560  }
570  catch (Throwable t1){
580   return t1.toString();
590  }
600     } 

610     public static String getString(String Campo){
620  try{
630   return rs.getString(Campo);
640  }
650  catch (Throwable t1){
660   return t1.toString();
670  }
680     } 

690     public static String moveNext(){
700  try{
710   if (! rs.next())
720    return "EOF";
730   else 
740    return "OK";
750  }
760  catch (Throwable t1){
770   return t1.toString();
780  }
790     } 


Come è possibile vedere, risulta molto semplice aggiungere il codice per la gestione di qualsiasi altro metodo o proprietà del database, in modo da ottenere un’interfaccia di comunicazione il più simile possibile a quella utilizzata per il normale accesso via JDBC ad un database. 
 

Utilizzo del server
In attesa di sviluppare l’applet per l’utilizzo del server, è comunque possibile fare degli esperimenti per testare o ampliare le funzionalità esposte dal server stesso. Allo scopo, è sufficiente fare le seguenti cose:
1) Creare un database e creare la voce ODBC corrispondente con un nome simbolico da inserire nel codice del server.
2) Compilare e lanciare il server
3) Aprire una sessione di telnet sull’indirizzo della macchina che lo ospita, porta 3000. Nel caso si usi il telnet standard di windows, ricordarsi di attivare l’eco, altrimenti non si vedranno visualizzati i dati digitati.
Una volta aperta la sessione, per eseguire una query, scrivere “Query: select * from tabella”, naturalmente con una query che abbia senso per il database che si utilizza. Dare l’invio. Se la query è corretta non ci sarà risposta, altrimenti comparirà il messaggio d’errore apposito.
Lanciare un “moveNext:” per posizionarsi sul primo record del recordset estratto.
Lanciare un “getString:Nome”, dove al posto di “Nome” ci sia il nome di un campo stringa esistente nel database. Se tutto va bene, si otterrà il valore del campo richiesto.
Con il comando “Esci:”, il server chiuderà la sessione in corso per mettersi in attesa di una nuova sessione.
In questo modo è possibile testare in maniera semplice tutto ciò che si voglia implementare nel server.
 
 
 

Note
In questa implementazione abbiamo volutamente trascurato tutta una serie di importanti funzionalità, parte delle quali vedremo nel prossimo articolo e parte sono lasciate al lettore in quanto semplici estensioni di quanto fin qui visto.
Ne cito alcune:
1) gestione completa delle eccezioni 
2) Comportamento multithreading del server
3) gestione dei MetaData del database
4) Possibilità di indicare via client anche il database al quale accedere
5) Tutta una serie di funzioni di recupero dati dal recordset.
6) Gestione delle query di update 
 
 
 

Conclusioni
Abbiamo visto un bel po’ di materiale sul quale ragionare. E’ bene digerire tutta la struttura prima di addentrarci nelle ulteriori implementazioni che vedremo nel prossimo articolo. Nel frattempo è comunque possibile già estendere notevolmente le potenzialità del nostro server. E’ da notare che questa struttura non si limita al caso di accesso ad un database; virtualmente, il nostro server è un genericissimo application server al quale possiamo far fare ciò che vogliamo, con un meccanismo analogo ad una “Remote Procedure Call”, in cui il client ordina al server di fare qualcosa. E'ora tempo di accomiatarsi e di darci appuntamento alla terza parte dell’articolo.
 

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it