MokaByte Numero  40  - Aprile 2000
Applicazioni Web Three-tier con utilizzo
di Java Applet, Servlet e JDBC per interrogazioni a DBMS eterogenei
di 
Stefano
Rossini
In questo articolo si parla di come organizzare il codice di un'applicazione Web Three-Tier sviluppando servlet utilizzanti JDBC

Vediamo come sia possibile ad un qualsiasi computer dotato di web browser, una dotazione software minima e ormai molto comune,  di accedere a informazioni distibuite in rete su DBMS eterogenei. Si otterrà questo scopo senza dover installare software specifico sulle macchine client in modo da potere erogare il servizio senza dovere intervenire su ogni singola macchina degli utenti. ottenere lo scopo prefissato si utilizzerà un'architettura di comunicazione di tipo Three-Tier utilizzando la tecnologia Web per il supporto di comunicazione e Java come linguaggio di sviluppo.

Nel precedente articolo abbiamo analizzato i vantaggi che comporta l'utilizzo di un'architettura Three-tier in ambito Web utilizzando la tecnologia Java sia sul livello di Presentazione (Applet), sia sul livello Middleware (Servlet e JDBC). In questo articolo vediamo una possibile organizzazione del codice al fine di avere un'applicazione facilemente configurabile, flessibile e scalabile. Si vuole inoltre che tale configurazione possa avvenire dinamicamente senza ricompilazione di codice. Come già detto il livello middleware è composto dal Web server e dai Servlet.
 
 

Servlet
I Servlet (come gli Applet) sono un framework: la creazione di un Servlet comporta l'eredità obbligatoria della classe HttpServlet, l'entry point è il metodo init() che viene chiamato solamente in fase di inizializzazione mentre in caso di chiamata dal Web Server il framework invoca il metodo :

service(HttpServlet req, HttpServletResponse res) 

dove gli oggetti req e res rappresentano le generiche richieste e risposte HTTP.
All'interno di quest'ultimo metodo vengono scandite tutte le operazioni per il trattamento della richiesta HTTP, la transizione con il DBMS e la relativa risposta contenente il risultato della transazione.


Figura 1

In figura 2 si riporta il class diagram di un Servlet che utilizza JDBC.
 

Figura 2

I servlet dell'applicazione sono tre : loginServlet, metaServlet e dbServlet.
Il loginservlet gestisce l'operazione di identificazione dell'utente avvalendosi del database locale al Web server contenente le username e le password degli utenti abilitati ad usufruire del servizio.[2]
Il dbServlet riceve il comando SQL, interroga il DB e restituisce il risultato formattato in una pagina HTML.[3]
Il metaServlet è il cuore dell'applicazione.
Tale servlet mediante JDBC preleva le informazioni sul DB e sugli oggetti del DB, tali informazioni sono i metadata cioè i dati descrittivi del DB.
Tramite i metadata è possibile interrogare dinamicamente un database senza conoscerne a priori la composizione.
Nel nostro caso specifico verranno prelevati i seguenti metadati: nome del poduttore del DBMS, nomi di tutte le tabelle contenute nel DB e, per ogni tabella, il nome ed il tipo di ogni campo.
Il metaServlet utilizzerà i metadata in due diversi modi:

  1. modo descrittivo: i metadata vengono formattati in una pagina HTML 
  2. modo operativo: gli stessi metadata vengono resi disponibili come parametri di un applet per permettere all'utente di formulare query SQL in modo grafico .
Questa distinzione avviene mediante il valore del parametro metadata che viene specificato come parametro HTTP.
La cosa importante è che l'Applet è trasparente al tipo e alla composizone del DB e quindi estremamente generica.
Si ottiene così la possibilità di permettere all'utente di effettuare query a DB senza che a priori lui ne debba conoscere la struttura.
In figura 3 viene riportato il funzionamento del metaServlet.
Nel caso l'utente richieda la descrizione del DB(fig.3(1)), viene inoltrata una richiesta al metaServlet che si collega al DB, preleva i metadata (fig.3(2)), e li restituisce formattati in tabelle HTML (fig.3(3)). Nel caso l'utente invece voglia interrogare il DB (fig.3(4)), il metaServlet preleva i metadata (fig.3(5)) come nel caso precedente, ma questa volta li utilizza come parametri dell'Applet QueryBuilder che viene scaricato via rete ed eseguito dalla JVM del browser del client (fig.3(6)).
A questo punto l'utente è in grado di interrogare il DB.
 
 


