Una delle esigenze maggiormente sentite dagli sviluppatori di software è quella di riutilizzare codice già scritto, procedure già ideate, in particolar modo quando si tratta di operazioni semplici (immissione di dati in un database, algoritmi già usati e testati), svolte centinaia di volte, magari nell'ambito dello stesso progetto.
In aiuto a questa esigenza viene incontro il mondo della programmazione orientata agli oggetti. In questo articolo ci occuperemo di creare un framework in Java utile per consentire la gestione delle operazioni di persistenza in maniera rapida e soprattutto automatica.
Attraverso i meccanismi di astrazione mostreremo come sia possibile creare un sistema capace di gestire per noi tutte le operazioni di lettura, scrittura e modifica di un database.
Analisi e requisiti
Chi ha avuto a che fare con lo sviluppo di web application in ambiente Java, sicuramente conosce i Javabean (da non confondere con gli Enterprise Java Beans), le cui particolarità consentono uno sviluppo per componenti basato proprio su questi oggetti "contenitore".
Un Javabean, infatti, è una classe Java che presenta una serie di campi, rappresentanti le proprietà dell'oggetto ed i relativi metodi accessori (get e set). Metodi di logica complessa possono essere previsti, in quanto normali classi java, ma, l'idea del Javabean è quello di incapsulare dei dati da mostrare (attraverso i loro specifici tag) nelle pagine JSP.
Questa loro particolarità li rende paragonabili a delle righe di una tabella di un database, dove le proprietà sono le colonne. Questo paragone, infatti, li rende spesso interpreti di quello che possiamo definire come mapping object-relazionale, cioè, trasposizione di dati da un database relazionale ad un oggetto (in questo caso una classe Java).
L'idea del framework nasce con l'esigenza di automatizzare l'operazione di mapping da database ad oggetto e viceversa, utilizzando come oggetti contenitore i Javabeans. La cosa sarà svolta in maniera automatica grazie alle proprietà della reflection di java, che consente a runtime, di interrogare la classe dell'oggetto per recuperarne i campi ed operare sui metodi accessori (get e set) per accedere ai dati contenuti.
La figura mostra con chiarezza l'idea alla base dell'operazione. Il Javabean Account virtualizza la tabella Account di un database relazionale ed ha come proprietà le medesime presentate dalla tabella.
Partendo dal presupposto che nella nostra applicazione web based abbiamo un database che ha una sua struttura, composta di tabelle, e che avremo tanti javabean quante sono le tabelle, vediamo come realizzare uno strumento capace di effettuare per noi tali operazioni ripetitive.
Progettazione e implementazione
Scopo del framework, una volta realizzato, è quello di creare un meccanismo di mapping automatico creando una semplice classe java che abbia lo stesso nome della tabella e le stesse proprietà della tabella.
La classe da cui faremo ereditare i nostri futuri Javabean è la classe AbstractStorageFactory
: come vediamo essa ha i metodi tipici per la gestione della persistenza. La classe inoltre implementa l'interfaccia vuota Storable (una marker interface, che ci serve solo per capire che un oggetto fa parte di una specifica gerarchia).
Tale classe ha un riferimento ad uno StorageBridge
, che ricalca gli stessi metodi di logica di AbstractStorageFactory
, in quanto, come vedremo nell'implementazione, ognuno di essi delega al bridge l'operazione.
Listato 1. Definisce la strategia da adottare per la persistenza dei dati
...//
public abstract class AbstractStorageStrategy implements Storable {
private StorageBridge sb;
..//
//Metodo per l'inserimento nello strato persistente
public void insert(){
//Siccome questa classe implementa Storable, possiamo passare il riferimento a se stessa
sb.insert(this);
}
//Metodo per l'update nello strato persistente
public void update(){
sb.update(this);
}
//Metodo per l'eliminazione nello strato persistente
public void delete(){
sb.delete(this);
}
//Metodo per il caricamento dallo strato persistente
public void load(){
sb.load(this);
}
}
Le operazioni passano il riferimento a this
, in quanto sarà proprio esso (dinamicamente, gli eredi che creeremo) oggetto delle operazioni dello StorageBridge
.
La realizzazione concreta della persistenza sul database è curata dalla classe DatabaseStorage
. Questo modo di operare ci darà la possibilità di cambiare il layer di persistenza senza dover modificare il resto del sistema qualora in futuro avremo quest'esigenza.
Listato 2. Effettua le operazioni concrete sullo strato di persistenza
public class DatabaseStorage implements StorageBridge {
//Connessione al database: manager che gestisce le connessioni
private DBConnection conn;
public void insert(Storable s) {
//Recupero il nome della classe (e quindi della tabella)
String tableName=ReflectionUtils.getClassName(s);
//Recupero la lista di campi della classe (e quindi della tabella)
String campi[]=ReflectionUtils.getStringFields(s);
//Creo la query, valorizzandola con il nome della classe, i suoi campi ed i rispettivi valori contenuti nell'oggetto
String sql="INSERT INTO "+tableName+" ("+fillQueryFields(campi)+")" +
" values ("+fillQueryValues(s,campi)+")";
try{
conn.openDBConnection();
Statement st=conn.getConnection().createStatement();
st.execute(sql);
st.close();
conn.closeDBConnection();
}catch(SQLException e){
e.printStackTrace();
}
}
//Restituisce la lista di campi valorizzata
private String fillQueryValues(Storable s,String[] campi) {
String toRet="";
for(int i=0;i<campi.length;i++){
String field=campi[i];
toRet+="'"+ReflectionUtils.getProperty(field,s)+"'";
if (i+1<campi.length)
toRet+=", ";
}
return toRet;
}
//Restituisce la lista di campi in forma di stringa
private String fillQueryFields(String[] campi) {
String toRet="";
for(int i=0;i<campi.length;i++){
String field=campi[i];
toRet+=field;
if (i+1<campi.length)
toRet+=", ";
}
return toRet;
}
..//
}
La classe appena vista fa uso di DBConnection
, per la gestione delle connessioni al database e della classe ReflectionUtils
, che ha una serie di metodi utili per interrogare l'oggetto e recuperarne a runtime le proprietà. L'oggetto, di tipo Storable, non sarà altro che una delle classi eredi di AbstractStorageStrategy
, quindi uno dei bean che creeremo.
Nel listato abbiamo riportato l'operazione di inserimento (le altre operazioni sono analoghe). Attraverso la classe ReflectionUtils
recuperiamo il nome del bean (quindi il nome della tabella) ed il nome dei suoi campi (quindi delle colonne della tabella). Interrogando l'oggetto sulle sue proprietà sarà facile costruire una query SQL che opererà sulla tabella del database, a patto che sia rispettata la convenzione sui nomi: ogni classe deve essere chiamata come la specifica tabella e la stessa cosa per quanto riguarda gli attributi. A questo punto creiamo la query SQL, avendo tutto quello di cui abbiamo bisogno grazie alla classe ReflectionUtils
che vediamo nel listato seguente.
A questo punto non ci rimane che iniziare a creare dei Javabean e le relative viste per l'immissione dei dati o la visualizzazione. Ad esempio, ipotizziamo di avere una tabella User, con le colonne id, nome e cognome, dovremo creare una classe User erede di AbstractStorageStrategy
con tre campi (id, nome e cognome) e così per tutte le tabelle su cui vogliamo operare.
La tabella avrà la seguente struttura:
CREATE TABLE 'user' (
'id' varchar(255) NOT NULL default '',
'nome' varchar(255) NOT NULL default '',
'cognome' varchar(255) NOT NULL default '',
PRIMARY KEY ('id')
)
Il Javabean, di conseguenza, questa struttura:
Listato 3. Struttura del JavaBean
package it.html.sm.javabeans;
..//
public class User extends AbstractStorageStrategy{
private String id;
private String nome;
private String cognome;
//Metodi getter e setter per ogni proprietà
public String getCognome() {
return cognome;
}
public void setCognome(String cognome) {
this.cognome = cognome;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
}
La classe estende la nostra classe del framework ed esporta i metodi accessori per gli attributi proposti (identici alla tabella del database). Importante è da ricordare che per poter sfruttare le proprietà dei Javabean sulle pagine JSP è necessario prevedere il costruttore di default (nel nostro caso esiste solo quello di default).
Ora, questa classe, richiamando i metodi insert
, update
, delete
e load
, effettuerà le specifiche operazioni, grazie allo strato di middleware che abbiamo creato: quindi vediamola all'opera insieme alle JSP.
Vediamo ad esempio l'operazione di inserimento:
Listato 4. Operazione di inserimento
//insertUser.jsp
<html>
<head>
<title>Maschera di inserimento</title>
</head>
<body>
<h1>Inserisci nuovo utente</h1>
<form method="post" action="forwardUser.jsp?op=insert">
<ul>
<li>ID: <input name="id" type="text"></li>
<li>Nome: <input name="nome" type="text"></li>
<li>Cognome: <input name="cognome" type="text"></li>
</ul>
<input type="submit" value="Continua">
</form>
</body>
</html>
Si tratta di una pagina al cui interno è presente una form con dei moduli di inserimento che hanno gli stessi nomi degli attributi.
Vediamo che la action della form non è una servlet come ci aspettavamo, ma un'altra pagina JSP:
Listato 5. Pagina JSP come action della form
<jsp:useBean id="user" class="it.html.sm.javabeans.User" scope="request"/>
<!--
Setto tutte le proprietà provenienti dalla form di immissione
-->
<jsp:setProperty name="user" property="*"/>
<!--
Passo il controllo al gestore delle azioni: la servlet
-->
<jsp:forward page="action"/>
Facciamo questo per poter sfruttare l'operazione <jsp:setProperty ...> che ci consente di settare automaticamente tutte le proprietà del Javabean in questione, recuperando i parametri dalla request in automatico. Questo avviene grazie alla presenza dei metodi accessori set e della convenzione di associare gli stessi nomi ai moduli di input della form nella pagina precedente (insertUser.jsp).
Fatto questo inoltriamo il controllo alla servlet action, che si ritroverà dentro la request il bean opportunamente valorizzato ed il parametro op=insert
. In generale questa pagina sarà utilizzata tutte le volte prima di richiamare la servlet, proprio per sfruttare il metodo setProperty
e valorizzare quindi le proprietà del bean.
La servlet si presenta in maniera molto semplice come strato di logica in cui viene effettuata l'operazione sul bean recuperato dalla request (e quindi già valorizzato secondo gli input dell'utente) e viene visualizzato un messaggio di testo o inoltrata una nuova pagina JSP.
Listato 6. Servelet con i metodi Insert, Load, ecc
..//
public class Dispatcher extends HttpServlet {
/**
* Useremo questo metodo come dispatcher in base al parametro op
* creando un metodo per ogni funzione richiesta
* @throws IOException
* @throws ServletException
* */
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException{
String op=req.getParameter("op");
if (op.equalsIgnoreCase("insert")){
doInsertUser(req,resp);
}
..//
if (op.equalsIgnoreCase("load")){
doLoadUser(req,resp,"showUser.jsp");
}
}
private void doInsertUser(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//Recupero del bean: usiamo la classe astratta, visto che dinamicamente, non
//conosciamo il tipo concreto.
AbstractStorageStrategy jbean=(AbstractStorageStrategy) req.getAttribute("user");
//Richiamiamo il metodo di inserimento
jbean.insert();
resp.getOutputStream().println("L'oggetto è stato salvato correttamente");
}
..//
private void doLoadUser(HttpServletRequest req, HttpServletResponse resp,String page) throws ServletException, IOException {
AbstractStorageStrategy jbean=(AbstractStorageStrategy) req.getAttribute("user");
//Richiamiamo il metodo di inserimento
jbean.load();
//Salviamo l'oggetto nella request, in modo da renderlo visibile
RequestDispatcher rd=this.getServletContext().getRequestDispatcher("/"+page);
//alla pagina JSP che verrà inoltrata
rd.forward(req,resp);
}
}
Oltre al metodo doInsertUser()
che effettua l'operazione di inserimento, vediamo anche il metodo doLoadUser()
che si preoccupa di caricare i dati e di inoltrarli verso una vista specifica.
Si faccia attenzione ora al tipo utilizzato nella servlet: si tratta del supertipo AbstractStorageStrategy
. Ciò implica che neanche lo strato di controllo avrà la necessità di conoscere il tipo concreto, quindi, anche la servlet potrà essere usata genericamente da tutti i Javabean che andremo a creare.
Per completare vediamo la JSP che si occupa di visualizzare l'oggetto.
Listato 7. JSP che si occupa di visualizzare l'oggetto
<jsp:useBean id="user" class="it.html.sm.javabeans.User" scope="request"/>
<html>
<head>
<title>Maschera di visualizzazione</title>
</head>
<body>
<h1>Visualizzazione stato</h1>
<ul>
<li>ID: <jsp:getProperty name="user" property="id"/></li>
<li>Nome: <jsp:getProperty name="user" property="nome"/></li>
<li>Cognome: <jsp:getProperty name="user" property="cognome"/></li>
</ul>
<hr/>
<p>
<a href="forwardUser.jsp?op=preUpdate&id=<jsp:getProperty name="user" property="id"/>">Modifica profilo</a> |
<a href="forwardUser.jsp?op=delete&id=<jsp:getProperty name="user" property="id"/>">Elimina profilo</a>
</p>
</body>
</html>
Notiamo due cose: prima di tutto che non abbiamo aperto nemmeno uno scriptlet (<% ... %>), quindi la pagina è fruibile anche da un tool di sviluppo o da un designer non esperto di java.
Come secondo aspetto notiamo i link che rimandano alla pagina forwardUser.jsp, passando come parametro l'id dell'oggetto in questione e l'operazione da effettuare. Come abbiamo visto, in quella pagina si effettuerà il setting delle proprietà (in questo caso del solo id) e verrà inoltrata l'operazione verso la servlet (identificata dal parametro op). Ad esempio, cliccando sul link di eliminazione verrà effettuata l'operazione di eliminazione dell'utente appena selezionato.
Per concludere vediamo il risultato di una generica operazione di inserimento e poi di visualizzazione.