MokaByte 93 - Febbraio 2005
MokaCMS - Open source per il Web Content Management
IV parte: lo strato di persistenza entity beans
di
Giovanni Puliti
Nei mesi scorsi sono stati introdotti i concetti generali della architettura ed è stata presentata la parte relativa alla scrittura degli articoli tramite applet. In questo articolo si affronterà la parte relativa alla gestione dei dati, la persistenza ed il ma-ping fra struttura relazionale e entity beans. Nel prossimo articolo verrà affrontata la parte relativa allo strato di busines logic remota dei session beans.

Introduzione
Gli argomenti in questi articoli non hanno la pretesa di offrire una dettagliata analisi della tecnologia EJB, ma piuttosto offrire alcune indicazioni base per la progettazione di una applicazione multistrato basata su EJB e web application.
E' quindi necessaria una buona conoscenza di Enterprise Java Beans così come della parte relativa allo sviluppo di applicazioni web.

 

Lo strato EJB
In questo layer troviamo due tipologie di componenti: entity beans che mappano la struttura dati della applicazione (articoli ed autori) e session bean stateless.
Per gli entity bean è stato scelto di utilizzare il framework CMP 2.0, il quale ha mostrato ottima capacità di adattarsi al caso in esame, semplificando enormemente il lavoro di mapping delle strutture dati (in particolar modo i campi blob del database sui quali verranno inserite le immagini e gli allegati dei vari articoli).
I session beans sono in questo caso tutti di tipo stateless e funzionano come session façade nei confronti dello strato web. Come si potrà vedere nel prossimo articolo, i session non forniscono funzionalità di business logic a grana fine o gestione del conversational state (che invece è stata inserita all'interno delle action del Model dello strato web organizzato secondo il pattern MVC), ma invece contengono le macro funzionalità di accesso ai dati, trasformazioni ed aggregazioni delle varie strutture dati.
Il mapping dei dati con entity beans CMP
Gli entity beans mappano la struttura dati che definisce il dominio della applicazione. La figura 1 riporta i legami di relazione con i quali essi sono legati.


Figura 1
- Schema di relazione fra i vari entity beans

Di seguito invece è riportato lo schema che spiega le "arietà" e la navigabilità delle relazioni

ArticleBean (m) ----> (n) WriterBean
ArticleBean (1) ----> (n) AttachmentBean
ArticleBean (1) ----> (n) ImageBean
ArticleBean (n) <---> (1) SectionBean
ArticleBean (1) ----> (n) KeywordBean
SectionBean (1) ----> (n) WriterBean

Il bean ArticleBean è in relazione con tutti gli altri bean della applicazione. Con alcuni di essi il legame è vitale (ad esempio una immagine non può esistere se non associata ad un articolo), mentre con altri si ha una relazione più debole (un autore di articoli può essere definito ed inserito nel db anche se non è associato a nessun articolo).
Il significato di tale schema dovrebbe essere piuttosto intuitivo per cui non ci soffermeremo oltre.
I vari attributi che definiscono articoli, autori e quant'altro sono implementati da campi nel database con una logica piuttosto semplice.
I campi dei vari entity sono mappati sul database secondo la consueta modalità dettata dalla specifica EJB CMP 2.0. La configurazione di questa relazione di mapping avviene in parte all'interno della applicazione, in parte nella configurazione dell'application server (per quello che verà mostrato qui si farà riferimento a JBoss 3.2.x)
All'interno della applicazione per prima cosa è necessario definire una sorgente dati sulla
quale verranno mappati i vari entity beans. La porzione di XML che definisce il mapping sul database è la seguente deve essere inserita nel file di deploy specifico per l'application server utilizzato. Nel caso di JBoss tale file è il jboss-jdbc.xml:

<jbosscmp-jdbc>
<defaults>
<datasource>java:/MokaCMSDS</datasource>
<datasource-mapping>mySQL</datasource-mapping>
</defaults>


La datasource MokaCMSDS verrà poi configurata per il database MySQL mokacms: le traduzione (da nome di datasource a database vero e proprio) viene gestita dal container J2EE in modalità pooled.
JBoss viene quindi configurato per connettersi al database mokacms ed offrire un oggetto di tipo javax.sql.Datasource associato al nome JNDI MokaCMSDS.
La connessione di JBoss al database viene configurata tramite apposito file XML che deve essere copiato nella directory di deploy

JBOSS_HOME/server/<nome_partizione>/deploy

dove <nome_partizione> è il nome della partizione JBoss utilizzata.
In questo caso è stato utilizzato il file mysql-mokacms-ds.xml che ha il seguente contenuto

<datasources>
<local-tx-datasource>
<jndi-name>MokaCMSDS</jndi-name>
<connection-url>jdbc:mysql://localhost/mokacms</connection-url>
<driver-class>org.gjt.mm.mysql.Driver</driver-class>
<user-name>root</user-name>
<password>pippo</password>
</local-tx-datasource>
</datasources>

Tornando alla configurazione della applicazione si deve proseguire nello specificare per ogni entity bean il nome della tabella SQL sulla quale rendere persistenti i vari campi. Questo può essere fatto con il seguente XML

<enterprise-beans>
<entity>
<ejb-name>Article</ejb-name>
<table-name>article</table-name>

Segue a questo punto l'elenco dei vari campi del bean che devono essere resi in qualche modo persistenti. Nella applicazione in esame si sono identificare le seguenti tipologie di campi

Campi persistenti: sono i comuni campi che il framework rende persistenti sul database tramite opportuno mapping specificato dal file di deploy XML. Ad esempio il campo title del bean artiche contiene il titolo di un articolo e viene mappato sul campo TITLE del database. Per la completa definizione di un campo persistente si devono specificare due informazioni. La prima è quella che dice che un campo è effettivamente persistente cosa che può essere fatta all'interno del file di deploy standard ejb-jar.xml:

<cmp-field>
<field-name>title</field-name>
</cmp-field>

Successivamente nel file di deploy relativo alla persistenza specifico per l'application server utilizzato (jboss-jdbc.xml) si deve dire per ogni campo su quale colonna deve essere effettuato il mapping

<cmp-field>
<field-name>title</field-name>
<column-name>title</column-name>
</cmp-field>


Campi chiave: sono campi persistenti come gli altri ma che hanno la particolarità di essere utilizzabili come chiavi di ricerca del bean. In genere sono associati ai campi chiave del database anche se tutti i controlli di unicità del bean in base a tale campo sono effettuati "anche" dal container EJB. I campi chiave, in quanto tali, possono partecipare anche nella definizione di una relazione con un altro bean, pur essendo anche campi persistenti (vedi oltre).
Per dire che un campo è una chiave all'interno del file di deploy verrà inserito il seguente codice XML

<primkey-field>id</primkey-field>

Campi di relazione: un campo di relazione serve per specificare il legame che unisce due entity beans. L'unica accortezza che si deve tenere a mente è che un campo di relazione non può essere reso persistente (è compito dell'application server mantenere il legame fra i record del db sincronizzando i campi corrispondenti). Tale relazione viene definita nel nel file di deploy relativo alla persistenza specifico per l'application server utilizzato (il solito jboss-jdbc.xml). Ecco il pezzo che definisce la relazione fra un articolo ed un autore