Figura 3








Configurazione del sistema
Il nome della classe driver da utilizzare e il nome del JDBC Url che identifica il DB a cui connettersi non sono stati cablati nel codice del Servlet bensì sono specificati come parametri HTML all'interno del file di nome dbConfig.html. Tali parametri vengono letti da un'Applet e inoltrati come parametri della richiesta HTTP al web server.
I servlet prelevano tali valori come parametri della richiesta HTTP ed effettuano la connessione al DB.
Abbiamo quindi che gli Applet effettuano un ponte tra configurazione (file dbConfig.html) e attuazione (i Servlet). I Servlet sono il punto finale della catena in cui avviene la conessione secondo i parametri configurati nel file HTML. 
 
 


Figura 4






In questo modo si ha che :
Applet 

  • legge i parametri HTML (fig.11(1)) specificati dalle tag PARAM mediante l'invocazione del metodo getParameter :

  • strAppletProperty= getParameter(“html_parameter”);
  • riceve username e password dall'utente (fig.11(2))
  • compone l'URL del servlet da invocare 

  • try { 
         URL urlNext = new URL(getDocumentBase(),strNextUrl);
        getAppletContext().showDocument(urlNext);
    } catch(Exception excp) { excp.toString(); }


L'invocazione del metaServlet avviene mediante il seguente URL :…

http://localhost:8080/servlet/metaservlet?
metadata=yes&driver=sun.jdbc.odbc.JdbcOdbcDriver         &jdbcurl=jdbc:odbc:ambulatorio&username=rossini&password=aran

Il servlet :

  • legge i parametri driver,jdbcurl,username e password della richiesta HTTP 
  • si connette al DBMS nelle modalità specificate dai parametri HTTP


La pagina HTML (dbConfig.html) diventa il punto di configurazione dell'intera applicazione (A) insieme alla configurazione dei DSN (B).
Inoltre i DSN sono il nome logico del driver ODBC che è una DLL invocata a run-time dall'ODBC Manager.
Una modifica nel file db.html viene subito propagata automaticamente nell'intero sistema senza dovere ricompilare il codice.

Naturalmente questa flessibilità la si "paga" con una "lentezza" dovuta al fatto che ogni volta che si invoca un Servlet si devono ripetere le operazioni dicarcamento driver e connessione.
 
 
 

Esempio di configurazione 
Nel file db.html per ogni database accessibile bisogna specificare il nome del database, il nome della classe driver da utilizzare ed il JDBC URL.

Si riporta a titolo di esempio il file db.html in grado di poter accedere ad un DB di nome Ambulatorio mediante ODBC driver avente DSN di nome mySqlSrv7.

<HTML>
<HEAD><TITLE>* DB PAGE *</TITLE></HEAD>
...
<applet code=Caller.class codebase="/applet" width=320 height=260>
<PARAM NAME="name_db_1" VALUE="Ambulatorio">
<PARAM NAME="driver_1" VALUE="sun.jdbc.odbc.JdbcOdbcDriver">
<PARAM NAME="subProtocol_subname_1" VALUE="odbc:mySqlSrv7">
...
</applet></BODY></HTML>

Si vuole ora configurare il sistema per essere in grado di accedere ad un nuovo DBMS di tipo Oracle8.
Sul Web Server si devono effettuare i seguenti passi :

  1. configurazione dell'ODBC manager per aggiungere un nuovo DSN 
  2. aggiunta di 3 righe al file db.html (le modifiche che bisogna apportare al file db.html sono evidenziate in neretto) :


