Un approccio bottom-up al moderno mapping object-relational. Usando i JavaBeans e la Java Reflection è possibile analizzare i meccanismi che stanno alla base del mapping di oggetti su un database relazionale. Vediamo in questo articolo come costruire una classe che funga da container per i bean.
Introduzione
Nel precedente articolo abbiamo visto come, ipotizzando 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 richiedano una gestione complessa della concorrenza degli accessi ai dati o dei meccanismi di cascade sui dati referenziati dalle foreign key, sia stato possibile realizzare un bean che utilizzi la reflection per recuperare buona parte delle informazioni necessarie a generare dinamicamente i comandi SQL, necessari a garantire la persistenza delle informazioni.
Sempre impiegando questa filosofia, in questa ultima parte vedremo come costruire una classe che funga da container per i nostri beans: 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.
Mappare l’esito di una SELECT con più record
Riprendiamo la tabella utilizzata in precedenza per costruire il Bean:
Figura 1 – La tabella sulla base della quale è stato costruito il bean.
Vediamo come sia possibile recuperare dalla tabella un gruppo di utenti con particolari caratteristiche, mappando ciascun record nel bean corrispondente:
DBBeanContainer cont = new DBBeanContainer();
cont.setTableName("GESTIONE_UTENTI");
String[] key_fields = { "ID_UTENTE" };
//passo al container la lista dei campi che formano
//la chiave univoca per il bean
boolean esito = cont.put_keys(key_fields);
Connection conn = getConnection();
//Prendo in considerazione solo gli utenti attivi
//la cui quota associative sia inferiore ad un certo importo
String where = "UTENTE_ATTIVO = 1 AND QUOTA_ASSOCIATIVA_UTENTE < 12.50 "
try
{
conn.getConnection().setAutoCommit(true);
Boolean esito = cont.put_keys(key_fields);
if(esito)
{
cont.setConnection(conn);
cont.setWhereCond(where);
cont.executeQuery();
int recs = cont.getNumBean();
if(recs >0)
{
BeanGESTIONE_UTENTI bean = (BeanGESTIONE_UTENTI)cont.get
//recupero un utente particolare in base alla chiave
//del record
Object[] keys = new Object[1];
keys[0] = new Integer(62);
int ret = cont.findBeanByKeys(keys);
if(ret != -1)
{
//modifico I dati di un singolo bean
BeanGESTIONE_UTENTI bean = (BeanGESTIONE_UTENTI)cont.getBean(ret);
bean.setquota_associativa_utente(new Float(125.78));
bean.update_bean();
}
for(int k=0; k{
BeanGESTIONE_UTENTI bean = (BeanGESTIONE_UTENTI)cont.getBean(k);
//rendo inattivi tutti gli utenti recuperati
//tranne quello modificato in precedenza
if(bean.getid_utente.intValue != 62)
{
bean. setutente_attivo(new Integer(0));
bean.update_bean();
}
}
}
cont.emptyContainer();
conn.close();
}
}catch(Exception e)
{
e.printStackTrace();
try{
conn.close();
}catch(SQLException ex){ ex.printStackTrace(); }
}
BDBeanContainer è una classe generica che può caricare, via reflection, il bean corrispondente al nome della tabella, secondo le regole di nomenclatura per i beans, definite nei precedenti articoli. Il container si aspetta di trovare il bean a livello del proprio package: in alternativa, è possibile definire due semplici metodi di get e set con cui passare il package della classe che si desidera venga riposta nel container.
Una volta caricati i beans, è possibile intervenire sul singolo bean recuperandolo attraverso la primary key, oppure modificare l’intero blocco eseguendo un ciclo sull’indice posizionale del container.
Iniziamo analizzando come potrebbe apparire il codice dell’interfaccia per il container, utilizzandolo come punto di partenza per analizzare lo scopo ed il contenuto dei vari metodi significativi
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.util.Vector;
public interface DBBeanContainerInterface
{
public void emptyContainer();
public void executeQuery();
public int findBeanByKeys(Object[] keys_val)
throws InstantiationException,ClassNotFoundException,
IllegalAccessException, InvocationTargetException,
NoSuchMethodException;
public Object getBean(int order);
public Vector getFieldList();
public int getNumBean();
public String getTableName();
public String getWhereCond();
public boolean put_keys(String[] st_keys)
throws InstantiationException,ClassNotFoundException,
IllegalAccessException, InvocationTargetException,
NoSuchMethodException;
public void setConnection(Connection conn);
public void setTableName(String table_name);
public void setWhereCond(String where_cond);
}
Il primo metodo che possiamo analizzare si occupa di verificare che i campi chiave siano definiti nel bean e di memorizzarli per passarli successivamente al bean, nel caso in cui la select produca qualche esito:
public boolean put_keys(String[] st_keys)
throws InstantiationException,ClassNotFoundException,
IllegalAccessException, InvocationTargetException,
NoSuchMethodException, IntrospectionException
{
boolean esito = true;
this.keys = new Vector();
String pack = this.getClass().getPackage().getName();
if(table_name != null)
{
Class bean = Class.forName(pack+".Bean"+table_name);
Object t_bean = bean.newInstance();
Class[] par = { new String().getClass()};
for(int k=0; k{
//controllo che esista un metodo per ogni chiave
Object ret = invoke_by_introspection(t_bean,
"exist_field_method",new Object[]{st_keys[k]});
if( ((Boolean)ret).booleanValue() )
keys.addElement(st_keys[k].toLowerCase());
else
{
esito = false;
break;
}
if(esito)
{
keys_array = st_keys;
}
}
}else
System.out.println("Non è stato impostato il nome della tabella");
return esito;
}
Il metodo put_keys utilizza un metodo private che impiega l’introspection per invocare i metodi del bean:
private Object invoke_by_introspection(Object bean, String methodName, Object[] par)
throws IntrospectionException,NoSuchMethodException,
IllegalAccessException,InvocationTargetException
{
MethodDescriptor[] m_desc = Introspector.getBeanInfo
bean.getClass()).getMethodDescriptors();
Object ret = null;
for(int s=0; s{
if(m_desc[s].getName().equals(methodName))
{
ret = m_desc[s].getMethod().invoke(bean,par);
break;
}
}
return ret;
}
Il metodo principale del container è certamente quello che si occupa di generare dinamicamente la query ed istanziare i beans, assegnando a ciascuno la connessione e valorizzando le proprietà via introspection con i dati recuperati dalla tabella:
public void executeQuery()
{
//Controllo che esista il bean con il nome tabella nel package
String pack = this.getClass().getPackage().getName();
if(table_name != null)
{
if(conn != null)
{
if(keys != null)
{
try{
Class bean = Class.forName(pack+".Bean"+ table_name);
Method[] mtd = bean.getDeclaredMethods();
String n_mtd = null;
for(int k=0; k{
n_mtd = mtd[k].getName();
if(n_mtd.indexOf("get") >-1)
{
field_list.addElement (n_mtd.substring(3,n_mtd.length()));
}
}
StringBuffer query = new StringBuffer();
query.append("SELECT ");
for(int s=0; s{
query.append(field_list. elementAt(s)+",");
}
query.deleteCharAt(query.lastIndexOf(","));
if(where_cond != null)
{
if(where_cond.toUpperCase().
indexOf("WHERE") != -1)
query.append(" FROM "+table_name+ " "+where_cond);
else
query.append(" FROM "+table_name+ " WHERE "+where_cond);
}else
query.append(" FROM "+table_name);
Statement st = null;
ResultSet rs = null;
try
{
st = conn.createStatement();
System.out.println(query);
rs = st.executeQuery(query.toString());
Object t_bean = null;
Method m = null;
while(rs.next())
{
t_bean = bean.newInstance();
Class[] par = { rs.getClass()};
invoke_by_introspection(t_bean, "populate_bean",new Object[]{rs});
Class[] par1 = { keys_array. getClass()};
invoke_by_introspection(t_bean,
"put_keys",new Object[]{keys_array});
Class[] par2 = {conn.getClass()};
invoke_by_introspection(t_bean,"useConnection",new Object[]{conn});
beans.addElement(t_bean);
bean_list++;
}
rs.close();
st.close();
}catch(Exception 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(); }
}
}catch(Exception e)
{
System.out.println("Non esiste la classe del
Bean con nome "+pack+".Bean"+table_name);
}
}else
System.out.println("Non sono state impostate le chiavi per il bean");
}else
System.out.println("Non è stata impostata la connessione al db");
}else
System.out.println("Non è stato impostato il nome della tabella");
}
Osserviamo che, questa volta attraverso la reflection (in quanto siamo interessati solo ai metodi di set e get del bean e non a quelli della superclasse), possiamo recuperare i nomi dei campi del bean che andranno mappati sulla tabella. Una serie di controlli verifica che gli elementi necessari a tutte le operazioni (connessione, nome tabella e primari keys) siano stati caricati, prima di procedere con l’SQL. Sempre utilizzando l’introspection vengono richiamati sull’istanza di ogni bean i metodi già analizzati nei precedenti articoli, a cui rimando per i dettagli di procedura.
A questo punto il più è fatto: un gruppo di metodi di servizio come findBeanByKeys(Object[]), che permette di ottenere il bean in base alla sua chiave, oppure getNumBean(), che recupera le dimensioni del Vector contenente I beans e infine getBean(int) , che recupera il bean in modo posizionale dal Vector, permettono di agire localmente o globalmente sui records recuperate dal container. Una volta terminate le operazioni su una tabella, è possibile svuotare il container con l’istruzione emptyContainer() per successivi utilizzi.
Conclusioni
In questa seconda e ultima parte abbiamo visto come sia possibile analizzare i meccanismi per rendere persistenti su un database relazionale le informazioni raccolte, scrivendo delle lassi che gestiscano il mapping di oggetti sulle tabelle, attraverso l’utilizzo dei JavaBean e delle loro proprietà di introspection, nonche‘ di una classe che svolga le mansioni di container dei beans. L’utilizzo di un container che faccia corrispondere dinamicamente i records di una tabella a un gruppo di oggetti della stessa classe, permette di eseguire operazioni multiple su aggregati specifici di dati.
Vorrei precisare in ultima istanza che questo lavoro non va inteso nei termini di un confronto con i moderni framework per gestire l’Object Relational Mapping, ma piuttosto come un approccio buttom-up a quei problemi che vari framework e tecnologie come JDO, Hibernate e CMP di EJB 2.0 risolvono brillantemente in contesti più complessi e strutturati di quelli da noi ipotizzati.
Riferimenti bibliografici
[1]
Richard Monson Haefel, “Enterprise Java Beans”, O’Reilly, 2002