Prosegue la serie dedicata alla nuova release della specifica EJB. In questa puntata parliamo di persistenza e della piccola rivoluzione che ha stravolto il mondo degli Entity Beans.
La persistenza degli oggetti persistenti: l‘evoluzione della specie
Nel momento in cui la programmazione ha iniziato a gestire le comuni attività dell‘uomo (e quindi ha smesso di essere usata esclusivamente per la produzione di procedure di calcolo matematico), è nato il problema della gestione dei dati e di come collegare le strutture ai sistemi di persistenza sottostanti. Questi problemi sono stati risolti negli anni con tecniche di vario genere, che vanno dalla gestione manuale della sincronizzazione dati per mezzo di istruzioni SQL cablate nel codice, fino ai moderni sistemi di mapping basati su framework esterni alle applicazioni e configurabili in XML.
Java, nato circa 10 anni fa, da questo punto di vista non fa eccezione, tanto che nel corso degli anni ha proposto varie soluzioni al problema del mapping dei dati.
Ma in Java, come con ogni linguaggio a oggetti, si pone l‘ulteriore problema del cambio di paradigma per la rappresentazione delle informazioni: dal modello a oggetti infatti è necessario eseguire una traduzione secondo il modello relazionale (risultano trascurabili, se non quasi inesistenti, esempi di motori di persistenza a oggetti o basati su modelli differenti).
Nel corso degli anni la maggior parte dei programmatori Java ha implementato soluzioni che vanno dall‘utilizzo di istruzioni SQL con JDBC all‘interno di componenti appositamente progettati per mantenere la persistenza (eventualmente utilizzando il pattern DAO al fine di isolare lo strato di persistenza) fino ai framework di persistenza moderni in chiave JavaEE.
In questo scenario in particolare si sono succeduti interessanti soluzioni che hanno visto strumenti terze parti come Castor, TopLink o il recente Hibernate, fronteggiarsi con le soluzioni proposte da Sun: JavaBlend (scarsamente utilizzato e poco sviluppato), JDO (un parto incompleto, anch‘esso poco diffuso) e il potente sistema di mapping di EJB basato su componenti Entity Beans.
Quest‘ultimo in particolare, quando fu presentato, appariva pronto a sbaragliare la concorrenza che all‘epoca era scarsamente organizzata: di fatto fu il primo sistema enterprise di mapping integrato con la piattaforma transazionale di EJB.
La storia narra di un primo tentativo, poco fruttuoso peraltro, basato sui componenti BMP e solo successivamente sui più potenti e astratti CMP.
Ma fu solo con l‘avvento della release 2.0 di CMP, che i programmatori poterono finalmente disporre di una soluzione veramente affidabile, veloce, e di facile gestione. Le potenzialità dello strumento erano veramente alte e il livello di astrazione non indifferente.
CMP 2.0 sembrava poter diventare “lo strumento” di mapping, se non fosse per il fatto che parallelamente la concorrenza si era mossa in una direzione differente, dando luogo a strumenti forse meno potenti (dal punto di vista semantico) ma certamente più flessibili e snelli.
Il differente approccio seguito da Sun per lo sviluppo di CMP, fu la causa principale delle molte critiche che si sollevarono (ricordo alcuni forum di discussione veramente infiammati contro CMP 2.0): in molti sottolineavano come la lentezza e pesantezza del modello (in realtà mai dimostrata definitivamente) e la difficoltà di adattarsi a scenari reali (effettivamente è questa la peggior limitazione della tecnologia) fossero limiti insormontabili per l‘adozione in scenari reali di produzione.
Figura 1 – Schema gerarchico delle tecnologie che si sono evolute nel corso della storia di Java
A causa delle molte limitazioni, complice anche la cattiva pubblicità contraria a CMP, vi è stato negli ultimi anni un crescente e inarrestabile successo di prodotti concorrenti, primo fra tutti Hibernate che ad oggi è probabilmente il framework di mapping OO-RDBMS più utilizzato nel mondo.
Se non puoi battere il nemico, alleati con lui
I punti di forza di Hibernate sono senza dubbio la maggior flessibilità di configurazione, la leggerezza, e la maggior possibilità di personalizzarne il comportamento. Il fatto che possa essere utilizzato al di fuori di un container (cosa che introduce però la necessità di gestire esplicitamente alcuni aspetti come le transazioni) è probabilmente l‘elemento più importante che ne ha decretato il successo: in Hibernate i beans che verranno mappati su tabelle sono visti dal programmatore come semplici Java Beans (in gergo POJO, Plain Old Java Object) e non è necessario implementare complesse interfacce o rispettare le firme di metodi remoti altrettanto complessi.
Sun, facendo tesoro di questi fattori, ha cambiato strategia nella realizzazione della nuova release entities EJB: se da un lato si è mantenuto l‘obiettivo di realizzare un sistema astratto ad alto livello per il mapping (probabilmente ancora più alto rispetto a CMP 2.0 e Hibernate), ha adottato in pieno la fiolosofia del bean-POJO al posto di complessi bean remoti, isolando il tool di mapping in uno strato nascosto sottostante.
Il motore di persistenza è stato quindi inglobato nella specifica con un meccanismo di plugin, in modo che il programmatore possa scegliere fra uno di quelli attualmente disponibili: l‘implementazione JBoss utilizza come motore di persistenza Hibernate, mentre Oracle spinge per Toplink.
Rispetto al passato, in EJB 3.0 gli entities hanno un altro indubbio vantaggio: essendo dei semplici POJO gestiti dal persistence engine, spariscono i complessi file XML utilizzati per la configurazione del mapping dei campi dei beans (anche se si possono utilizzare ove sia necessario effettuare particolari operazioni di configurazione).
Infine il vantaggio più importante è senza dubbio quello di poter finalmente mappare strutture dati più complesse rispetto al passato consentendo la realizzazione di schemi più vicini alla realtà dei progetti enterprise.
I nuovi Entities
Seguendo la filosofia base di EJB 3.0 anche nell‘ambito degli entities i progettisti hanno per prima cosa pensato a semplificare: adesso un entity è un semplice Java bean che tramite l‘utilizzo di annotazioni viene sincronizzato con lo strato di persistenza.
Il programmatore non deve quindi implementare alcuna interfaccia remota, nessun metodo di callback e nessun complesso file XML. Gli entities sono quindi POJO che mappano concettualmente un dataset (tabella di un DB).
Il primo innegabile vantaggio che deriva da questo approccio è che non è più necessario vincolare lo sviluppo in ottica enterprise-container: un entity è prima di tutto un bean che può essere sviluppato e testato fuori dal container senza la necessità di eseguire l‘assemblamento e il deploy di complessi file .jar o .ear.
Ovviamente fuori dal container, un entity non può essere considerato un oggetto persistente o gestito da un sistema di sicurezza e transazionalità , servizi questi che verranno attivati nel momento in cui il bean verrà deployato e quindi trasformato in un enterprise bean vero e proprio.
Un Entity 3.0 è di fatto CMP, anche se in questo nuovo contesto perde di significato la definizione di Container Managed Persistence. Infine è da notare una importante modifica nella natura degli entities: sparisce per questi componenti il concetto di interfaccia locale e remota. Un bean non può essere in alcun modo invocato da un cliente remoto, il quale interagisce solamente con i session remoti. Un entity quindi è ancora più di prima un oggetto che rappresenta una entità del data model.
Definizione di un Entity 3.0
Per creare un entity di questo tipo, il processo, molto semplice, parte dalla definizione di un oggetto POJO che rappresenti una entità del data model (entità nel senso astratto di un elemento che compone il dominio applicativo, come una persona, uno studente, una abitazione etc.).
Il POJO conterrà quindi un elenco di proprietà con i relativi metodi accessori getXXX/setXXX e quel poco di metodi di business che sono stati individuati durante la procedura di analisi: un entity e quindi una entità non dovrebbero avere metodi di business se non quelli relativi alla manipolazione del proprio stato o per fornire informazioni a partire dal proprio stato, i cosiddetti attributi calcolati.
Ad esempio si potrebbe pensare di definire un POJO per un utente con la seguente classe:
public class User implements Serializable {
private String cognome;
private Integer id;
private String nome;
private String userType;
// seguono i metodi get/set delle varie proprietà ...
...
...
// il costruttore del bean che per la specifica JavaBeans
// deve essere vuoto e pubblico
public User(){...}
}
Fatto questo è necessario etichettare il POJO in modo che diventi un entity al momento del deploy, grazie alla annotazione @Entity:
@Entity@Table(name = "user")
public class User implements Serializable { ...}
Si noti l‘associazione con il nome della tabella sulla quale l‘entity verrà mappato; i dettagli relativi alla connessione con il database sono specificati esternamente al codice e sono la sola parte di configurazione che rimane in XML in un file apposito che verrà mostrato in seguito.
L‘associazione fra attributi del bean e colonne della tabella avviene tramite la seguente annotazione riportata in questo caso direttamente sul metodo getXXX
@Id@Column(nullable = false)
public Integer getId() { return id;}
Se non si fosse specificato il nome della tabella, come avveniva in passato il container esegue una associazione di default del tipo
nome-bean = nome-tabella
che vale anche per i campi
nome campo = nome colonna
Ciclo di vita di un entity
Si è detto in precedenza che, rispetto alla versione precedente, in EJB 3.0 non è più necessario o possibile (dipende dai punti di vista) gestire il ciclo di vita di una entità implementando complessi metodi di callback al fine di implementare in maniera corretta la persistenza e la sincronizzazione.
Questo certamente vero per quanto concerne quello che è necessario fare, ma non è esatto per quello che invece è possibile fare: gli entities sono gestiti in tutto e per tutto dal container in maniera trasparente ma il programmatore può implementare metodi etichettati con apposite annotations per essere invocati in determinati momenti relativi al ciclo di vita: non si tratta di metodi di interfaccia di callback, ma di comuni metodi che il programmatore decide di eseguire in concomitanza di uno scambio di contesto o della creazione/distruzione del bean.
Le annotazioni che permettono di legare un qualsiasi metodo ai vari eventi del ciclo di vita sono le seguenti:
- annotazione @PrePersist: il metodo è invocato appena prima che il bean sia reso persistente nel DB;
- annotazione @PostPersist: il metodo è invocato appena dopo che il bean sia reso persistente nel DB;
- annotazione @PreRemove: il metodo è invocato appena prima che il bean sia rimosso dal DB;
- annotazione @PostRemove: il metodo è invocato appena dopo che il bean sia rimosso dal DB;
- annotazione @PreUpdate: il metodo è invocato appena prima che il bean sia aggiornato nel DB;
- annotazione @PostUpdate: il metodo è invocato appena dopo che il bean sia aggiornato nel DB;
- annotazione @PostLoad: il metodo è invocato appena dopo che i dati sono letti dal DB e immessi nel bean.
Da notare anche la presenza della annotazione @Remove che non crea metodi di callback dato che la rimozione di un entitie viene comandata dal client e non dal container. Si faccia attenzione che le modifiche eseguite in un metodo annotato come @Remove verranno perse.
Le modifiche eseguite nei metodi di callback verranno propagate nelle entità per le quali sono definite le modifiche in cascata.
Qualche autore suggerisce di sfruttare a proprio vantaggio la maggior flessibilità derivante dal non dover più implementare alcuna interfaccia EJB, proponendo di creare apposite classi delegate alla gestione dei metodi di callback dette “callback listener class” tramite la annotazione @EntityListener: in questo caso a partire dalla classe User, si è deciso di creare una sottoclasse che verrà gestita in modalità controllata dal container:
@Entity@EntityListener(ControlledUserEntityListener.class)
public class ControlledUser extends User{
...
...
}
La classe ControlledUser delega alla ControlledUserEntityListener la definizione dei metodi di callback, che ricevono come parametro al costruttore una ControlledUser:
public class ControlledUserEntityListener {
@PrePersist
public preparePersistence (ControlledUser rec) {
...
...
}
@PreUpdate
public updateData (ControlledUser rec) {
...
...
}
}
Il mondo reale non è fatto di oggetti isolati
Ogni entità normalmente instaura relazioni con altre entità nel domani model ed anzi è piuttosto rado che un oggetto viva nel dominio applicativo in modo isolato. Nell‘esempio riportato si potrebbe immaginare che un utente del sistema (User) sia associato a un numero arbitrario di indirizzi: questa relazione viene implementata a livello di codice Java nel seguente modo:
public class User implements Serializable {
private String cognome;
private Integer id;
private String nome;
private String userType;
private List addressList;
...
}
dove si sono usati i generics, una caratteristica di Java5, per permettere di specificare il tipo degli elementi di una lista. Tramite la annotazione @OneToMany è possibile in questo caso legare i due POJO nel seguente modo
@OneToMany(mappedBy = "user")
public List getAddressList() {
return addressList;
}
public void setAddressList(List addressList) {
this.addressList = addressList;
}
Ovviamente, nel caso si avesse una relazione differente (sia per molteplicità che per per navigabilità della relazione), si potrebbero utilizzare le altre annotazioni @OneToOne, @ManyToMany e @ManyToOne, apportando le opportune modifiche al codice Java.
Ereditarietà fra entities
Una delle maggiori limitazioni di EJB 2.0 era la impossibilità di esprimere l‘altra relazione che, nell‘ambito della OOP, si può instaurare fra classi: la relazione Generalizzazione Specializzazione o padre figlio.
A causa della complessa dipendenza di un bean CMP 2.0 con le interfacce soprastanti (che di fatto bloccavano la possibilità di estendere da un padre comune) i programmatori non potevano (a meno di complessi salti mortali e acrobazie nel codice) gestire le relazioni padre figlio che si potrebbero instaurare fra due entità .
Nella realtà questo impedimento rappresenta una grave carenze specie se si pensa che gli entity beans mappano oggetti del datamodel e che in tale ambito è impossibile pensare a oggetti creati a partire da zero: molto spesso un utente del sistema è prima di tutto una persona e potrebbe essere ulteriormente specializzato in utente di sistema, utente ospite, super-utente.
Dato che i vari ORM devono in definitiva mappare entità su un modello relazionale e che tale modello non prevede la relazione di generalizzazione-specializzazione, spesso si finiva per non considerare tale carenza rimandando le associazioni ad oggetti wrapper dei beans stessi.
Questo approccio, per quanto possibile, è comunque complesso e introduce non poche problematiche oltre a delegittimare in un certo senso l‘uso di un ORM. In EJB 3.0 questa limitazione è stata eliminata tanto che adesso è possibile definire un POJO che estende da un altro POJO come nell‘esempio che segue:
public class PowerUser extends User implements Serializable {
public PowerUser() {
}
}
Indipendentemente da come EJB gestisce questa cosa tramite annotazioni, il problema che si pone a questo punto è come realizzare questa relazione nel modello relazionale che non prevede questo costrutto. Immaginando di avere una gerarchia costituita da n classi legate in relazione padre figlio (dove il padre base ha un set limitato di attributi e dove ogni figlio della relazione aggiunge alcuni attributi), sono possibili due approcci: una tabella per ogni classe della gerarchia, oppure una tabella associata alla classe in fondo alla gerarchia e che conterrà tutti i campi dei padri più quelli dei figli.
Nel primo caso ogni tabella mappa tutti e soli gli attributi della classe, ottenendo probabilmente una maggior pulizia: come controindicazione si ha una maggiore ridondanza di informazioni, oltre a una minor robustezza in caso di refactoring (è necessario molto lavoro per aggiornare tutte le tabelle se cambia la definizione di un qualche elemento nel mezzo della gerarchia).
La soluzione di utilizzare una tabella associata alla classe in fondo alla gerarchia prevede di creare la tabella in modo che possa ospitare elementi relativi ad un qualsiasi oggetto della gerarchia: tale tabella deve quindi contenere tutte le colonne corrispondenti a tutti gli attributi della classe più in basso nella gerarchia.
Ovvio che per ogni riga corrispondente a un oggetto di mezzo alcune colonne avranno valore nullo; per cui occorre fare molta attenzione ai vincoli not-null che si impongono sulle colonne della tabella stessa. In pratica conviene limitare al set minimo comune fra tutte le classe della gerarchia. In questo secondo approccio conviene utilizzare una colonna come discriminante del tipo di oggetto memorizzato nella riga della tabella.
EJB 3.0 supporta entrambe le strategie di ereditarietà (per default la seconda). Nell‘esempio che segue si è deciso di utilizzare una tabella per tutte le classi della gerarchia: in questo caso si è voluto come introdurre estensione della entità base User la classe PowerUser che nel sistema rappresenta un utente avanzato.
La colonna discriminante è associata al campo person_type: se person_type==”PW” allora si tratta di una istanza del tipo PowerUser, altrimenti è un utente normale.
@Entity
@Inheritance(strategy =InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="user_type")
@DiscriminatorValue(value="power")
public class PowerUser extends User {
protected int level;
public void setLevel (int l) {
this.level = l;
}
public int getLevel () {
return level;
}
}
Conclusione
La parte dedicata agli entity bean in EJB 3.0 continua nel prossimo articolo della serie in cui parleremo di metodi di ricerca e del motore di persistenza tramite il quale si possono realizzare tutte le operazioni relative alla persistenza e sincronizzazione dei dati con il DB.