EclipseLink, come altre implementazioni della specifica JPA (Java Persistence Api), solleva il programmatore da molti affanni. Tuttavia ci sono casi in cui è necessario entrare in merito a cosa stia succedendo. In questo post vediamo come intercettare eventi di tipo
SessionEvent
e utilizzarli per le nostre esigenze più segrete.
Nani sulle spalle di un gigante
Con JPA lo sviluppatore si vede sollevato da molti compiti e dalla scrittura di molto codice definito boilerplate: codice configurativo, ripetitivo, che non richiede creatività e che risulta essere molto error-prone. JPA ci solleva infatti dalla necessità di:
- gestire la connessione;
- aprire e chiudere la transazione (se si lavora in modalità CMT, Container-Managed Transactions, ovvero transazioni gestite dal Container EJB dell’application server);
- scrivere le query (questo è tanto più vero e conveniente quanto più il nostro database non è legacy).
Tuttavia cosa succede se nel nostro codice vogliamo fare qualcosa nel momento in cui viene aperta una transazione? O sapere se una Query ha modificato o meno alcune righe? O loggare quando la transazione è andata in Rollback?
L’interfaccia SessionEventListener
EclipseLink mette a disposizione l’interfaccia SessionEventListener
. Implementandola e registrando la propria implementazione con la sessione di lavoro di EclipseLink è possibile inserire il proprio codice all’interno del ciclo di vita della sessione in corrispondenza di determinati eventi. Gli eventi principali che vengono messi a disposizione dall’interfaccia sono:
// Evento lanciato dopo che una query di lettura
// ha reperito più di una singola riga dal database
void moreRowsDetected(SessionEvent event)
// Evento lanciato dopo che un SQL di update
// o delete ha restituito un row count of zero
void noRowsModified(SessionEvent event)
// Evento lanciato dopo che una
// database transaction è iniziata
void postBeginTransaction(SessionEvent event)
// Evento lanciato dopo che una
// database transaction è stata committata
void postCommitTransaction(SessionEvent event)
// Evento lanciato dopo che una Query
// è stata eseguita
void postExecuteQuery(SessionEvent event)
// Evento lanciato dopo che una
// database transaction è terminata con Rollback
void postRollbackTransaction(SessionEvent event)
// Evento lanciato prima che una
// database transaction sia iniziata
void preBeginTransaction(SessionEvent event)
// Evento lanciato prima che una
// database transaction sia committata
void preCommitTransaction(SessionEvent event)
// Evento lanciato prima che una Query
// è stata eseguita
void preExecuteQuery(SessionEvent event)
// Evento lanciato prima che una
// database transaction sia terminata con Rollback
void preRollbackTransaction(SessionEvent event)
Per la documentazione completa sull’interfaccia SessionEventListener
consultare la relativa documentazione di riferimento.
Devo per forza implementare SessionEventListener e scrivere decine di metodi?
Per fortuna la risposta è no: EclipseLink ha già una sua implementazione di SessionEventListener
, la classe SessionEventAdapter
; possiamo estendere da questa per avere una implementazione già pronta ed eseguire l’override solo di quei metodi specifici che vengono chiamati in corrispondenza di eventi per noi di interesse.
Un esempio concreto di utilizzo di SessionListener
Un caso in cui ci siamo trovati a usare con profitto il SessionListener
è quello di un DB legacy Oracle dove i Database Admnistrator tracciavano le modifiche al database utilizzando dei cosiddetti campi di Audit: nella fattispecie su ogni tabella erano previsti quattro campi aggiuntivi:
- campo data inserimento;
- campo data ultimo aggiornamento;
- campo utente inserimento;
- campo utente ultimo aggiornamento.
oltre a dei trigger after insert
e after update
che valorizzavano i due campi data con SYSDATE
, i due campi utente con il valore della variabile Oracle client_info
.
E’ una variabile contenuta nel package DBMS_APPLICATION_INFO che contiene informazioni sull’ultimo client, così come valorizzato dalla procedura SET_CLIENT_INFO. Vedere la documentazione Oracle per maggiorni informazioni
Affinchè questo sistema di tracciatura DB centrico funzioni bisogna impostare correttamente lato applicativo il valore del client_info
ad ogni transazione: la presenza del connection pool dell’application server tuttavia fa sì che impostarlo all’avvio dell’applicativo non sia sufficiente.
Ci siamo riusciti implementando il nostro SessionEventAdapter
ed eseguendo l’override di alcuni eventi specifici.
AuditSessionListener e i suoi eventi
ThreadLocal consente di restituire una copia di un determinato oggetto a un singolo thread; questo oggetto seguirà infine tutta la vita del thread a cui è stato associato, così come il suo valore sarà indipendente dagli altri oggetti della stessa classe, ma associati ad altri thread. Consultare il JavaDoc per approfondimenti.
La classe da noi realizzata, AuditSessionListener
, sfrutta gli eventi:
-
preExecuteQuery
, lanciato ogni volta che una nuova query viene eseguita; grazie a questo evento riusciamo a capire se è stata lanciata una query di modifica: in tal caso impostiamo ilclient_info
con l’utente loggato (grazie al metodo di utilitàWSSubject.getCallerPrincipal()
, utilizzabile sull’application server WebSphere se è abilitato JAAS) e memorizziamo il fatto che questo è accaduto in un ThreadLocal -
preBeginTransaction
, lanciato ogni volta che una nuova transazione viene creata; ci serve soltanto per resettare il valore delThreadLocal
In questo modo andiamo a impostare il client_info
solo nel caso in cui nella transazione ci sia una query di modifica (org.eclipse.persistence.queries.ModifyQuery
) e solo la prima volta, minimizzando le chiamate al database.
AuditSessionListener: il codice
Ed ecco infine l’implementazione di AuditSessionListener
:
public class AuditSessionListener extends SessionEventAdapter
{
public final static ThreadLocal auditExecuted = new ThreadLocal();
@Override
public void preBeginTransaction(SessionEvent event)
{
auditExecuted.set(false);
}
private void setOracleClientInfo(SessionEvent event)
{
String userName = getLoggedUserName();
event.getSession().getActiveUnitOfWork().executeQuery("call DBMS_APPLICATION_INFO.set_client_info(" + userName + ")");
}
private String getLoggedUserName()
{
String principal = WSSubject.getCallerPrincipal();
if (principal != null)
{
return principal;
}
else
{
return "UNAUTHENTICATED";
}
}
@Override
public void preExecuteQuery(SessionEvent event)
{
if (!Boolean.TRUE.equals(auditExecuted.get()))
{
if (event != null && event.getSession() != null
&& event.getSession().getActiveUnitOfWork() ! = null)
{
DatabaseQuery query = event.getQuery();
if ((query instanceof ModifyQuery))
{
setOracleClientInfo(event);
auditExecuted.set(true);
}
}
}
}
}
Registrazione di AuditSessionListener come listener della sessione EclipseLink
E’ sufficiente aggiungere al file persistence.xml la riga seguente:
affinchè EclipseLink chiami il metodo da noi sovrascritto in corrispondenza degli eventi preBeginTransaction
e preExecuteQuery
.
Conclusioni
La specifica JPA semplifica di molto il lavoro del programmatore, ma allo stesso tempo astrae e può rendere difficile capire come effettuare operazioni di basso livello. EclipseLink, con le sue API specifiche, offre tutti gli strumenti necessari per sporcarsi le mani senza per questo fare indispettire l’Ejb Container dell’application server.