<ejb-relation>
<ejb-relation-name>article-writer</ejb-relation-name>
<relation-table-mapping>
<table-name>writer_article</table-name>
</relation-table-mapping>
<ejb-relationship-role>
<ejb-relationship-role-name>ArticleRelationshipRole</ejb-relationship-role-name>
<key-fields>
<key-field>
<field-name>id</field-name>
<column-name>artcile_id</column-name>
</key-field>
</key-fields>
</ejb-relationship-role>
<ejb-relationship-role>
<ejb-relationship-role-name>WriterRelationshipRole</ejb-relationship-role-name>
<key-fields>
<key-field>
<field-name>id</field-name>
<column-name>wrtier_id</column-name>
</key-field>
</key-fields>
</ejb-relationship-role>
</ejb-relation>


Per capire come siano gestiti i campi di relazione all'interno del codice si può prendere ad esempio la coppia di metodi ejbCreate() ed ejbPostCreate() del bean Article indicato dalla specifica EJB infatti tutti i campi di persistenza devono essere impostati all'internprimo o del metodo mentre quelli di relazione nel postCreate():

public String ejbCreate(ArticleDTO articleDTO) throws CreateException {
logger.debug("--- Enter - Parameters: articleDTO=" + articleDTO);
this.setId(articleDTO.getId());
this.setFields(articleDTO);
logger.debug("--- Exit");
return "";
}