<HTML>
<HEAD><TITLE>* DB PAGE *</TITLE></HEAD>
...
<applet code=Caller.class codebase="/applet" width=320 height=260>
<PARAM NAME="name_db_1" VALUE="Ambulatorio">
<PARAM NAME="name_db_2" VALUE="Politecnico">
<PARAM NAME="driver_1" VALUE="sun.jdbc.odbc.JdbcOdbcDriver">
<PARAM NAME="driver_2" VALUE="sun.jdbc.odbc.JdbcOdbcDriver">
<PARAM NAME="subProtocol_subname_1" VALUE="odbc:mySqlSrv7">
<PARAM NAME="subProtocol_subname_2" VALUE="odbc:myOra8Poli">
</applet></BODY></HTML>

Al prossimo caricamento della pagine db.html l'utente è già in grado di accedere al database Politecnico.
Le operazioni appena descritte riguardano esclusivamente l'amministratore di Web Server, senza la necessità di effettuare alcuna modifica sulle postazioni client.
 
 
 
 
 

Il MetaServlet 
Come prima operazione, il Servlet verifica se l’utente possiede il Cookie, cioe’ se l’utente ha gia’ affrontato con esito positivo l’operazione di identificazione mediante loginServlet.
Per verificare se e’ presente il cookie dell’applicazione si utilizza il metodo getCookies della Classe Cookie :

private boolean isCookieValid(HttpServletRequest req)
  {
   Cookie cookies[] = null;
   boolean hadMyCookie = false;
   if ((cookies = req.getCookies ()) != null) 
   {
    for (int i = 0; i < cookies.length; i++) 
    {
     if (cookies [i].getName().equals(myCookieName)) {
      hadMyCookie = true;
      break;
     } 
    } 
   } 
   return hadMyCookie;
  }

Come seconda operazione, il servlet preleva i parametri HTTP della richiesta ricevuta utilizzando
il metodo getParameter dell’oggetto di classe HttpServletRequest e di nome Req.
strServletProperty   = req.getParameter("httpRequestParameter");

