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:
-
modo descrittivo:
i metadata vengono formattati in una pagina HTML
-
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 :
-
configurazione
dell'ODBC manager per aggiungere un nuovo DSN
-
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
:
-
TABLE_CAT
È il catalogo della tabella corrente
-
TABLE_SCHEM
È lo schema della tabella corrente
-
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
-
TABLE_CAT
Catalogo della tabella della colonna corrente
-
TABLE_SCHEM
Schema della tabella della colonna corrente
-
TABLE_NAME
Nome della tabella della colonna corrente
-
COLUMN_NAME
Nome della colonna corrente
-
DATA_TYPE
Tipo di dato SQL della colonna da java.sql.Types
-
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.
|