public void ejbPostCreate(ArticleDTO articleDTO) throws CreateException {
logger.debug("--- Enter - Parameters: articleDTO=" + articleDTO);
this.setRelations(articleDTO);
logger.debug("--- Exit");
}

Come si può notare l'uso di un DTO di tipo ArticleDTO rende il codice molto pulito e semplice da leggere.
Il metodo setFields() preleva tutti i campi del DTO per andare ad instanziare i relativi campi del bean.

public void setFields(ArticleDTO articleDTO){
logger.debug("--- Enter - Parameters: articleDTO=" + articleDTO);
this.setAbstractText(articleDTO.getAbstractText());
this.setBody(articleDTO.getBody());
this.setIntroducion(articleDTO.getIntroducion());
this.setName(articleDTO.getName());
this.setNotes(articleDTO.getNotes());
this.setSubTitle(articleDTO.getSubTitle());
this.setTitle(articleDTO.getTitle());
logger.debug("--- Exit");
}


Il metodo setRelations() è invece più complesso. Se infatti le relazioni sono mantenute nel database come associazioni fra colonne chiave, in entity bean le realzioni sono mantenute associando le relative interfacce local dei vari bean.
Partendo quindi dall'id dell'oggetto in relazione con l'articolo deve ricercarne l'interfaccia local ed assegnarla nel campo di relazione di ArticleBean

public void setRelations(ArticleDTO articleDTO){
logger.debug("--- Enter - Parameters: articleDTO=" + articleDTO);
….

String sectionId = articleDTO.getSectionId();
logger.debug("Si procede alla assegnazione della sezione " + sectionId);
this.assignSectionById(sectionId);
….
logger.debug("--- Exit");
}

ed infine il metodo di assegnazione di una sezione

