Inizia una nuova parte della serie dedicata alla progettazione di applicazioni EJB: in questo e nei prossimi articoli parleremo di quali siano le indicazioni da seguire per la realizzazione dello strato di mapping OO-RDB basato su Entity beans CMP.
Sui meccanismi di mapping
In questo articolo proseguiamo l‘analisi di CMP 2.0 concentrando l‘attenzione su due aspetti molto importanti relativi al mapping object-relational. Spesso una loro cattiva interpretazione ha portato a scelte sbagliate ed errate valutazioni a tutto danno della reputazione del framework; non è infrequente trovare team di sviluppo, capi progetto o semplici programmatori EJB (o presunti tali) che si portano dietro una impressione del tutto negativa sulla base di valutazioni affrettate su alcuni aspetti di CMP come la gestione della cache, il corretto mantenimento delle relazioni, il caricamento in pre-fetch.
È certamente vero che il framework non è utilizzabile con successo in ogni scenario e caso d‘uso, ma questo è vero per ogni framework, tecnologia e simili.
I due aspetti che sono analizzati in questo articolo sono proposti con lo scopo di rendere giustizia al sistema di mapping di EJB, consapevoli del fatto che in realtà le insidie potrebbero essere anche molte altre e che è sempre bene valutare le cose nel loro complesso e indagare sulle possibili cause di un problema, ricercare tutte le possibili soluzioni prima di procedere a una valutazione definitiva.
Mai come in questo caso, la scelta del framework di mapping implica anche conoscere nel dettaglio quella che è la politica seguita dal framework, il suo approccio nel risolvere il problema e come sia raggiunto l‘obiettivo finale.
Dire ad esempio che Hibernate è meglio di CMP (valutazione molto in voga attualmente) senza specificare sulla base di quali considerazioni si preferisce uno o l‘altro, è sicuramente superficiale e limitante.
Late binding e attributi ingombranti
Uno degli aspetti più delicati quando si parla di mapping Object-Relational è legato alla politica adottata per il caricamento degli attributi degli oggetti mappati. In particolare è necessario porre la massima attenzione per la fase di caricamento dei vari attributi di ogni entità , dato che si possono rischiare gravi rallentamenti nelle performance complessive della applicazione.
Per comprendere meglio questo aspetto (e quindi poter fare le debite considerazioni su tutti gli aspetti collegati, esercizio questo lasciato al lettore), si consideri un caso molto semplice. Sempre prendendo spunto dalla applicazione MokaByte CMS (di cui si è parlato già in precedenza) si consideri l‘entità immagine associata ad una articolo, rappresentata dall‘entity bean ImageBean. Tale entità contiene al suo interno una serie di attributi che ne definiscono propriamente il contenuto. Di seguitoè riportato un pezzo della interfaccia locale di tale bean:
public interface ImageLocal extends EJBLocalObject {public String getId();public void setFileName(String fileName);public String getFileName();public void setFormat(String format);public String getFormat();public void setBytes(byte[] bytes);public byte[] getBytes();public void setTemplate(TemplateLocal template);public TemplateLocal getTemplate();}
Si noti la presenza della coppia di metodi
public void setBytes(byte[] bytes);public byte[] getBytes();
i quali rappresentano la proprietà astratta dove vengono memorizzati i byte dell‘immagine prelevati dal campo BLOB del database.
Un utilizzo improprio di questa proprietà potrebbe impattare in maniera negativa sulle prestazioni della applicazione nel suo complesso: se ad esempio si volesse fare un list di tutte le “n” immagini associate agli “m” articoli di una sezione, si avrebbero in definitiva n x m accessi al DB. Se l‘obiettivo finale è semplicemente ricavare l‘elenco delle immagini, non è di nessuna utilità caricare anche il campo bytes, cosa che invece è necessaria al momento della effettiva rappresentazione a schermo dell‘articolo con le sue immagini.
Fortunatamente la specifica EJB impone al container, se non diversamente specificato, il cosiddetto late binding, o lazy load delle proprietà di un entity.
Quindi fino a quando non si acceda alla proprietà in modo esplicito, il campo non verrà caricato dal DB, e quindi non si ha nessuna conseguenza sulle prestazioni complessive.
Questo è quanto dice la teoria, ma in realtà le cose possono essere meno semplici e lineari. Per prima cosa occorre capire cosa significhi accedere alla proprietà in modo esplicito, e individuare chiaramente il punto della applicazione dove ciò avvenga.
Si immagini una situazione semplice, ovvero una applicazione EJB che non faccia uso in maniera strutturata dei vari pattern visti fino a oggi (DTO, Assembler, Faà§ade…). Per poter ricavare una immagine per prima cosa è necessario eseguire una ricerca. Normalmente un session bean può invocare un metodo di ricerca findByXXX per ricavare l‘elemento cercato.
Per prima cosa si ricava l‘interfaccia home locale del bean
IconLocalHome iconLocalHome;final String ENTITY_NAME = "java:comp/env/ejb/icon";try {ServiceLocator locator = ServiceLocator.getInstance();iconLocalHome = (IconLocalHome) locator.getEjbLocalHome(ENTITY_NAME);} catch (ServiceLocatorException e) {throw new EJBException(e.getMessage());
Dalla quale poi si può eseguire la ricerca vera e propria
IconLocal iconLocal = IconLocalHome.findByPrimaryKey(id)
In questo momento il programma ha ricavato l‘interfaccia locale del bean, per cui dispone effettivamente del puntatore all‘oggetto, ma grazie al meccanismo del lazy load, a parte la chiave primaria, nessuna delle proprietà del bean è stata effettivamente caricata in memoria dal database.
Per questo, anche se le immagini associate fossero tutte in formato TIFF non compresso ad alta risoluzione (quindi ogni immagine potrebbe ingombrare diversi MByte), l‘esecuzione ripetuta del metodo findByPrimaryKey() non richiederebbe che pochi millisecondi.
Per ogni proprietà , solo la diretta invocazione del metodo getXXX() provoca il caricamento dei dati dal DB:
String fileName = imageLocal.getFileName();
Quindi fino a quando il session bean non esegue l‘invocazione al metodo getBytes() non vi saranno particolari ripercussioni sulle prestazioni.
Riconsideriamo ora lo scenario base che si è adottato nei precedenti articoli immaginando di dover inviare una serie di informazione legate alle immagini al lato client (esempio per visualizzare in una pagina JSP l‘elenco delle immagini associate a un articolo). Sulla base di quanto visto in precedenza si dovranno necessariamente trasferire i dati dal bean al DTO. Quindi, utilizzando il pattern DTOAssembler visto in precedenza, è all‘interno del metodo createDTO() che si deve eseguire il trasferimento:
public static ImageDTO createDto(ImageLocal imageLocal) {logger.debug("--- Enter - Parameters: imageLocal= " + imageLocal);ImageDTO imageDTO = new ImageDTO();if (imageLocal != null) {imageDTO.setId(imageLocal.getId());imageDTO.setTemplateId(imageLocal.getTemplate().getId());imageDTO.setFileName(imageLocal.getFileName());imageDTO.setFormat(imageLocal.getFormat());logger.debug("Attenzione il DTO dell‘immagine viene caricato senza i bytes");// imageDTO.setBytes(imageLocal.getBytes());}return imageDTO;}
Si noti come proprio per quanto detto fino a questo momento, il campo bytes del DTO viene lasciato a null. È questa una imposizione forte, che deve essere ben documentata all‘interno di un progetto, o divenire addirittura una delle linee guida per lo sviluppo di applicazioni.
Appare quindi evidente che accanto al metodo createDTO(), si dovrà anche mettere un metodo che permette al client di ricavare i bytes delle immagini: questo può essere inserito nel faà§ade, considerando il recupero di tale informazione come un servizio a se stante.
Si può quindi introdurre un metodo che restituisca il DTO completo, senza pero la necessità di creare un nuovo DTO da affiancare al DTO Light e DTO normale (vedi [1])
public ImageDTO getFullDTOByID(String imageId) {logger.debug("--- Enter - Parameters: imageId= " + imageId);try {ImageLocal imageLocal = imageHome.findByPrimaryKey(imageId);// per la creazione del DTO base si usa il DTOAssemblerImageDTO imageDTO = ImageDTOAssembler.createDTO(imageLocal);logger.debug("imageLocal.getId() = " + imageDTO.getId());// si aggiunge il campo bytesimageDTO.setBytes(imageLocal.getBytes());logger.debug("--- Exit");return imageDTO;} catch (FinderException e) {return null;}}
Gruppi di caricamento
Parlando di lazy load, al lettore più navigato dovrebbe essere sorto un dubbio spontaneo: se infatti tale meccanismo permette di ottimizzare le chiamate al database, e ridurre i tempi di caricamento, è anche vero che in determinate circostanze potrebbe risultare controproducente o addirittura devastante.
Il container è in grado di capire che le istruzioni viste poco sopra, contenute nel metodo createDTO() sono eseguite all‘interno dello stesso scope di esecuzione, e quindi è in grado di accorpare (o almeno ci prova) le chiamate eseguendo un caricamento cumulativo.
Non sempre questo è possibile, oppure non sempre il container è in grado di capire che se si ricava la proprietà X poco dopo sarà necessario ricavare la proprietà Y. Gli algoritmi di prefetch non possono certo fare miracoli e in alcuni casi non sono di nessun aiuto. Occorre l‘intervento di chi ha progettato l‘applicazione e ne conosce il flusso di lavoro.
Per questo è possibile “aiutare” il container definendo i cosiddetti gruppi di caricamento (load-group) all‘interno dei quali si definiscono le proprietà che presumibilmente potranno essere utili nello stesso frangente. Data l‘importanza dell‘argomento, si parlerà di questo tema in un prossimo articolo.
Inserimento ritardato fra ejbCreate e ejbPostCreate()
Come dovrebbe essere ormai noto al momento della creazione di un nuovo entity bean il container esegue una serie di operazioni di sincronizzazione atte a trasferire i dati del bean presente in memoria nelle tabelle del database.
Nota: come in precedenza prenderemo spunto dalla applicazone MokaByte CMS, in particolare concentrando l‘attenzione sui bean ArticleBean e SectionBean che instaurano una relazione bidirezionale 1:n come riportato nella figura 1:
Tale processo si attua eseguendo per prima cosa la persistenza dei dati del bean (ovvero scrittura dei valori degli attributi del bean) e successivamente collegando tutti i campi di relazione che il bean in questione instaura con altre entità collegate. Questo dualismo a livello di codice si traduce nella invocazione da parte del container dei due metodi ejbCreate() e ejbPostCreate().
Normalmente il programmatore limita il suo lavoro nel prelevare i dati che arrivano dall‘esterno (in genere contenuti in un DTO) e li assegna ai campi del bean.
Ad esempio
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, SectionNotFoundException {logger.debug("--- Enter - Parameters: articleDTO=" + articleDTO);this.setRelations(articleDTO);logger.debug("--- Exit");}
Si noti come nel primo metodo ci si preoccupi di impostare la chiave del bean (il passaggio fondamentale nella procedura di creazione) e successivamente si deleghi l‘impostazione dei vari campi al metodo setFields().
Nel metodo successivo invece si limita ad allacciare la relazione che nel caso specifico il bean Article instaura con i vari bean dipendenti (si veda a tal proposito l‘articolo [XXX] in cui si presenta il modello dati di base in questi articoli).
L‘approccio della specifica EJB di separare i due momenti di scrittura è da un certo punto di vista quantomai pulito e coerente, ma presenta un grave difetto. Infatti benchà© i due metodi siano eseguiti all‘interno della stessa transazione logica, non è garantita la transazionalità SQL: ovvero viene garantito il rispetto del paradigma ACID a livello applicativo, ma non a livello dati dove le due operazioni possono essere eseguite in due momenti differenti.
In genere lo specifico comportamento varia da produttore a produttore e non vi è certezza, a meno di controllare attentamente i file di configurazione del container e la documentazione del prodotto utilizzato.
Questo vincolo operativo è fonte di gravi limitazioni tanto che spesso viene imputato come una delle cause principali del non utilizzo di CMP in progetti reali (ovvero non in studi tecnologici o prototipi).
Per capire meglio il punto si pensi al lavoro svolto dal container in concomitanza del metodo ejbPostCreate(): l‘obiettivo è quello di allacciare le relazioni fra il bean principale e i suoi dipendenti per cui, anche se EJB nasconde i dettagli di basso livello, in definitiva questa operazione si traduce nella assegnazione di una chiave esterna sulla tabella sulla quale viene mappato il bean che si sta rendendo persistente.
La relazione che si instaura fra il bean Article è e il bean Section, viene resa persistente nelle tabelle tramite l‘associazione delle chiavi: come mostra la figura 2 la tabella ARTICLE ha un campo definito come chiave esterna (FK) che punta alla chiave primaria (PK) della tabella SECTION.
Normalmente su una chiave esterna viene imposto il vincolo di NOTNULL al fine di garantire la integrità referenziale dei dati.
Questo vincolo è incompatibile con la modalità utilizzata dal container EJB per la creazione di un entity: la creazione del bean materialmente eseguita in due fasi (per default il container esegue prima una insert e poi una update), prima l‘inserimento dei campi di persistenza nel metodo ejbCreate() e poi il salvataggio della chiave esterna nel metodo ejbPostCreate().
Al termine della esecuzione del primo metodo nella tabella principale (ARTICLE) si troverà una riga in cui il campo FK di relazione con la tabella dipendente (SECTION) non è valorizzato. Questo genera una eccezione che blocca l‘inserimento stesso del nuovo bean.
Per superare questo impedimento ci sono due possibili soluzioni, entrambe non sempre praticabili: la prima consiste nel rilassare il vincolo NOTNULL sulla FK della tabella principale, in modo da consentire all‘application server di lavorare secondo la modalità a due fasi (ejbCreate-ejbPostCreate).
Questa scelta “forte” è spesso incompatibile con la politica di progetto o aziendale adottata sulla gestione dei dati. Si può giustificare questa soluzione sulla base del fatto che, avendo scelto di adottare un container come ausilio a tutte le operazioni di persistenza (ma non solo), è necessario fidarsi in tutto e per tutto di tale strumento in modo da consentirgli di lavorare nel migliore dei modi (efficienza, sicurezza, praticità e cosi via).
Raramente questo è possibile: non è infrequente che certe decisioni si scontrino con la volontà di chi gestisce il database, oppure che il DB non sia a solo uso e consumo della applicazione EJB, per cui rilassare un vincolo del genere potrebbe in ogni momento indurre errori molto gravi.
È questa una situazione molto comune, che anzi si potrebbe definire la norma (raramente capita di dover scrivere una applicazione ex-novo in cui ci viene data carta bianca per progettare anche il DB-schema).
La soluzione alternativa consiste nel configurare (ove possibile) il container in modo che esegua la scrittura dei dati in una sola volta (ovvero mantenere la transazionalità non solo a livello applicativo ma anche a livello SQL).
Ad esempio in JBoss questa cosa è possibile utilizzando la seguente configurazione
INSERT after ejbPostCreate Container true
Si tratta di dire al container di eseguire l‘inserimento nel DB solo dopo il completamento del metodo ejbPostCreate() rendendo atomiche tutte le scritture dei dati.
Non sempre l‘application server e in particolare il container EJB permettono questo livello di dettaglio nella configurazione per cui è bene consultare la documentazione del prodotto utilizzato.
Maggiori dettagli su questo argomento si possono trovare sul Forum e sul Wiki di JBoss.org, in particolare [3]
Conclusione
Questo mese abbiamo visto due aspetti molto importanti legati alla programmazione di applicazioni CMP: non sono questi certamente i più importanti ne tantomeno i soli su cui si debba porre attenzione; molto altro serve per poter maneggiare con successo la tecnologia EJB e in particolare CMP.
I due punti affrontati sono però argomenti chiave per la buona riuscita di un progetto basato su Enterprise Java Beans e tendono a togliere un po‘ di quei pregiudizi che circolano su questa tecnologia spesso sottovalutata o mal considerata.
Proseguendo con questo approccio, il prossimo mese parleremo di ottimizzazione CMP e di come sfruttare il container per la generazione delle chiavi primarie.
Bibliografia
[1] “Architetture e tecniche di progettazione EJB – III parte: la trasmissione dei dati fra gli strati tramite il pattern DTO (introduzione)” di Giovanni Puliti, MokaByte 102 Dicembre 2005 (e successivi).
https://www.mokabyte.it/2005/12/ejbarchitectures-3.htm
[2] Core Developers Networks
http://www.coredevelopers.net/
[3] InsertAfterEjbPostCreate, su Wiki JBoss.org
http://wiki.jboss.org/wiki/Wiki.jsp?page=InsertAfterEjbPostCreate