Introduzione
Un
entity bean è un particolare tipo di EJB il cui scopo è
quello di rappresentare un oggetto, un elemento o un soggetto sul server.
Per sua natura questo genere di componenti delegato a memorizzare
delle informazioni e di offrire una serie di metodi per la gestione da
remoto da parte del client.
In
quest’ottica è possibile quindi suddividere le funzionalità
(metodi) di un bean in due categorie: da una parte si trova tutto ciò
che è relativo alla sua manipolazione da remoto direttamente tramite
il client, ovvero la cosiddetta business logic, mentre dall’altro si trova
ciò che è relativo alla gestione del ciclo di vita del componente
del suo mantenimento dello stato e della gestione della sicurezza.
Già
in precedenza [1] si è avuto modo di vedere come un client possa
interagire con un componente remoto, ricavandone prima la sua interfaccia
remota e poi invocandone i metodi pubblici.
Uno
degli aspetti più importanti degli entity bean è come questi
implementano la persistenza dei dati all’interno del database. Due sono
le possibilità: da un lato tutto il processo di sincronizzazione
viene gestito dal container in modo trasparente (e si parla in questo caso
di persistenza container managed), mentre nell’altro caso tutte le
informazioni sono salvate su disco direttamente tramite operazioni effettuate
dal bean (è il caso della gestione bean managed).
In
questo articolo ci soffermeremo sulla gestione della persistenza tramite
container, rimandando al mese prossimo il caso per così dire manuale.
Container Managed
Persistence
In
questo caso il server si preoccupa in modo del tutto trasparente di allineare
le informazioni contenute nel bean al variare dello stato e soprattutto
in funzione dello stato assunto all’interno del ciclo di vita del componente.
Come detto in precedenza [2] il database di appoggio può essere
relazionale o ad oggetti.
Tranne
per il caso delle variabili definite transient, tutti gli oggetti serializzabili,
o le variabili primitive possono essere salvati. Nella maggior parte si
tende ad utilizzare variabili primitive a causa della loro più semplice
adattamento alla struttura relazionale del db.
La
specifica EJB 1.1 permette di rendere persistenti anche campi di un bean
che contengano riferimenti ad altri bean: in questo caso il costruttore
del server deve implementare alcune tecniche piuttosto complesse al fine
di effettuare un mapping il più sensato possibile. Normalmente questo
si traduce nella memorizzazione della chiave primaria (oggetto Primari
Key), degli oggetti Handle o HomeHandle, o di altri riferimenti che
identifichino univocamente il bean contenuto.
La
gestione della persistenza a carico del container semplifica molto il lavoro
di implementazione del bean, ma complica non poco la fase di progettazione
del server; infatti in questo caso le tecniche di persistenza devono essere
il più generiche e automatiche possibili, visto che non si può
fare nessuna assunzione a priori su come sia fatto il componente.
Nel
caso invece dei bean-managed si possono implementare tecniche ad
hoc, a seconda del caso.
Identificazione
di un bean: la Primary Key
Quando
un client effettua una operazione di lookup su un oggetto remoto, si deve
poterlo identificare un maniera univoca fra tutti quelli messi a disposizione
dal server.
Per
questo motivo ad ogni bean è associata una chiave, detta Primary
Key, che può essere costituita da una classe apposita, oppure identificata
da un campo del bean stesso.
Nel
primo caso la classe ha un nome che segue lo schema NomeBeanPK: tale classe
contiene al suo interno tutte le informazioni necessarie per individuare
il bean. Ad esempio supponendo di avere un bean denominato MyBean, si potrebbe
scrivere
public
void MyBeanPK{
public int beanId;
public MyBeanPK(){}
public MyBeanPK(int id){
beanId=id;
public boolean equals(Object obj){
if (obj == null || !(obj instanceof MyBeanPK))
return false;
else if(((MyBeanPk)obj).beanId == this.beanId)
return true;
else
return false;
}
public int hashCode(){
return this.beanId
}
// converte in stringa il valore della chiave
public String toString(){
return this.beanId+””;
}
}
In
questo caso questa classe funge da wrapper per una variabile intera che
rappresenta la chiave del bean. Per questo motivo sono stati ridefiniti
i metodi equals() ed hashCode() in modo da operare su tale variabile: nel
primo caso viene fatto un controllo con la chiave dell’oggetto passato,
mentre nel secondo si forza la restituzione di tale intero (cosa piuttosto
comoda ad esempio nel caso in cui si vogliano memorizzare i bean in una
tabella hash, per cui due codici hash potrebbero essere differenti anche
se fanno riferimento allo stesso Id).
Si
noti la presenza del costruttore vuoto, necessario per poter permettere
al container di istanziare un nuovo componente in modo automatico
e di popolarne gli attributi con valori prelevati direttamente dal database
utilizzato per la persistenza. Questa operazione effettuata in automatico,
si svolge piuttosto frequentemente durante il ciclo di vita del componente,
come visto in [2], ed è alla base della gestione della persistenza
ad opera del container.
Per
questo motivo tutti i campi della Primary Key devono essere pubblici, in
modo che tramite la reflection il container possa accedervi in modo
automatico.
Alcuni
server permettono di accedere, tramite tecniche alternative, anche a variabili
con differente livello di accesso, ma questo diminuisce la portabilità
della Primary Key. Inoltre una Primary Key deve sottostare alle regole
imposte per l’invocazione remota basata su RMI: questo significa che possono
essere chiavi classi serializzabili o remote dove sia stato ridefinito
i metodi equals() ed hashCode().
Si
è detto che una chiave può non essere una Primary Key
definita come come classe a se stante: infatti in alternativa è
possibile utilizzare direttamente classi String o wrapper di tipi primitivi
(ad esempio Integer); in questo caso si parla di chiave semplice. Il metodo
di ricerca in questo caso agisce direttamente sul campo del bean, come
ad esempio
public
interface MyBeanHome implements javax.ejb.EJBHome{
public MyBean findByPrimaryKey(java.lang.Integer key)
throws
FinderException, RemoteExcepion;
}
In
questo caso anche se la chiave primaria non può essere costituita
direttamente un tipo primitivo, ma occorre utilizzare sempre un wrapper,
al fine di garantire una maggiore standardizzazione della interfaccia;
ad esempio il metodo getPrimaryKey() restituisce un Object generico, che
verrà convertito a seconda del caso.
L’utilizzo
di una classe Primary Key apposita è da preferirsi nel caso in cui
si desideri consentire ricerche basate su chiavi composte (uno o più
campi), oppure quando sia necessaria una maggiore flessibilità
e scalabilità del codice.
La
specifica EJB 1.0 lasciava indefinito questo aspetto, rimandando al costruttore
del server la scelta di supportare o meno le chiavi semplici: in tal caso
deve essere presente un solo campo di questo tipo.
Con
la versione 1.1 invece le cose sono state definite in modo molto più
preciso: in questo caso infatti, nel caso di single field key, la
scelta del campo da utilizzare come chiave non viene fatta durante la progettazione
del componente, ma piuttosto durante la fase di deploy grazie ad un apposito
documento XML. Ad esempio si potrebbe decidere di scegliere un campo beanId
contenuto nel bean stesso tramite la seguente sequenza di script XML
<ejb-jar>
<enterprise-beans>
<entity>
<primary-field>beanId</primary-field>
</entity>
</enterprise-beans>
</ejb-jar>
Il
poter definire in fase di deploy quale sia la chiave da utilizzare ha una
importante impatto sulla portabilità: in EJB 1.0 infatti il progettista
deve decidere a priori quale sarà il campo da utilizzare per tale
scopo, dovendo quindi fare una serie di assunzioni su quello che sarà
il database utilizzato per la persistenza (relazionale o ad oggetti) e
sulla struttura dati da esso offerta (ad esempio struttura delle tabelle).
Rimandando
alla fase di deploy tale scelta di fatto permette di svincolare completamente
la fase di progettazione ed utilizzazione del componente dalla quella di
utilizzo finale.
E’
per questo motivo quindi che tutti i metodi che hanno a che fare con la
chiave lavorano con parametri di tipo Object, rendendoli del tutto
slegati dalla particolare scelta sul campo scelto come chiave.
In
definitiva questa impostazione facilita la creazione di un mercato di componenti
terze parti pronti per l’uso indipendenti dalla piattaforma d’utilizzo.
La ricerca tramite
i metodi EJBFind
La
ricerca di un bean, una volta definite le chiavi, avviene tramite i metodi
di find messi a disposizione dalla interfaccia remota.
La
specifica infatti dice che la Remote Interface deve fornire zero o più
metodi creazionali, ed uno o più metodi di ricerca.
Per
quanto riguarda i metodi di ricerca, nel caso dei container managed, tali
metodi sono creati automaticamente al momento del deploy, in base alla
politica particolare implementata dal container per effettuare la ricerca,
ed in base a quanto specificato nei parametri di deploy. In alcuni casi
il server permette di specificare attraverso il tool di deploy anche il
comportamento di tali metodi.
Il
findByPrimaryKey() è il metodo standard, ovvero quello che consente
la ricerca in base alla chiave primaria, ma non è detto che sia
il solo messo a disposizione dalla Remote. Ad esempio potrebbe essere comodo
effettuare ricerche sulla base di particolari proprietà del bean:
in questo caso i nomi dei metodi seguono lo schema findByNomeCampo(). Secondo
le specifiche infatti, questa deve fornire oltre ai metodi creazioni
Ovviamente
nel caso di ricerche per campi differenti dalla chiave primaria possono,
si possono avere risultati multipli: per questo con la specifica 1.1 il
risultato della ricerca può essere un oggetto di tipo Collection.
I
metodi di ricerca che ritornano un solo oggetto devono generare una
eccezione di tipo FinderException in caso di errore, mentre una ObjectNotFoundException
nel caso in cui non sia stato trovato niente.
L’interfaccia
EntityBean e la gestione del contesto
L’interfaccia
EntityBean definisce una serie di metodi di callback atti alla gestione
dello stato di un bean.
public
interface EntityBean extends EnterpriseBean{
public
abstract void ejbActivate() throws RemoteExcetpion;
public
abstract void ejbPassivate() throws RemoteExcetpion;
public
abstract void ejbLoad() throws RemoteExcetpion;
public
abstract void ejbStore() throws RemoteExcetpion;
public
abstract void ejbRemove() throws RemoteExcetpion;
public
abstract void setEntityContext(EntityContext ec)
throws
RemoteExcetpion;
}
La
maggior parte di tali metodi però non sono particolarmente
utili nel caso in cui sia il container a dover gestire la persistenza dei
dati memorizzati nel componente. Per questo motivo parleremo in seguito
in modo più dettagliato di tali aspetti, fatta eccezione per quello
che riguarda la gestione del contesto.
L’interfaccia
EntityContext fornisce una serie di informazioni utili durante il
ciclo di vita di un bean, indipendentemente dalla politica adottata per
il mantenimento dello stato, ed è inoltre il tramite per il collegamento
fra il bean ed il proprio container.
Il
primo metodo invocato successivamente alla creazione della istanza del
bean è il setEntityContext() il quale passa al componente
una istanza del contesto in cui agisce: esso viene invocato prima che il
bean appena creato entri nel pool delle istanze pronte per l’utilizzo (vedi
[2]).
Ripensando
al ciclo di vita di un bean, visto in precedenza, si potrà rammentare
come in
questa
situazione le varie istanze non sono ancora state popolate con i dati prelevati
dal database, rendendole di fatto tutte equivalenti. E’ al momento della
invocazione da parte del client uno degli elementi del pool viene contestualizzato:
le sue proprietà vengono istanziate, con dati prelevati dal database,
e l’istanza viene associata o “wrapperizzata” con un EJB object per l’interfacciamento
con il client.
In
questa fase viene creato un contesto per il bean, sia da un punto di vista
astratto, sia concretamente assegnando un oggetto EntityContext apposito.
Tale contesto rappresenta di fatto il tramite fra il bean ed l’EJB Object,
oltre a fornire ad esempio al bean stesso la sua Primary Key.
Il
meccanismo di pool e di swap di istanze al suo interno, si è
detto necessario per garantire maggiori prestazioni e ridurre al contempo
lo spreco di risorse.
Da
un punto di vista implementativo questo viene resto possibile grazie al
continuo cambio di contesto associato al bean stesso il quale non necessita
di sapere cosa sta succedendo fuori dal suo container o quale sia il suo
stato corrente, dato che vive il mondo esterno grazie all’utilizzo di un
context personalizzato.
Fino
a quando il metodo ejbCreate() non ha terminato la sua esecuzione nessun
contesto è assegnato al bean, e quindi il bean non può accedere
alle informazioni relative al suo contesto come la chiave primaria o dati
relativi al client invocante: tali informazioni sono invece disponibili
durante l’invocazione del metodo ejbPostCreate().
Invece
tutte le informazioni relative al contesto di esecuzione come ad esempio
le variabili d’ambiente sono già disponibili durante la creazione.
Quando
un bean termina il suo ciclo di vita, il contesto viene rilasciato grazie
al metodo unsetEntityContext().
Per
quanto riguarda invece i metodi creazionali, benché acquistino particolare
importanza nel caso di gestione della persistenza bean managed, possono
rivelarsi utili per ottimizzare la gestione del componente e delle sue
variabili.
Ogni
invocazione di un metodo di questo tipo sulla Home Interface viene propagata
sul bean in modo del tutto analogo a quanto avviene per i business
methods invocati sulla remote Interface. Questo significa che si dovrà
implementare un metodo ejbCreate() per ogni corrispondente della
Home Interface.
La
loro implementazione così come la loro firma dipende da quello che
si desidera venga compiuto in fase di creazione. Tipicamente si tratta
di una inizializzazione delle variabili di istanza. Ad esempio si potrebbe
avere
//
versione EJB 1.1 che ritorna un bean
public
MyBean ejbCreate(int id, String name, ….){
this.beanId=id;
this.beanName=name;
…
return
null;
}
//
versione EJB 1.0 che ritorna un void
public
void ejbCreate(int id, String name, ….){
this.beanId=id;
this.beanName=name;
…
}
Come
si può notare la differenza fondamentale fra la specifica EJB 1.0
e 1.1 è il valore ritornato.
Nella
1.0 infatti il metodo crea un componente senza ritornare niente, mentre
nella 1.1 viene ritornato una istanza null della classe che si sta creando.
In definitiva il risultato finale è lo stesso, dato che il valore
ritornato viene ignorato in entrambi i casi. Il motivo di questa scelta
è infatti di tipo progettuale: la scelta fatta in 1.1 permette infatti
un subclassing del codice più semplice permettendo l’estensione
da parte di un componente bean managed di un container managed bean in
modo più semplice. Nella versione precedente questo non era possibile
dato che in Java non è permesso l’overload di un metodo che differisca
solo per il tipo ritornato.
La
nuova specifica permette quindi ai costruttori di server di supportare
la persistenza container managed semplicemente estendendo un bean container
managed con un bean generato di tipo bean managed.
La
sincronizzazione con il database: i metodi ejbLoad() ed ejbStore()
Anche
nel caso di un bean di tipo container managed, se si ha la necessità
di effettuare particolari operazioni di conversione o preparazione del
bean prima che questo sia reso persistente, o viceversa subito dopo la
lettura dal database, allora i metodi ejbLoad() ed ejbStore() possono essere
d’aiuto.
Si
pensi ad esempio al caso in cui uno dei campi del bean sia costituito da
un oggetto non serializzabile: nel pezzo di codice che segue ad esempio
si è preso in esame un oggetto di una ipotetica classe MyTable non
serializzabile. In questo caso l’istanza è indicata transient, in
modo da impedire la memorizzazione automatica nel db nella tabella del
database, rimandando tale operazione sull’oggetto strTable, il quale offre
una rappresentazione a stringa della tabella.
public
class MyBean extends EntityBean{
public
transient MyTable table;
public
String strTable;
public
void ejbLoad(){
strTable=table.toString();
}
public
void ejbStore(){
table=
new MyTable(strTable);
}
...
}
Senza
entrare nei dettagli dell’implementazione della classe MyTable, sarà
sufficiente dire che il costruttore accetterà come parametro una
stringa contenente i valori riga-colonna che costituiscono la tabella,
e che il metodo toString() effettuerà l’operazione opposta. Anche
se l’esempio è volutamente poco significativo da un punto di vista
applicativo, mostra come con estrema semplicità sia possibile implementare
uno strato di pre e post elaborazione intorno all’operazione di salvataggio
su database. Dovrebbe essere altresì chiaro come questa organizzazione
permette di risolvere in modo estremamente elegante problemi complessi.
Il deploy In 1.0
e 1.1
Dopo
aver visto tutte le varie parti che compongono un entity bean, resta da
vedere come sia possibile rendere funzionante tale componente, ovvero come
effettuare il deploy di un EJB nel server. Le cose sono molto differenti
a seconda che si desideri seguire la specifica 1.0 o 1.1: nel secondo caso
le cose si sono semplificate moltissimo rispetto al passato, dato che è
sufficiente scrivere in un documento XML tutte le informazioni necessarie.
Molto spesso tale documento viene generato in automatico dal tool di sviluppo:
ad esempio JBuilder dalla versione 4 offre un editor visuale delle varie
proprietà di funzionamento del bean, editor che poi rende possibile
la creazione del file XML. Un esempio che si può ottenere
con tale prodotto potrebbe essere il seguente
<?xml
version="1.0" encoding="Cp1252"?>
<!DOCTYPE
ejb-jar PUBLIC '-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN'
'http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd'>
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>TellerBean</ejb-name>
<home>quickstart.TellerHome</home>
<remote>quickstart.Teller</remote>
<ejb-class>quickstart.TellerBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
<entity>
<ejb-name>Accounts</ejb-name>
<home>quickstart.AccountsHome</home>
<remote>quickstart.Accounts</remote>
<ejb-class>quickstart.AccountsBean</ejb-class>
<persistence-type>Container</persistence-type>
<prim-key-class>java.lang.String</prim-key-class>
<reentrant>False</reentrant>
<cmp-field>
<field-name>name</field-name>
</cmp-field>
<cmp-field>
<field-name>balance</field-name>
</cmp-field>
<primkey-field>name</primkey-field>
<resource-ref>
<res-ref-name>jdbc/accounts</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</entity>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>TellerBean</ejb-name>
<method-name>*</method-name>
</method>
<method>
<ejb-name>Accounts</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
L’esempio
a cui si riferisce tale file XML è uno di quelli presenti nel tutorial
Borland scaricabile direttamente dal sito della casa americana.
Per
quanto riguarda invece il deploy di componenti secondo la specifica 1.0,
le cose sono alquanto più complesse. In questo caso infatti il deploy
descriptor è costituito da un oggetto Java serializzato istanza
di una classe che nel caso degli entity è la EntityDescriptor, che
deriva dalla più generica DeploymentDescriptor. Nel caso dei session
bean la classe da utilizzare sarebbe stata la SessionDescriptor.
Dopo
aver istanziato un oggetto tramite il costruttore
EntityDescriptor
ed = new EntityDescriptor();
sarà
possibile memorizzare tutte le informazioni relative al bean tramite i
metodi messi a disposizione. Ad esempio si potrà indicare i nomi
della classi che compongono complessivamente il bean nel seguente modo
ed.setEnterpriseBeanClassName(“MyBean”);
ed.setHomeInterfaceClassName(“MyBeanHome”);
ed.setRemoteInterfaceClassName(“MyRemoteBean”);
ed.setPrimaryKeyClassName(“MyBeanPK”);
Per
quanto riguarda la persistenza poi attraverso le seguenti righe di codice
è possibile indicare quali campi debbano essere memorizzati nel
database
Class
BeanClass = MyBean.Class;
Field
[]persistentFields= new Field[n];
PersistentFields[0]
= BeanClass.getDeclaredField(“beanId”);
PersistentFields[1]
= BeanClass.getDeclaredField(“beanName”);
…
Tramite
l’istruzione
ed.setReentrant(false);
è
possibile avvertire il container che l’oggetto in questione contiene metodi
non rientranti (vedi [2]). Grazie all’utilizzo di un oggetto di tipo ControlDescriptor
è poi possibile definire gli attributi relativi alla transazionalità
e sicurezza. Ad esempio
ControlDescriptor
cd = new ControlDescriptor();
cd.setIsolationLevel(ControlDescriptor.TRANSACTION_READ_COMMITTED);
cd.setTransactionAttribute(ControlDescriptor.TX_REQUIRED);
cd.setRunAsMode(ControlDescriptor.CLIENT_IDENTITY);
Avendo
letto la puntata precedente del corso ([2]) si dovrebbe intuire piuttosto
facilmente il significato di tali istruzioni. In ogni caso riprenderemo
ed approfondiremo in una delle prossime puntate gli aspetti relativi alla
gestione delle transazioni.
E’
da notare l’opzione RunAsMode, con la quale si specifica il modo con cui
i metodi del bean potranno agire: in questo caso ogni invocazione altro
metodo o risorsa accedute verranno validate in base ai permessi del client.
Conclusione
Si
conclude qui la trattazione relativa agli entity bean di tipo bean managed.
Resta da vedere il mese prossimo la persistenza bean managed.
Rimando
per maggiori approfondimenti alla voce [3] della bibliografia.
Bibliografia
[1]
– “Enterprise Java Beans” – di Giovanni Puliti - www.mokabyte.it/2000/05/ejb.htm
[2]
– “Corso EJB: i servizi di sistema” – di Giovanni Puliti - www.mokabyte.it/2000/11/ejb_2.htm
[3]
– ”Enterprise Java Beans, 2’ Edizione” – di Richard Monson Haefel – Ed.
O’Reilly
|