|
L'ultimo
tassello del mosaico del nostro Framework è quello di rendere
semplice l'accesso al database. La API messa a disposizione da Java è
il JDBC, che richiede di creare una connessione ad database prima di
poter eseguire degli stament. Il posto logico più logico dove
creare la connessione è nella init della nostra Servlet.
Successivamente si possono eseguire statement SQL
(stat.executeQuery("select * from Utenti"). Questo
metodo però ha due svantaggi: il primo è che quasi
tutti i database, prima di eseguire uno statement lo compilano in un
formato interno e poi lo eseguono, quindi su ha un rallentamento per
ogni query, che si potrebbe evitare preparando prima tutti gli
statement. Il secondo svantaggio è che devo
fornire degli statement sintatticamente corretti, per cui devo
preoccuparmi di dettagli come il quoting di una stringa o la
formattazione di una data.
Il
JDBC supporta la nozione di PreparedStatemet, ovvero di
statement precompilato che può essere eseguito più
volte cambiando i parametri. Per esempio per inserire una stringa in
una tabella posso usare un PreparedStatement pstat =
con.prepareStatement("insert into mytable values(?)"),
creato una volta e per tutte all'inizializzazione della servlet, che
può essere rieseguito quante volte si vuole cambiando i
parametri: pstat.setString(1, val); pstat.executeQuery();.
Notare che in quest'ultimo caso, poiché specifichiamo che il
tipo del parametro è una stringa, il quoting diventa un
problema del database server e non più nostro. Vediamo
dunque che il modello delle nostre servlet comincia a prendere forma:
class
MiaServelet extends javax.servlet.http.HttpServlet {
//
Statement SQL
private
static final String INSERT = "insert into MyTable values (?)";
private
String driver, dburl, username, password;
public
void init(ServletConfig cfg) {
//
leggi dai parametri i parametri della connessione
...
initConnection();
}
//
inizializziamo la connessione
private
Connection con;
private
PreparedStatement insert;
private
void initConnection() {
Class.forName(driver);
con
= DriverManager.createConnection(con);
insert
= con.prepareStatement(INSERT);
}
//
serviamo le richieste
public
void service(HttpServletRequest req, HttpServletResponse res) {
...
}
}
Notare
come abbiamo isolato gli statement SQL in costanti modo da isolarli
nel codice, anche se forse sarebbe opportuno porli addirittura in un
file di risorse. Per quanto i database siano tra di loro teoricamente
compatibili, all'atto pratico qualche problema che costringe a
mettere mano all'SQL si incontra, anche se devo dire dopo un po' si
impara a scrivere statement SQL che funzionano indifferentemente con
vari database come Oracle, Interbase e SQLServer (sempre tenendosi
molto bassi e facendo finta di essere molto ingnoranti di SQL…).
I problemi maggiori si presentano nella creazione delle tabelle e nei
tipi di dato, che tendono ad essere diversi da database a database.
Un
altro accorgimento importante è l'isolamento delle operazioni
di inizializzazione del database in un metodo initConnection
per rendere il codice più robusto. Infatti database e web
server sono due programmi diversi e separati: in pratica succede, e
anche abbastanza spesso, che il database cada o divenga indisponibile
per qualche ragione, bloccando irrimediabilmente la servlet, finché
non viene reinizializzata. Per cui è opportuno scrivere il
codice in modo da resettare la connessione quanto più spesso
possibile. Io solitamente resetto la connessione al database ogni
volta scatta una eccezione imprevista. In pratica nove volte su dieci
si tratta di qualche eccezione relativa a problemi di connessione con
il database. Questo piccolo accorgimento è la differenza tra
una servlet che può girare per mesi senza problemi e una
servlet che si blocca ogni due giorni e va monitorata costantemente…
Quando
si effettua un inserimento, i PreparedStatement hanno uno svantaggio:
si devono trasferire a mano tutti i dati dalla form allo statement.
Quindi un metodo doInsert che salva i dati specificati un una
form sara qualcosa come:
void
doInsert(HttpData data) {
insert.setString("nome",
data.get("nome"));
insert.setInt("eta",
data.get("eta"));
insert.setString("email",
data.get("email"));
...
insert.executeUpdate();
}
Questa
incombenza è sufficientemente frequente da indurmi a cercare
un modo di evitare di copiare i dati da una form ad un
PreparedStatement: per questa ragione ho inventato il MarkedStatement
del listato 2.
package agenda;
import java.sql.*;
import java.util.*;
public class MarkedStatement {
private Hashtable keys = new Hashtable();
private String stat;
private PreparedStatement prep;
public PreparedStatement getStat() { return prep; }
public MarkedStatement(Connection con, String stat)
throws SQLException
{
this.stat = stat;
int max = stat.length();
boolean inQuote = false;
StringBuffer res = new StringBuffer();
String tmp; char c; int j; int n =0;
for(int i=0; i<max; ++i)
switch(c=stat.charAt(i)) {
case '\'':
inQuote = !inQuote;
break;
case '$':
case '%':
case '!':
if(inQuote)
res.append(c);
else {
for(j=i+1; j<max && Character.isLetterOrDigit(stat.charAt(j)); j++);
if(j>i+1) {
++n;
res.append('?');
tmp =stat.substring(i,j).toUpperCase();
keys.put(tmp.intern(), new Integer(n));
i=j-1;
}
}
break;
default:
res.append(c);
break;
}
System.err.println("preparing statement :\n"+res);
try {
prep = con.prepareStatement(res.toString());
} catch(Exception ex) {
ex.printStackTrace();
}
}
public void copyFrom(HttpData data)
throws SQLException
{
int i;
String k, v;
int nfields = keys.size();
String[] fields = new String[nfields];
Enumeration e = keys.keys();
while(e.hasMoreElements()) {
k = (String)e.nextElement();
i = ((Integer)keys.get(k)).intValue();
fields[i-1]=k;
}
for(i=0; i<nfields; ++i) {
k = fields[i];
switch(k.charAt(0)) {
case '$':
prep.setString(i+1, v = data.getString(k.substring(1)));
System.err.println("<br>copying string "+k+"="+v+" in "+(i+1));
break;
case '%':
int vi;
prep.setInt(i+1, vi = data.getInt( k.substring(1)));
System.err.println("<br>copying int "+k+"="+vi+" in "+(i+1));
break;
case '!':
double vd;
prep.setDouble(i+1, vd=data.getDouble(k.substring(1)));
System.err.println("<br>copying double "+k+"="+vd+" in "+(i+1));
break;
default:
System.err.println("unknown type field "+k);
break;
}
}
}
public ResultSet executeQuery() throws SQLException {
return prep.executeQuery();
}
public int executeUpdate() throws SQLException {
return prep.executeUpdate();
}
private String quote(String s) {
StringBuffer res = new StringBuffer("'");
char c;
for(int i=0; i<s.length(); ++i) {
c=s.charAt(i);
if(c=='\'')
res.append("''");
else
res.append(c);
}
res.append('\'');
return res.toString();
}
}
Si
tratta semplicemente di un PreparedStatement che ricorda il
tipo dei suoi campi ed ha la capacità di trasferire
automaticamente i campi di una HttpData in campi omonimi del
PreparedStatement. Nell'esempio di prima, si costruisce
all'inizializzazione un MarkedStatemente insert = new
MarkedStatement("insert into Utenti values($nome, %eta,
$email"). Il prefisso $ indica campi stringa, il
prefisso % quelli interi e il prefisso ! i campi di
tipo float. Adesso è possibile trasferire i valori dalla form
automaticamente con un insert.copyFrom(data) evitando di
specificarli ad uno ad uno. Il problema inverso, ovvero di trasferire
i dati dal risultato di una query a una form HTML è stato già
risolto tramite l'uso di variabili nei template HTML ottenuti con il
JSPCompiler.
|