Tra questi parametri il Servlet si aspetta di ricevere :

  • il nome del driver JDBC da utilizzare (memorizzato nell'oggetto String strDriver)
  • il nome del JdbcUrl (memorizzato nell'oggetto String strJdbcUrl)
  • username (memorizzato nell'oggetto String strUsername )
  • password (memorizzato nell'oggetto String strPassword )


Il Servlet provvede al caricamento esplicito della classe del driver avente come nome il contenuto della proprietà strDriver, utilizzando il metodo Class.forName.
Class.forName(strDriver);
Il driver istanzia automaticamente un oggetto ed invoca la propria registrazione tramite il metodo DriverManager.registerDriver.
IL DriverManager è la classe del JDBC che fornisce i servizi per la gestione dei Driver. Utilizzando il metodo getConnection del DriverManager è possibile creare la connessione con il database.
Tale metodo utilizza l’URL del database e due oggetti di classe String. 
La prima stringa identifica il nome dell’utente con cui connettersi al database mentre la seconda contiene la password dell’utente specificato. 
Tale metodo restituisce un oggetto Connection che rappresenta la connessione con il database.
Connection con = DriverManager.getConnection(strJdbcUrl, strUsername, strPassword);
Una volta creato può essere usato per creare oggetti per l’esecuzione delle istruzioni SQL. 
Tra questi parametri il Servlet si aspetta di ricevere :

  • il nome del driver JDBC da utilizzare
  • il nome del JdbcUrl
  • La username per l’accesso al Web Server
  • La password per l’accesso al Web Server
  • Il parametro metaData
A questo punto il Servlet effettua :
  • caricamento della classe del driver 

  • Class.forName(strDriver);
  • Connessione al DB e relativa creazione dell’oggetto connection

  • con = DriverManager.getConnection(strJdbcUrl, strUsername, strPassword);
Il Servlet per ottenere i metadata del database utilizza l’interfaccia DataBaseMetadata del JDBC. Il metodo getMetaData dell’oggetto Connection restituisce un oggetto DatabaseMetaData. 
Non è possibile dichiarare direttamente questa interfaccia (come qualsiasi interfaccia JDBC), ma la si deve creare assegnandole il valore restituito dall'appropriato metodo (in questo caso il Connection.getMetaData()).
L’oggetto DatabaseMetaData fornisce dei metodi che restituiscono le informazioni specifiche sui database, quali le tabelle, le colonne presenti nella tabelle, i tipi dei dati ecc…
La sintassi per ottenere l’interfaccia DatabaseMetaData dall’oggetto Connection  è la seguente:
DatabaseMetaData dbmd = con.getMetaData();


Ottenuto l’oggetto DatabaseMetaData viene chiamato il metodo getMetaDataAndFillHeap al fine di  reperire :

  • il numero delle tabelle dle DB richiesto
  • Il nome di ogni tabella
  • per ogni tabella il numero di campi contenuto
  • per ogni campo reperire il suo nome e il suo tipo di dato
Il metodo getMetaDataAndFillHeap della Servlet utilizza i metodi dell’interfaccia DataBaseMetaData per reperire tutte le informazioni desiderate dal database memorizzandole nei seguenti References del Servlet.

Contatore delle tabelle del DB : private int myServletTablesCounter
Nomi delle tabelle : private String myServletTableName[]; 
Numero di colonne per ogni tabella : private int myServletNumColumns[]; 
Nomi delle colonne di ogni tabella : private String myServletColumnName[][]; 
Tipo del dato contenuto nella colonna di ogni tabella : private String myServletColumnType[][];
Nome del produttore del database : private String strDbProductName;

Il metaServlet dichiara questi references perché non sa a priori come è costituito il DB che deve interrogare.
L'effettiva allocazione di memoria verrà effettuata una volta note le dimensioni del DB.
In figura 5 si riporta un esempio nel caso di un semplice DB costituito da tre tabelle.
 
 


Figura 5

Il nome del produttore del Data Base (“Microsoft”,”Oracle”,etc…) si ottiene chiamando il metodo getDatabaseProductName().

strDbProductName = dbmd.getDatabaseProductName();

Successivamente  utilizza il metodo getTables() che restituisce un elenco di tutte le tabelle presenti nel database corrente.
Queste informazioni sulle tabelle sono restituite in un oggetto ResultSet e sono :
 

  1. TABLE_CAT È il catalogo della tabella corrente
  2. TABLE_SCHEM È lo schema della tabella corrente
  3. TABLE_NAME È il nome della tabella correnteTABLE_TYPE  (TABLE, VIEW, SYSTEM TABLE, GLOBAL TEMPORARY)
rsTables = dbmd.getTables(null, null, null, null); 


Per contare le tabelle di tipo “TABLES” si effettua una comparazione del parametro TABLE_TYPE contenuto nella posizione 4 dell’oggetto ResultSet.
 

// Count the TABLES
    rsTables = dbmd.getTables(null, null, null, null); 
    while(rsTables.next())
    {
     if(rsTables.getString(4).equals("TABLE"))
     {
      ++myServletTablesCounter;
     } 
    }  // end while getTables


Ottenuto il numero nelle tabelle si puo’ allocare la memoria per i vettori e per le righe degli oggetti matrice.

myServletTableName  = new String[myServletTablesCounter];
myServletNumColumns = new int[myServletTablesCounter];
myServletColumnName = new String[myServletTablesCounter][ ];
myServletColumnType = new String[myServletTablesCounter][ ];

Si memorizza ogni nome della tabella nel vettore myServletTableName.
Per ottenere il nome della tabella dall’oggetto tables, si utilizza il metodo getString(), per ottenere il valore memorizzato nella colonna 3.

    counter=0;
    rsTables = dbmd.getTables(null, null, null, null); 
    while(rsTables.next())
    { 
     String strTable = rsTables.getString(3);
     if(rsTables.getString(4).equals("TABLE"))
     {
      myServletTableName[counter]=strTable;
      ++counter;
     } 
    }  // end while getTables

Ottenuto il nome di tutte le tabelle, si vuole ottenere il nome ed il tipo di ogni colonna della tabella utilizzando il metodo getColumns(sempre dell'interfaccia DataBaseMetaData)

Il metodo getColumns() del DatabaseMetaData consente di ottenere tutte le colonne di una
tabella specificata. 

rsColumns = dbmd.getColumns(null, null,myServletTableName[counter], null); 

Questo metodo restituisce un oggetto ResultSet che ha le seguenti colonne 

  1. TABLE_CAT Catalogo della tabella della colonna corrente
  2. TABLE_SCHEM Schema della tabella della colonna corrente
  3. TABLE_NAME Nome della tabella della colonna corrente
  4. COLUMN_NAME Nome della colonna corrente
  5. DATA_TYPE Tipo di dato SQL della colonna da java.sql.Types
  6. TYPE_NAME Nome del tipo di dato (dipendente dalla sorgente di dati)


A questo punto per ogni tabella 

for(counter=0; counter<myServletTablesCounter; ++counter){
//si contano il numero di colonne che possiede.
for(counter=0; counter<myServletTablesCounter;++counter){
rsColumns = dbmd.getColumns(null, null,myServletTableName[counter], null);
     while(rsColumns.next()){
      ++myServletNumColumns[counter];
     }  // end while getColumns 

Sapendo il numero di colonne per ciascuna tabella e’ possibile allocare la memoria per le colonne degli oggetti matrici :

myServletColumnName[counter] = new 
   String[myServletNumColumns[counter]];
myServletColumnType[counter] = new 
   String[myServletNumColumns[counter]];

Allocata la memoria agli oggetti si può finalmente compiere l’ultimo "sospirato" passo : ricavare nome e tipo di ogni colonna memorizzandolo nell'opportuna riga e colonna degli oggetti matrici.
Si può ottenere il nome delle colonne effettuando un ciclo sui record dell’elemento columns
dell’oggetto, e ottenendo il valore dalla colonna COLUMN_NAME, che è la colonna numero 4.

rsColumns = dbmd.getColumns(null, null,myServletTableName[counter], null); 
  while(rsColumns.next()){
         myServletColumnName[counter][j]=
              ServletTableName[counter]+ "."+
              rsColumns.getString(4);
         myServletColumnType[counter][j] =
              rsColumns.getString(6); 
         ++j;
    }  // end while getColumns 
     } // end of for

A questo punto abbiamo tutti gli oggetti allocati e riempiti opportunamente di tutte le informazioni che volevamo.
A secondo del valore del parametro meta, il contenuto di tali oggetti sara’ utilizzato in modo differente.
Se la proprietà meta vale  true, l'utente ha richiesto una semplice descrizione del database, e viene quindi chiamato il metodo displayMetaData del Servlet
 


Figura 6


private void displayMetaData(PrintStream out) throws SQLException
  {
   int i=j=0;
   try {
   out.println("<center>");
   // Display the DB name 
   out.println("<font size=5><B>Database " + strDataBase + " is composed by :</font></B><br><br>"); 
   out.println("</center>");
for(i=0; i<myServletTablesCounter; ++i)
   {
    out.println("<center>");
    out.println("<table width=100 border=5 >"); // begin of HTML table 
    out.println("<tr>");
// Display the Table name 
    out.println("<td colspan=3 align=center><h3><i>"+myServletTableName[i]+"</i></h3></td>");
    out.println("</tr>"); 
    for(j=0; j<myServletNumColumns[i]; ++j)
    {
      out.println("<tr>");
      out.println("<td width=6% align=center>"+(j+1)+"</td>");
      out.println("<td width=47% align=center>"+myServletColumnName[i][j]+"</td>"); 
     out.println("<td width=47% align=center>"+myServletColumnType[i][j]+"</td>"); 
     out.println("</tr>"); 
    } 
    out.println("</table>");
    out.println("</center>");
    out.println("<br>");
   }
Se la proprietà meta vale false, viene chiamato il metodo della Servlet prepareApplet, dove tutte le informazioni dei metadata precedentemente ottenute, vengono utilizzate come parametri d'ingresso all'Applet AppletQueryBuilder.
 


Figura 7


Tale Applet costituirà la GUI di interrogazione del database.
private void prepareApplet(PrintStream out)
  {
   out.println("<BR><center>");
   out.println("<font size=5><B>Query Builder</font></B><br><br>"); 
   out.println("</center>");
   out.println("<center>");
   out.println("<applet code=appletQueryBuilder.class  codebase=/applet width=650 height=480>");
   out.println("<param name=backgroundParam    value=" + "008FFF"   + ">");
   out.println("<param name=foregroundParam    value=" + "000000"     + ">");
  out.println("<param name=next_url             value=" + "/servlet/dbservlet"+ ">");
   out.println("<param name=driver_name          value=" + strDriver  + ">");
   out.println("<param name=jdbc_url              value=" + strJdbcUrl + ">");
   out.println("<param name=name_dataBase  value=" + strDataBase+">"); 
   out.println("<param name=name_dataBase_product  value=" + strDbProductName +">");
   out.println("<param name=number_of_tables  value=" + myServletTablesCounter+">");
// Compose HTML parameters about DB tables and fields
   for(int i=0;i<myServletTablesCounter; ++i)
   {
    out.println("<param name=name_table_"+ (i+1) +" value="+myServletTableName[i]+">");
    out.println("<param name=num_of_columns_"+ (i+1) +" value="+myServletNumColumns[i]+">");
    for(int j=0; j < myServletNumColumns[i]; ++j)
    {
     out.println("<param name=name_field_"+ (i+1) +"_"+ (j+1) +" value="+myServletColumnName[i][j]+">");
     out.println("<param name=type_field_"+ (i+1) +"_"+ (j+1) +" value="+myServletColumnType[i][j]+">");
    }
   }
   out.println("<param name= number_of_max_columns value=" + getNumMaxColumns() + ">");
  // echo, send back, cookie value for user permission, Db username and DB password
out.println("<param name=user_permission value="+strCookieValue+">");
out.println("<param name=db_username value="+strUsername+">");
   out.println("<param name=db_password value="+strPassword+">");
  out.println("</applet>"); 
   out.println("</center>");
}
 
 

Conclusioni
Concludo nel ricordare che la flessibilità della soluzione proposta deve tenere conto del fatto che scrivere comandi SQL è un'operazione difficile se la compatibilità con DBMS eterogenei è uno degli obiettivi primari. 
SQL è un linguaggio comune di interrogazione di database nei limiti in cui il DBMS lo supporta.
Lo standard SQL 92 (standardizzato nel 1992 dall'ISO) definisce diversi livelli di compatibilità (Entry-Intermediate-Full) ma spesso il solo livello Entry è supportato.
Per questo motivo, per operazioni SQL di una certa entità, bisogna prestare molta attenzione.
L'Applet proposto nell'articolo è in grado di comporre SQL nella forma di semplici JOIN naturali, per estrarre dati da una o più tabelle funzionando senza problemi sia su Oracle che su Microsoft.
 
 
 

Bibliografia
[1] S.Rossini- "ApplicazioneWeb Three-tier", Mokabyte N.40, Aprile 2000
[2] G.Puliti - "JDBC: la teoria", Mokabyte N.9, Giugno 1997
[3] G.Puliti - "Servlet, JDBC e JavaServer", Mokabyte N.20, Giugno 1998
[4] D. Di Girolamo - "Servlet & Database", Mokabyte N.16, Febbraio 1998
[5] M.Sciabarrà "Un framework per applicazioni Web inJava" PDJ N.15 /Ed.Globali, 1999
[6] D.Esposito "Verso un database universale" CP N.83 /Infomedia, 1999
[7] M.Russo "Quanto sono compatibile con SQL 92 i DBMS?", CP N.83 /Infomedia, 1999
[8] Jamie Jaworski - "Java 2 - Tutto & oltre", - Apogeo, 1999
 

Stefano Rossini ha conseguito il diploma di laurea in Ingegneria Informatica.
Si occupa di applicazioni Network Management nell'ambito delle Telecomunicazioni, con l'utilizzo dei linguaggi C e Visual Basic.

 

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