public void assignSerialById(String serialId){
logger.debug("--- Enter - Parameters: serialId=" + serialId);
if (serialId == null){
logger.debug("la serie non è stata impostata nel DTO.
              Si assegna relazione nulla");
this.setSerial(null);
}
else{
SerialLocal serialLocal;
SerialLocalHome serialLocalHome;
String SerialHomeName = "java:comp/env/ejb/serial";

try {
serialLocalHome = (SerialLocalHome) ServiceLocator.getEjbLocalHome(SerialHomeName);
} catch (ServiceLocatorException sle) {
String msg ="Impossibile procedere perché la home di Serial non è stata
             trovata\n" + sle;
logger.error(msg);
throw new EJBException(msg);
}

try {
logger.debug("si assegna la serie");
serialLocal = serialLocalHome.findByPrimaryKey(serialId);
logger.debug("trovata la serie " + serialLocal.getDescription());
this.setSerial(serialLocal);
} catch (ObjectNotFoundException onfe) {
logger.debug("Non esiste una serie con id=" + serialId +", si lascia non assegnata");
} catch (FinderException fe) {
String msg = "Impossibile procedere nella assegnazione della sezione -
               Exception durante la ricerca\n" +fe;
logger.error(msg);
throw new EJBException(msg);
}
}
logger.debug("--- Exit");
}


I campi blob: i campi blob sono utilizzati in questa applicazione per rendere persistenti entità binarie come ad esempio le immagini di un articolo e gli allegati, così come la foto dell'autore. Utilizzare un framework di mapping Obejct-Relational è quanto mai utile in questo caso. Infatti non è necessario dover manipolare in alcun modo gli stream di I/O da e verso i campi BLOB del database (operazione quanto mai scomoda) essendo questo compito a totale carico dell'application server.
Nella applicazione i byte di un campo blob potranno essere acceduti in modo molto semplice tramite il metodo

public abstract byte[] getBytes();

che è astratto dato che il campo BLOB fa parte del cosiddetto Abstract Persistent Schema.
Per quanto riguarda la definizione della associazione campo dell'entity colonna del database non vi è niente di differente rispetto al caso relativo ai comuni campi di persistenza EJB.

Campi calcolati: la prima particolarità di questa applicazione rispetto alla comune pratica di sviluppo CMP 2.0 è forse legata ai campi calcolati. Fra le varie proprietà di un articolo, particolarmente importante sono le parole chiave ad esso associato; ad esempio l'articolo in questione, che tratta di EJB ma anche di CMS di CMP e di editoria web, potrebbe avere le seguenti parole:

{cmp 2.0 entity, sessions, content management system, cms, web publishing}

Questa organizzazione permette di trovare anche questo articolo se si effettua una ricerca di
tutti gli articoli che parlano di CMS o di web publishing.
E' piuttosto commune che una determinata parola chiave potrebbe essere associata a più articoli e per questo motivo si potrebbe pensare di creare una relazione mn fra ArticleBean e KeywordBean.
Questa scelta però comporterebbe la creazione di un sistema di gestione delle parole chiave, editing, gestione duplicati e molto altro ancora. Dato che il problema da gestire era piuttosto elementare si sono volute mantenere le cose semplici: per questo motivo il ArticleBean ha al suo interno un campo keywordsString (persistente) di tipo stringa che contiene la lista di parole chiave separate da virgola, ed un campo non persistente keywords che restituisce un array di stringhe. Tale campo in realtà è fittizio dato che non corrisponde a nessun campo reale ma solamente ad una coppia di metodi getKeywords() setKeyworkds() condizione necessaria per la specifica JavaBeans per la definizione di un campo.
Tali metodi al loro interno potrebbero essere così definiti:

public void setKeywords(String[] keywords) {
logger.debug("--- Enter - Input Parameters: keywords=" + keywords+",
              dimensione:"+keywords.length);
if (keywords == null || keywords.length==0){
logger.debug("le keywords non sono state impostate nel DTO. Non si modifica");
this.keywords = null;
this.setKeywordsString("");
}
else{
String str = " ";
for (int i = 0; i < keywords.length; i++){
logger.debug("keys[i]="+keywords[i]);
str += "," + keywords[i];
}
// imposta il campo di persistenza CMP
this.setKeywordsString(str.substring(1));
}
logger.debug("--- Exit");
}

public String[] getKeywords() {
logger.debug("--- Enter");
LinkedList keywords = new LinkedList();
String keywordsString = this.getKeywordsString();
logger.debug("keywordsString: "+keywordsString);
if (keywordsString != null){
StringTokenizer st = new StringTokenizer(keywordsString, ",");
while (st.hasMoreTokens()) {
keywords.add(st.nextToken());
}
logger.debug("travasate le properties su keywords " + keywords);
String[] returnArray = new String[keywords.size()];
return (String[]) keywords.toArray(returnArray);
}
else{
return new String[0];
}
}

Quindi dall'esterno tutte le volte che si vorrà impostare un set di parole chiave si invocherà il metodo setKeywords() il quale preso l'array di stringhe comporra la srtinga delle parole chiave separate da virgola e la assegnerà al campo CMP persistente keywordsString. Discorso analogo per il metodo getKeywords().

Per quanto riguarda la ricerca di un articolo data una particolare parola chiave si è implementato un metodo di ricerca che verifica se la stringa data dal campo contiene al suo interno una o più parole chiave passate come parametro di ricerca. Forse non è molto efficiente e probabilmente in futuro si cercherà una soluzione più rigorosa.
Come si può facilmente intuire non vi è niente di particolare rispetto a CMP, si tratta di una comune tecnica di programmazione nemmeno troppo sofisticata. Ogni tanto sfatare il mito di EJB tecnologia complessa non fa male.

 

Trasferimento dati tramite DTO
Per rendere quanto visto fino ad ora funzionale e funzionante, si rende necessario realizzare un qualche sistema che permetta da un lato di spostare informazioni fra i vari strati applicativi, dall'altro di realizzare un meccanismo di cache sullo strato web.
Questi due obiettivi sono ottenuti grazie alla presenza di due categorie di oggetti nella applicazione: gli oggetti di trasporto (pattern Data Transfert Object, DTO) e gli oggetti di rappresentazione delle strutture dati (pattern Value Object VO).
Senza entrare troppo nello specifico della teoria dei pattern appena citati, (ottima la trattazione che viene fatta in [EJBDesignPattern]), si può brevemente dire che ogni struttura dati remota basata su entity è stata replicata con una struttura isomorfa di DTO e VO.
Il pattern DTO ci dice che ogni volta che si desideri spostare una struttura dati da uno strato all'altro (dal layer EJB a quello web e viceversa) si dovrà creare una apposita struttura dati di trasferimento (un DTO appunto), popolarlo e spedirlo.
Il DTO è a tutti gli effetti un oggetto di trasporto e non dovrebbe permettere nessuna modifica ai dati contenuti ne tantomeno agli oggetti dipendenti. Alcuni progettisti amano non includere metodi di setXXX() su un DTO, ma passare tutte le informazioni direttamente al costruttore.
Per fare un esempio ogni volta che si renda necessario inviare informazioni relative ad un articolo, un session bean tramite opportuni metodi di business logic ricaverà gli entity bean, ne estrapolerà tutti i dati, creerà una struttura dati DTO analoga a quella degli entity e la invierà allo strato web. Qui si potranno effettuare tutte le modifiche sui VO e per effettuare le modifiche sul database si dovrà estrapolare il DTO dal VO e rispedirlo indietro.


Figura 2
- gestione e trasporto dei dati nei vari strati applicativi

In una prima approssimazione e semplificazione si potrebbe dire che per ogni struttura dati rappresentata da una aggregazione di entity beans si potrebbe pensare di crearne una analoga di DTO VO.
In realtà in alcuni casi non è detto che sia necessario mantenere questo livello di complessità.
Si pensi ad esempio al caso in cui nello strato web si debba semplicemente visualizzare l'elenco di tutti gli autori presenti nel sistema. In un caso come questo non è necessario inviare una collezione di DTO che contengano tutti i campi degli autori, così come probabilmente non è necessario inviare i DTO relativi ai bean con i quali WriterBean instaura una relazione. In questo caso è necessario per lo strato web semplicemente ricavare una lista di nomi e cognomi.
Si potrebbe quindi pensare di creare un LightWriterDTO che contenga al suo interno solo alcune informazioni elementari dell'autore e nessun legame con i DTO dipendenti. Certamente questo DTO leggero, pensato per operazioni di elencazione, non dovrebbe contenere i bytes relativi alla immagine dell'autore.
Quali DTO creare e come aggregarli è probabilmente la decisione più diffiicle da prendere: in genere una buona analisi sugli use case da implementare è probabilmente lo strumento più utile.

Il DTO per gli articoli
Per avere un'idea della strata da seguire nella progettazione di questo genere di componenti, si farà una breve analisi dei vari DTO presenti nella applicazione.
La classe ArticleDTO rappresenta un oggetto di trasporto relativo ad un articolo.

public class ArticleDTO implements Serializable {

private String name;
private String abstractText;
private String notes;
private String title;
private String subTitle;
private String introducion;
private String body;
private String id;
private SectionDTO section;
private SerialDTO serial;
private String sectionId;
private String serialId;

Un DTO deve essere serializzabile in quanto deve essere strasferito come parametro di input/output di metodi RMI. I campi name,title, id etc.. servono per memorizzare le varie proprietà dell'oggetto. Esse saranno popolate a partire dai campi dell'entity bean. Successivamente nella definizione della struttura della classe si possono trovare i campi di relazione, implementati da array di DTO dipendenti

private String[] keywords;
private ImageDTO[] images;
private AttachmentDTO[] attachments;
private WriterDTO[] writers;

La prima cosa da notare è la scelta di utilizzare array di stringhe per memorizzare la collezione di parole chiave associate all'articolo: dato che le parole chiave sono in definitiva "parole" utilizzare un DTO apposito sarebbe stata una scelta probabilmente sovradimensionata.
Il fatto che siano state utilizzati array e non collezioni dinamiche (Collection, List o altro) è frutto di una importante scelta: in questo modo infatti si può mantenere un più forte tipechecking sugli oggetti dipendenti. Poter disporre del metodo

public WriterDTO[] getWriters() {
return writers;
}

offre una impronta più forte dell' equivalente

public Collection getWriters() {
return writers;
}

dato che nel primo caso si dice al mondo esterno esattamente cosa verrà restituito nel set di autori.
Gli array pero' sono oggetti piuttosto scomodi da trattare, ed è per questo motivo che sono stati scelti come contenitori solo all'interno dei DTO ma non negli entity (la specifica EJB obbliga l'uso di Collection) e nemmeno nei Value Obects corrispondenti.
Questa precisa scelta si adatta alla perfezione a quella che è normalmente la natura dei DTO di oggetti a sola lettura, per cui non dovrebbe mai essere necessario aggiungere o rimuovere dinamicamente un WriterDTO ad un ArticleDTO.

Il DTO per gli allegati e le immagini
Allegati ed immagini sono strutture dati che non dovrebbero mai vivere indipendentemente dall'articolo a cui fanno riferimento. I DTO relativi sono semplici strutture dati serializzabili che contengono informazioni relative alla immagine o all'allegato ed un campo byte[] all'interno del quale viene fisicamente memorizzato il file ZIP o l'immagine JPG/GIF.

public class AttachmentDTO implements Serializable {
static Logger logger = Logger.getLogger(AttachmentDTO.class.getName());

private String id;
private String fileName;

/** @todo cambiare questa prop in fileExtension */
private String format;

private byte[] bytes;
private String articleId;


public AttachmentDTO(){}

public AttachmentDTO(String id, String articleId, String fileName,
                      String format, byte[] bytes){
this.id=id;
this.articleId=articleId;
this.fileName=fileName;
this.format=format;
this.bytes=bytes;
logger.debug("AttachmentDTO creato con questi valori: "+this.toString());
}


public class ImageDTO implements Serializable {

private String id;
private String articleId;
private String fileName;
private String format;
private byte[] bytes;

public ImageDTO(){}

public ImageDTO(String id, String articleId, String filename,
                String format, byte[] bytes){
this.id=id;
this.articleId=articleId;
this.fileName=filename;
this.format=format;
this.bytes=bytes;
}

Il DTO per gli autori
La classe WriterDTO rappresenta l'oggetto di trasporto di un autore di articoli. Essa può essere utilizzata in maniera autonoma oppure come oggetto dipendente all'interno di un ArticleDTO.
Non ci sono particolari dettagli di cui tener conto, se non che al solito è una classe serializzabile e contiene un array di bytes dove verranno inseriti i byte della fotografia dell'autore stesso. Si tenga presente che in questo caso specifico il DTO potrebbe essere popolato ed inviato fra uno strato e l'altro anche senza popolare tale campo: si pensi al caso unin cui si voglia semplicemente avere informazioni sull'autore senza visualizzarne la fotografia. In questo caso lasciare in bianco il campo porta ad un risparmio di tempo di trasferimento e di memoria.

public class WriterDTO implements Serializable {
private String id;
private String firstName;
private String lastName;
private String email;
private String biography;
private byte[] picture;


Gli oggetti Value Object e la conversione da DTO a VO
Per adesso si sono analizzati i vari DTO presenti nella applicazione. Il mese prossimo vedremo quali sono le soluzioni per gestire tali oggetti e trasformarli nei corrispondenti Value Object. Vedremo anche il ruol svolto in tutto ciò dai vari session beans.

 

Conclusione
Per questo mese concludiamo qui la trattazione. Come si sarà potuto notare l'articolo riporta una serie di idee e soluzioni progettuali senza entrare nello specifico della teoria di EJB. Non è infatti questa la sede dato che una trattazione completa richiederebbe molto più spazio di quello qui a disposizione. Spero che quanto qui proposto possa rappresentare un buon punto di partenza da cui approfondire l'argomento della progettazione ed implementazione di architetture basate su EJB.
Il prossimo mese parleremo dei session beans che compongono lo strato EJB.
Ricordo a tutti che presto il sistema verrà rilasciato con tutti i sorgenti nell'ambito del progetto MokaLab ([ML])

 

Bibliografia
[ML] - MokaLab il laboratorio virtuale di MokaByte - http://www.mokabyte.it/mokalab/
[MVC] - MokaPackages: il framework MVC di MokaByte
http://www.mokabyte.it/2004/06/mokapackages-1.htm
[EJBDesignPattern] "EJB Design Pattern" di Floyd Marinescu Ed. Wiley.
http://www.theserverside.com/books/wiley/EJBDesignPatterns/

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it