Un approccio bottom-up al moderno mapping object-relational. Usando i JavaBean e Java Reflection è possibile analizzare i meccanismi che stanno alla base del mapping di oggetti su un database relazionale.
Introduzione
Una delle principali difficoltà che si trova a fronteggiare chi progetta e sviluppa applicazioni java utilizzando database per mantenere in modo strutturato lo stato delle informazioni gestite, è proprio l’accesso e la manipolazione via script SQL dei dati. Infatti la maggior parte dei database utilizzati è di tipo relazionale; si tratta di un modello di database che consiste di diversi file separati che sono correlati l’un l’altro attraverso campi chiave. Si può accedere alle informazioni memorizzate in un file attraverso uno o più degli altri file, grazie alle relazioni stabilite tra questi. Ad esempio, un database relazionale vede il collegamento fra un database anagrafico degli impiegati in una società ed il database delle retribuzioni, tramite un codice univoco che identifica il singolo impiegato. Questo tipo di struttura richiede una certa mole di lavoro per essere mappata all’interno di un linguaggio ad oggetti: il modo più semplice per stabilire una corrispondenza tra tabelle e record di un db relazionale e classi java è quello di associare ogni record di una tabella ad una oggetto, generato come istanza di una classe che dispone di metodi in grado di leggere e modificare i valori associati ad ogni colonna della tabella. I componenti che meglio si prestano a questo tipo di utilizzo sono i JavaBean. L’evoluzione tecnologica degli ultimi anni ha ampiamente giustificato e anzi fortemente consigliato l’utilizzo di strumenti di mapping Object relational per sincronizzare lo stato di un oggetto. Lo stato dell’arte attuale vede lo scenario dominato da vari framework e tecnologie; accenneremo brevemente ad alcune delle più utilizzate: JDO, Hibernate, CMP di EJB 2.0.
JDO è principalmente un insieme di specifiche ed interfacce che devono essere implementate concretamente dai fornitori di prodotti e che sovrintendono all’accesso delle funzionalità; un importante caratteristica di JDO è la presenza del componente bytecode enhancer, che si occupa di modificare il bytecode delle classi che devono essere rese persistenti, aggiungendo il codice necessario all’operazione. Le specifiche JDO definiscono 10 stati in cui l’oggetto coinvolto in JDO può passare (sette obbligatori e tre opzionali), in funzione delle diverse chiamate ed operazioni che è possibile eseguire.
Hibernate è un framework si occupa non solo del mappaggio dalle classi Java alle tabelle della base di dati (e dai tipi di dato Java a quelli SQL), ma fornisce anche funzionalità di interrogazione e recupero dei dati (query), e può ridurre significativamente i tempi di sviluppo altrimenti impiegati in attività manuali di gestione dei dati in SQL e JDBC. Lo scopo di Hibernate è alleggerire lo sviluppatore dai più comuni compiti di programmazione legati alla persistenza dei dati. Hibernate è principalmente utile con modelli di dominio orientati agli oggetti in cui la logica di business sia collocata nello strato intermedio di oggetti Java (middle-tier). In ogni caso, Hibernate può aiutare senza dubbio a rimuovere o incapsulare codice SQL che sia dipendente dal particolare fornitore, e aiutare con i compiti più comuni di traduzione dei set di dati da una rappresentazione tabellare ad un grafo di oggetti.
In pratica uno o più file di configurazione stabiliscono la corrispondenza tra gli oggetti della nostra applicazione (e le relazioni che sussistono tra essi) e le tabelle del nostro db. Il motore di persistenza di Hibernate può occuparsi così di tutto il lavoro necessario per estrarre, inserire o modificare i dati che costituiscono lo stato degli oggetti del nostro dominio applicativo, generando autonomamente ed in tutta trasparenza i comandi e le interrogazioni SQL necessari. Hibernate rende persistente praticamente ogni classe Java che lo richieda, senza eccessivi vincoli. Ogni POJO (Plain Old Java Object), disegnato in fase di modellazione del dominio applicativo, per poter essere gestito da Hibernate deve semplicemente aderire alle fondamentali regole dei più pratici javabeans: metodi pubblici getXXX e setXXX per le proprietà persistenti, esistenza di un costruttore nullo.
Il mapping object relational: JavaEE
Enterprise JavaBean definisce un modello a componenti lato server che permette di sviluppare applicazioni distribuite all’interno di un robusto ambiente transazionale. Ogni componente, ossia ogni enterprise bean, si basa su di un semplice modello di programmazione che permette allo sviluppatore di focalizzare l’attenzione esclusivamente sulla business logic. Ogni bean è definito da una classe, che estende EntityBean o SessionBean a seconda dei casi, da un’interfaccia remote e da un interfaccia home remote:
- la classe del bean implementa i metodi business e quelli di callback relativi l ciclo di vita: i metodi business sono i soli visibili all’applicazione client mentre gli altri sono visibili solo al container EJB o alla stessa classe del bean, i metodi di callback informano il bean tramite notifiche sui cambiamenti in atto relativamente al suo ciclo di vita ed hanno a che fare con la gestione della persistenza, che può essere affidata al container EJB stesso. La classe del bean non implementa nessuna delle interfacce componenti del bean ma deve contenere i metodi corrispondenti a quelli definiti nelle interfacce remote e local.
- l’interfaccia remote definisce i metodi business del bean che devono essere resi accessibili ad applicazioni esterne al container EJB ed adotta le convenzioni e gli idiomi che sono usati nei protocolli ad oiggetti distribuiti; l’interfaccia remote estende infatti javax.ejb.EJBObject, che a sua volta estende java.rmi.Remote.
- l’interfaccia home remote permette l’accesso da applicazioni esterne al container EJB e definisce i metodi del ciclo di vita del bean; questi ultimi sono impiegati per la creazione di nuovi bean, per trovarli o rimuoverli. L’interfaccia home estende javax.ejb.EJBHome, che a sua volta estende java.rmi.Remote.
I session bean possono essere di due tipi, stateful e stateless: i primi mantengono un conversational state con il client che però non viene memorizzato nel database come invece avviene nella persistenza degli entity bean, ma viene mantenuto in memoria per tutta la durata della sessione con il client. Nei session bean stateless invece ogni metodo è completamente indipendente ed utilizza solo i dati passati come parametri. A partire dalle interfacce, il server EJB implementa le classi EJB object ed EJB home durante il processo di deploy nel container. Per effettuare il deploy occorre creare un jar che contenga le classi e le interfacce del bean insieme ad un descrittore di deploy; un deployment descriptor è un file xml che permette di definire gli attributi a runtime di componenti server side (sicurezza, contesto transazionale, ecc.), senza alterare la classe bean o le sue interfacce. Gli Entità Bean sono utili per modellare dati secondo una filosofia object oriented, ma non a rappresentare un processo o un task: di questo si occupano i session bean, che lavorano con i primi, con i dati ed altre risorse per gestire la logica del workflow. Una delle novità introdotte per il CMP in EJB 2.0 è la presenza di un nuovo componente all’interno del container, il persistence manager, il quale si occupa di gestire tutte le fasi di sincronizzazione degli entity, lasciando il container libero di occuparsi di tutto il resto (sicurezza, pooling, transazioni,…).
In questo articolo utilizzeremo un approccio al mapping basato su JavaBeans, che uniscono alla semplicità di utilizzo (il modello è gestito dal package java.beans ha come parti fondamentali metodi, proprietà ed eventi) una serie di caratteristiche molto utili:
- Un bean ha tutti i vantaggi del paradigma Java di riusabilità, permettendo agli sviluppatori di sfruttare al meglio i benefici dello sviluppo rapido di applicazioni, assemblando componenti predefiniti per creare funzionalità più robuste
- Le proprietà, gli eventi ed i metodi di un bean esposti ad uno strumento costruttore di applicazioni possono essere controllati utilizzando l’introspezione
- Un bean può essere progettato in modo da operare correttamente in ambienti diversi
- Le impostazioni di configurazione di un bean possono essere salvate in modo persistente e ripristinate in un secondo momento
- Un bean può essere registrato in modo da ricevere eventi da altri oggetti e da generare eventi che vengono inviati ad altri oggetti.
I Bean API provvedono ad una standardizzazione del formato delle classi Java. Grazie ai Java Beans quindi i programmi possono reperire automaticamente informazioni riguardo le classi che seguono il formato API e di conseguenza creare e manipolare classi senza che lo user debba scrivere un codice esplicito. Si ottiene perciò permettendo quindi un ottimo incapsulamento del codice, peraltro riutilizzabile. Al programmatore quindi sarà pressochè invisibile la sezione di codice puro, sostituito da richiami ai metodi delle classi incluse. I 3 punti fondamentali dei Beans sono i seguenti:
1. Una classe bean deve avere un costruttore ad argomento nullo (vuoto): ciò è ottenibile definendo esplicitamente un costruttore o omettendoli tutti, per cui viene automaticamente creato un costruttore vuoto;
2. Una classe bean non deve avere variabili pubbliche;
3. Gli accessi ai valori persistenti devono avvenire tramite metodi denominati getXxx e setXxx.
Possiamo quindi definire delle proprietà di un bean associate ad una lista di attributi, a cui accedere tramite dei metodi di get e set: se ogni attributo rappresenta il valore di un campo della tabella per il record associato al bean, una chiamata al metodo get corrisponderà a recuperare il valore dal db attraverso una query di SELECT, mentre eseguire il corrispondente set determinerà un’interrogazione di tipo UPDATE o INSERT, a seconda che il bean corrisponda ad un record già presente sul db o ad un nuovo record.
L’approccio buttom-up: costruiamo il Bean
Senza cercare di reinventare strumenti potenti e ampiamente utilizzati e consolidati, come quelli a cui abbiamo appena accennato, può tuttavia risultare interessante cercare di capire le esigenze di un efficiente sistema di mapping Object-relational partendo dal basso. Supponiamo quindi che le nostre necessità di transazione sul database siano piuttosto semplici (leggere i dati da singole tabelle, fare operazioni di insert,update e delete di singoli record) e non richiedono una gestione complessa della concorrenza degli accessi ai dati o dei meccanismi di cascade sui dati referenziati dalle foreign key; in questo caso vedremo come sia possibile analizzare, nel dettaglio del codice, il problema di associare a record e tabelle alcuni oggetti java che gestiscano le nostre necessità di transazione sul db relazionale e persistenza delle informazioni. Le due classi che andremo a costruire faranno da “motore” per dei bean che le stenderanno e impiegheranno la reflection per recuperare buona parte delle informazioni necessarie a generare dinamicamente i comandi SQL.
Java Reflection è uno dei frameworks principali di Java: con Java Reflection è possibile trattare le classi come dei metaoggetti per eseguire operazioni sofisticate, come ad esempio scoprire e invocare dinamicamente i metodi di una classe per realizzare modelli a componenti quali Java Beans. Il concetto di metaoggetto viene esteso non solo alle classi ma anche ai riferimenti di oggetto.
Ecco quindi come potrebbe apparire il codice dell’interfaccia per la prima classe, da utilizzare come punto di partenza per analizzare lo scopo ed il contenuto dei vari metodi significativi:
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface DBBeanInterface
{
public int select_bean();
public int update_bean();
public int delete_bean();
public int insert_bean();
public void useConnection(Connection conn);
public boolean test_coupling();
public void chiamaSet(String metodo, Class[] cl,Object val);
public boolean addSequence(String seq_field,String seq_name);
public int findNextFromMax(String seq_field);
public Object format_date(Object val);
public void put_date_format(String format_date);
public boolean exist_field_method(String field);
public boolean findBeanByKeys(Object[] keys_val)
throws NoSuchMethodException,IllegalAccessException,InvocationTargetException;
public int populate_bean(ResultSet rs) throws SQLException;
public boolean put_keys(String[] st_keys);
}
La classe che implementerà questa interfaccia sarà DBBean.
Supponiamo inoltre di avere una tabella sul nostro database preferito fatta come quella di figura 1.
Figura 1 – La tabella GESTIONE_UTENTI del database.
Abbiamo utilizzato i tipi di dati più comuni: numeri interi, stringhe, date e numeri in virgola mobile.
Il Java Bean che mappa i record di questa tabella avrà il seguente codice:
public class BeanGESTIONE_UTENTI extends DBBean
{
java.lang.Integer id_utente;
java.lang.String nome_utente;
java.lang.String cognome_utente;
java.lang.String codice_fiscale_utente;
java.lang.Integer telefono_utente;
java.util.Date data_inserimento_utente;
java.lang.Float quota_associativa_utente;
java.lang.Integer utente_attivo;
public void setid_utente(java.lang.Integer id_utente)
{
this.id_utente = id_utente;
}
public java.lang.Integer getid_utente ()
{
return id_utente;
}
public void setnome_utente (java.lang.String nome_utente)
{
this.nome_utente = nome_utente;
}
public java.lang.String getnome_utente ()
{
return nome_utente;
}
public void setcognome_utente (java.lang.String cognome_utente)
{
this.cognome_utente = cognome_utente;
}
public java.lang.String getcognome_utente ()
{
return cognome_utente;
}
public void setcodice_fiscale_utente (java.lang.String codice_fiscale_utente)
{
this.codice_fiscale_utente = codice_fiscale_utente;
}
public java.lang.String getcodice_fiscale_utente ()
{
return codice_fiscale_utente;
}
public void settelefono_utente (java.lang.Integer telefono_utente)
{
this.telefono_utente = telefono_utente;
}
public java.lang.Integer gettelefono_utente ()
{
return telefono_utente;
}
public void setdata_inserimento_utente (java.util.Date data_inserimento_utente)
{
this.data_inserimento_utente = data_inserimento_utente;
}
public java.util.Date getdata_inserimento_utente ()
{
return data_inserimento_utente;
}
public void setquota_associativa_utente (java.lang.Float quota_associativa_utente)
{
this.quota_associativa_utente = quota_associativa_utente;
}
public java.lang.Float getquota_associativa_utente ()
{
return quota_associativa_utente;
}
public void setutente_attivo (java.lang.Integer utente_attivo)
{
this.utente_attivo = utente_attivo;
}
public java.lang.Integer getutente_attivo ()
{
return utente_attivo;
}
}
Si noti che il bean contiene solo metodi di set e get; considereremo questi prefissi come riservati e li utlizzeremo nel DBBean per eseguire i controlli di coerenza interni e la creazione delle query SQL utilizzando proprio queste prefissi. Si consideri che le convenzioni scelte sono assolutamente arbitrarie e motivate dal solo interesse a semplificare il più possibile il codice, visto il nostro obiettivo di analisi dei meccanismi base per gestire il mapping su db.
Mappare l’esito di una SELECT nel Bean
Vediamo come usare il bean appena creato per le operazioni più comuni sul database; cominciamo con una select del tipo “SELECT * FROM GESTIONE_UTENTI WHERE ID_UTENTE = 3”. Ecco il codice da inserire nella classe di test per utilizzare il bean:
BeanGESTIONE_UTENTI bean = new BeanGESTIONE_UTENTI ();
Connection conn = getConnection();
try
{
conn.getConnection().setAutoCommit(true);
bean.useConnection(conn);
String[] key_fields = { "ID_UTENTE" };
//passo al bean la lista dei campi che formano la chiave univoca
boolean esito = bean.put_keys(key_fields);
if(esito)
{
bean.setid_utente(new Integer(3));
int rec = bean.select_bean();
if(int >0)
System.out.println("Ho caricato il bean con con chiave: "
+ bean.getid_utente());
}
conn.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
conn.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
Il metodo getConnection() è quello che la classe di test utilizzerà per recuperare una connessione jdbc dal database, quindi non serve dettagliarlo. Il metodo useConnection passa al bean la connessione, mentre il metodo put_keys passa al bean un array di stringhe che contiene i campi chiave per le where conditions. Tale metodo controlla se nel bean esistono le coppie di metodi pubblici getXXX e setXXX per i campi dichiarati come chiave e in caso contrario restituisce false. A questo punto viene settato il valore per il campo chiave, mentre il metodo select_bean si occupa di generare la query per estrarre i dati e di popolare il bean con i valori ottenuti.
È a quest’ultimo metodo che dedicheremo un po’ di attenzione. Viene riportato di seguito il codice del metodo:
public int select_bean()
{
int rec = 0;
if(test_coupling())
{
String nome_tab
= this.getClass().getName().substring(this.getClass().getName().lastIndexOf(".")
+ 5,this.getClass().getName().length());
Method[] mtd = this.getClass().getDeclaredMethods();
//trovo il nome dei campi
String n_mtd = null;
StringBuffer query = new StringBuffer();
String st_query = null;
query.append("SELECT ");
for(int k=0; k{
n_mtd = mtd[k].getName();
if(n_mtd.indexOf("get") >-1)
{
query.append(n_mtd.substring(3,n_mtd.length())+",");
}
}
query.deleteCharAt(query.lastIndexOf(","));
if(keys != null && keys.size()>0)
{
query.append(" FROM "+nome_tab+" WHERE ");
for(int i=0; i{
query.append( keys.elementAt(i).toString().toUpperCase()
+ " = "+findValue("get"+keys.elementAt(i).toString())+" AND " );
}
st_query = query.toString().substring(0,query.toString().lastIndexOf("AND"));
}else
{
System.out.println("Non sono state valorizzate le primary keys della tabella");
}
System.out.println(st_query);
Statement st = null;
ResultSet rs = null;
if(conn != null)
{
try{
st = conn.createStatement();
rs = st.executeQuery(st_query);
select_map = new HashMap();
while(rs.next())
{
rec++;
for(int k=0; k{
n_mtd = mtd[k].getName();
if(n_mtd.indexOf("set") >-1)
{
String mtd_field = n_mtd.substring(3,n_mtd.length());
Class[] par = mtd[k].getParameterTypes();
if(par[0].getName().indexOf("String") >-1)
{
String val = rs.getString(mtd_field);
chiamaSet(n_mtd,par,val);
select_map.put(mtd_field,val);
}
else if(par[0].getName().indexOf("BigDecimal") >-1)
{
BigDecimal val = rs.getBigDecimal(mtd_field);
chiamaSet(n_mtd,par,val);
select_map.put(mtd_field,val);
}
else if(par[0].getName().indexOf("Double") >-1)
{
double val = rs.getDouble(mtd_field);
chiamaSet(n_mtd,par,new Double(val));
select_map.put(mtd_field,new Double(val));
}
else if(par[0].getName().indexOf("Float") >-1)
{
float val = rs.getFloat(mtd_field);
chiamaSet(n_mtd,par,new Float(val));
select_map.put(mtd_field,new Float(val));
}
else if(par[0].getName().indexOf("Integer") >-1)
{
int val = rs.getInt(mtd_field);
chiamaSet(n_mtd,par,new Integer(val));
select_map.put(mtd_field,new Integer(val));
}
else if(par[0].getName().indexOf("Long") >-1)
{
long val = rs.getLong(mtd_field);
chiamaSet(n_mtd,par,new Long(val));
select_map.put(mtd_field,new Long(val));
}
else if(par[0].getName().indexOf("Date") >-1)
{
Date val = rs.getDate(mtd_field);
chiamaSet(n_mtd,par,val);
select_map.put(mtd_field,val);
}
else if(par[0].getName().indexOf("Blob") >-1)
{
Blob val = rs.getBlob(mtd_field);
chiamaSet(n_mtd,par,val);
select_map.put(mtd_field,val);
}
else if(par[0].getName().indexOf("Clob") >-1)
{
Clob val = rs.getClob(mtd_field);
chiamaSet(n_mtd,par,val);
select_map.put(mtd_field,val);
}
}
}
}
rs.close();
st.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
if(rs != null)
rs.close();
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
finally
{
try{
if(rs != null)
rs.close();
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
}else
System.out.println("Non è stata settata la connessione al bean");
}
return rec;
}
Il codice in se‘ è piuttosto semplice; possiamo vedere come, utilizzando le reflection e basandosi sulle regole per il nome di classe e metodi sia possibile ricavare il nome della tabella e dei campi che si desidera mappare sul bean. I campi per la where condition vengono presi da un attributo di tipo Vector caricato dal metodo put_keys(String[]), mentre i rispettivi valori vengono recuperati dal metodo findValue(String), che riceve in input il nome del campo, attraverso la reflection invoca il corrispondente get e si occupa della formattazione minima necessaria a produrre un istruzione SQL corretta (le stringhe devono iniziare e terminare con un’apice, i campi di tipo data devono essere inseriti con un’opportuna formattazione, che viene passata al bean attraverso il metodo put_date_format(String)).
private Object findValue(String metodo)
{
Object val = null;
Method m = null;
try{
m = this.getClass().getDeclaredMethod(metodo.toLowerCase(), new Class[]{});
} catch (NoSuchMethodException e){ System.out.println(e); }
if(m != null)
{
if(!m.isAccessible())
m.setAccessible(true);
try
{
val = m.invoke(this,new Object[]{});
//controllo dei dati nulli
if(val != null)
{
String tipo = val.getClass().getName();
System.out.println(metodo+"---------->"+tipo);
if(tipo.indexOf("String") >-1)
val = "'"+val+"'";
else if(tipo.indexOf("Date") >-1)
{
val = format_date(val);
}
}else
val = "null";
}catch(IllegalAccessException e)
{
System.out.println(e);
}
catch(InvocationTargetException e)
{
System.out.println(e);
}
}
return val;
}
Dopo aver popolato il ResultSet viene fatto un ciclo sull’array dei metodi ottenuto con
Method[] mtd = this.getClass().getDeclaredMethods();
in corrispondenza ad ogni ciclo si controlla se il metodo ha un prefisso set; in caso positivo si ricavano il nome del campo e la classe del parametro in input e a seconda di questa viene utilizzato un metodo diverso per recuperare il valore dal resultset e passarlo all’attributo corrispondente attraverso chiamaSet(String, Class[], Object), che incapsula l’invoke del metodo il cui nome è contenuto nella stringa e il cui valore è nell’Object.
public void chiamaSet(String metodo, Class[] cl,Object val)
{
Method m = null;
try
{
m = this.getClass().getDeclaredMethod(metodo.toLowerCase(),cl);
} catch (NoSuchMethodException e){ System.out.println(e); }
if(m != null)
{
if(!m.isAccessible())
m.setAccessible(true);
try
{
m.invoke(this,new Object[]{val});
}catch(Exception e)
{
System.out.println(e);
}
}
}
Infine le coppie nome campo / valore contenuto vengono conservati in un’HashMap per i successivi scopi (ad esempio, controllare quali valori del bean siano stati effettivamente modificati prima di lanciare un update). Si noti che i tipi supportati dal metodo populate sono quelli più comuni:
- String
- BigDecimal
- Double
- Float
- Integer
- Long
- Date
- Blob
- Clob
Rendere persistente il Bean sul database
Vediamo adesso come effettuare una query di insert:
BeanGESTIONE_UTENTI bean = new BeanGESTIONE_UTENTI ();
Connection conn = getConnection();
try
{
conn.getConnection().setAutoCommit(true);
bean.useConnection(conn);
String[] key_fields = { "ID_UTENTE" };
//passo al bean la lista dei campi che formano la chiave univoca
boolean esito = bean.put_keys(key_fields);
if(esito)
{
bean.setid_utente(new Integer(3));
bean.setnome_utente("Mario");
bean.setcognome_utente("Terzilli");
bean.setcodice_fiscale_utente("TRZMRA72L09E796H")
bean.settelefono_utente(new Integer(789654679));
GregorianCalendar dt = new GregorianCalendar(2006,4,18);
bean.setdata_inserimento_utente(dt.getTime());
bean.setquota_associativa_utente(new Float(125.78));
bean.setutente_attivo(new Integer(1));
int rec = bean.insert_bean();
if(int >0)
System.out.println("Ho salvato il bean con con chiave: "
+ bean.getid_utente());
}
conn.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
conn.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
L’unica differenza dal caso precedente, a parte il fatto che sono stati assegnati i valori a tutte le proprietà del bean, consiste nell’utilizzo del metodo insert_bean(), che viene riportato di seguito.
public int insert_bean()
{
int ret = 0;
if(test_coupling())
{
String nome_tab
= this.getClass().getName().substring(this.getClass().getName().lastIndexOf(".")
+5,this.getClass().getName().length());
Method[] mtd = this.getClass().getDeclaredMethods();
//trovo il nome dei campi
String n_mtd = null;
Vector values = new Vector();
StringBuffer query = new StringBuffer();
query.append("INSERT INTO "+nome_tab+" ( ");
for(int k=0; k{
n_mtd = mtd[k].getName();
if(n_mtd.indexOf("get") >-1)
{
values.addElement(findValue(n_mtd));
query.append(n_mtd.substring(3,n_mtd.length())+", ");
}
}
query.deleteCharAt(query.toString().lastIndexOf(","));
query.append(") VALUES(");
for(int k=0; k{
query.append(values.elementAt(k)+",");
}
query.deleteCharAt(query.toString().lastIndexOf(","));
query.append(") ");
System.out.println(query);
Statement st = null;
if(conn != null)
{
try
{
st = conn.createStatement();
ret = st.executeUpdate(query.toString());
st.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
finally
{
try{
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
}else
System.out.println("Non è stata settata la connessione al bean");
}
return ret;
}
Il metodo utilizza la reflection per creare una query di insert del tipo “INSERT INTO NOME TABELLA (NOME_CAMPO) VALUES(VALORE_CAMPO)”; i valori da inserire nel database sono recuperati utilizzando il metodo findValue(String), che riceve in imput il nome del metodo ed esegue l’invoke dello stesso come mostrato di seguito. Anche qui viene fatta la formattazione minima necessaria a produrre un istruzione SQL corretta.
Modificare i dati associati al Bean
Veniamo ora alle due ultime operazioni relative alla modifica e alla cancellazione di un record sulla tabella. Supponiamo di aver già salvato sul db lo stato del nostro bean come nell’esempio precedente. Occupiamoci ora di recuperarlo e modificarlo.
BeanGESTIONE_UTENTI bean = new BeanGESTIONE_UTENTI ();
Connection conn = getConnection();
try
{
conn.getConnection().setAutoCommit(true);
bean.useConnection(conn);
String[] key_fields = { "ID_UTENTE" };
//passo al bean la lista dei campi che formano la chiave univoca
boolean esito = bean.put_keys(key_fields);
if(esito)
{
bean.setid_utente(new Integer(3));
int rec = bean.select_bean();
if(int ==1)
{
System.out.println("Ho caricato il bean con con chiave: "
+ bean.getid_utente());
GregorianCalendar dt = new GregorianCalendar(2006,6,20);
bean.setdata_inserimento_utente(dt.getTime());
bean.setquota_associativa_utente(new Float(218.20));
bean.update_bean();
}
}
conn.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
conn.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
Abbiamo modificato la data di inserimento utente e la quota associativa ed abbiamo provveduto a invocare il metodo update_bean(); vediamo come si comporta questo metodo analizzandone il codice:
public int update_bean()
{
int ret = 0;
if(test_coupling())
{
String nome_tab
= this.getClass().getName().substring(this.getClass().getName().lastIndexOf(".")
+ 5,this.getClass().getName().length());
Method[] mtd = this.getClass().getDeclaredMethods();
//trovo il nome dei campi
String n_mtd = null;
Vector fields = new Vector();
StringBuffer query = new StringBuffer();
String st_query = null;
query.append("UPDATE "+nome_tab+" SET ");
for(int k=0; k{
n_mtd = mtd[k].getName();
if(n_mtd.indexOf("get") >-1)
{
//controllo che il campo sia stato modificato
if( control_update(n_mtd.substring(3,n_mtd.length())) )
{
fields.addElement(n_mtd.substring(3,n_mtd.length()));
query.append(n_mtd.substring(3,n_mtd.length())
+" = "+findValue(n_mtd)+", ");
}
}
}
query.deleteCharAt(query.toString().lastIndexOf(","));
//metto le where cond basate sulle keys
if(keys != null && keys.size()>0)
{
query.append(" WHERE ");
for(int i=0; i{
Object value
= format_field(keys.elementAt(i).toString(),
select_map.get(keys.elementAt(i).toString().toLowerCase()));
query.append(keys.elementAt(i).toString().toUpperCase()
+" = "+value+" AND " );
}
st_query
= query.toString().substring(0,query.toString().lastIndexOf("AND"));
}else
{
System.out.println("Non sono state valorizzate le primary keys della tabella");
return -1;
}
System.out.println(st_query);
Statement st = null;
if(conn != null)
{
try
{
st = conn.createStatement();
ret = st.executeUpdate(st_query);
st.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
finally
{
try{
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
}else
System.out.println("Non è stata settata la connessione al bean");
}
return ret;
}
Come preannunciato, nel creare la query di update viene eseguito un controllo sulle proprietà modificate attraverso il metodo control_update(String), che restituisce true solo se il valore dell’attributo differisce da quello nell’HashMap. Prima di inserire il valore modificato nella sequenza di SET viene chiamato un metodo di formattazione (format_field(String field,Object value), di seguito riportato integralmente), per le motivazioni più volte enunciate. Nel generare la where condition, viene fatto un controllo che il vettore dei campi chiave non sia vuoto. Si noti che viene lanciato l’update solo se il numero di record secuperati dalla select è uguale a 1: infatti se la tabella dispone di una unique key multipla mentre noi indichiamo solo una parte dei campi necessari tra quelli chiave, rischiamo di caricare più record, di cui verrà memorizzato nel bean solo l’ultimo. Questo in teoria dovrebbe preservarci dal rischio di un grave inquinamento dei dati.
private Object format_field(String field,Object value)
{
String metodo = "get"+field;
Object val = null;
Method m = null;
try{
m = this.getClass().getDeclaredMethod(metodo.toLowerCase(),new Class[]{});
} catch (NoSuchMethodException e){ System.out.println(e); }
if(m != null)
{
if(!m.isAccessible())
m.setAccessible(true);
try
{
val = m.invoke(this,new Object[]{});
if(val != null)
{
String tipo = val.getClass().getName();
System.out.println(metodo+"-------->"+tipo);
if(tipo.indexOf("String") >-1)
value = "'"+value+"'";
else if(tipo.indexOf("Date") >-1)
{
value = format_date(value);
}
}else
value = "null";
}catch(IllegalAccessException e)
{
System.out.println(e);
}
catch(InvocationTargetException e)
{
System.out.println(e);
}
}
return value;
}
Cancellare il record del Bean
Vediamo adesso come effettuare il delete:
BeanGESTIONE_UTENTI bean = new BeanGESTIONE_UTENTI ();
Connection conn = getConnection();
try
{
conn.getConnection().setAutoCommit(true);
bean.useConnection(conn);
String[] key_fields = { "ID_UTENTE" };
//passo al bean la lista dei campi che formano la chiave univoca
boolean esito = bean.put_keys(key_fields);
if(esito)
{
bean.setid_utente(new Integer(3));
int rec = bean.select_bean();
if(int ==0)
{
System.out.println("Ho caricato il bean con con chiave: "
+ bean.getid_utente());
bean.delete_bean();
}
}
conn.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
conn.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
Veniamo al metodo chiave delete_bean() che, dopo l’analisi già fatta, non presenta nulla di nuovo.
public int delete_bean()
{
int ret =0;
if(test_coupling())
{
StringBuffer query = new StringBuffer();
String nome_tab
= this.getClass().getName().substring(this.getClass().getName().lastIndexOf(".")
+ 5, this.getClass().getName().length());
query.append("DELETE FROM "+nome_tab);
String st_query = null;
if(keys != null && keys.size()>0)
{
query.append(" WHERE ");
for(int i=0; i{
Object value
= format_field(keys.elementAt(i).toString(),
select_map.get(keys.elementAt(i).toString().toLowerCase()));
query.append(keys.elementAt(i).toString().toUpperCase()
+ " = "+value+" AND " );
}
st_query
= query.toString().substring(0,query.toString().lastIndexOf("AND"));
}else
{
System.out.println("Non sono state valorizzate le primary keys della tabella");
return -1;
}
System.out.println(st_query);
Statement st = null;
if(conn != null)
{
try
{
st = conn.createStatement();
ret = st.executeUpdate(st_query);
st.close();
}catch(SQLException e)
{
e.printStackTrace();
try{
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
finally
{
try{
if(st != null)
st.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
}else
System.out.println("Non è stata settata la connessione al bean");
}
return ret;
}
Per concludere l’analisi della classe DBBean occupiamoci di scrivere due metodi sussidiari, che possono essere utilizzati per valorizzare la proprietà che corrisponde alla primary key. Molti database consigliano l’utilizzo per le chiavi univoche numeriche di una sequence; la sequence e’ in generale un oggetto che genera numeri progressivi usato per creare chiavi primarie. È necessario creare una sequence corrispondente alla tavola in questione e incrementarla del valore desiderato, di solito 1. In ORCLE ad esempio, la sequence e’ referenziata negli statements SQL con le pseudo-colonne NEXTVAL e CURRVAL. NEXTVAL genera un nuovo numero quando necessario. CURRVAL referenzia il numero corrente e puo’ essere usato solo se NEXTVAL e’ stato precedentemente usato nella sessione corrente. Il metodo addSequence(String,String) prende in input il nome della proprietà a cui associare il valore recuperato dal sequence e il nome a cui è associata la sequence sul db. Per i database come MySQL, che non dispone di sequence, è possibile utilizzare un metodo che lancia una query sulla tabella per recuperare il valore più grande corrispondente alla chiave (il MAX del campo), findNextFromMax(String), che prende in input il nome del campo che fa da primary key e restituisce un intero, che va poi castomizzato per il set della proprietà corrispondente.
Conclusioni
In questa prima parte abbiamo visto come, se le nostre esigenze per la transazione sul database non richiedono gestioni sofisticate della concorrenza degli accessi ai dati o dei meccanismi di cascade sui dati referenziati dalle foreign key, sia possibile analizzare i meccanismi per rendere persistenti su un database relazionale le informazioni raccolte scrivendo delle classi che, attraverso l’utilizzo dei JavaBean e della reflection, gestiscono il mapping di oggetti sulle tabelle. Come nota di chiusura vorrei far rilevare che si è utilizzata la reflection per ottenere i nomi dei metodi e procedere ad invocarli, ma dato che si tratta di beans, avremmo potuto impiegare ugualmente bene l’introspection, utilizzando la classe java.beans.Introspector per recuperare, via java.beans.BeanInfo, le stesse informazioni.
Vale la pena di ribadire che lo scopo di questa analisi non può essere quello di confrontarsi con i moderni framework per gestire l’Object Relational Mapping, ma piuttosto di “spacchettare” i problemi più semplici risolti da questi ultimi, utilizzando un approccio buttom-up.
Sempre impiegando questa filosofia, nella seconda parte vedremo come costruire una classe che funga da container per i nostri bean: dove il bean mappa un singolo record, il container può corrispondere ad un’intera tabella o ad un suo opportuno sottoinsieme, permettendo di eseguire più transazioni su un’unica connessione jdbc.
Per chi intenda approfondire l’argomento di Hibernate suggerisco, oltre agli ottimi articoli pubblicati da MokaByte anche la visione del tutorial in italiano disponibile sul sito http://www.hibernate.org/hib_docs/reference/it/html/
Riferimenti
[1]
Richard Monson Haefel, “Enterprise Java Beans”, O’Reilly, 2002
Luca Svetina è laureato in Fisica. Si occupa da diversi anni di consulenza come Analista Programmatore Senior nel campo della progettazione e sviluppo di applicazioni web basate su tecnologia J2EE.