EclipseLink Historical Session: come tenere traccia dei cambiamenti di una tabella – Parte II

Abbiamo affrontato in un altro post il tema della tracciabilità dei cambiamenti sui dati sensibili e i sistemi teorici che permettono di realizzarla. Siamo scesi poi nel dettaglio di come EclipseLink implementa uno di questi sistemi e di come configurare l’ambiente di lavoro per potercene avvalere in modo piuttosto semplice. Vediamo quindi adesso come poter interagire con i dati salvati nello storico… effettuando query nel passato!!

Lavorare nel passato con EclipseLink: pregi e difetti

Normalmente, anche se in ambiente EE è mascherato dall’entity manager, EclipseLink permette di interagire con il database attraverso un oggetto Session che rappresenta una vista sulle versioni più aggiornate dei dati sul database. Come è facile intuire dal nome, quando si lavora con una Historical Session si ha accesso invece alle versioni passate di un oggetto in un dato momento nel tempo.

Esistono 3 modi piuttosto semplici per effettuare query nel passato attraverso l’uso dei seguenti oggetti:

  1. ObjectLevelReadQuery;
  2. ExpressionBuilder;
  3. Session.

Tale semplicità però si paga con qualche limitazione:

  • Nella nostra entità non si possono mappare gli attributi data inizio/fine perché concettualmente non esistono: sono dei marker per la gestione dello storico.
  • Non è possibile effettuare query in un intervallo di tempo nel passato, ma solo in un momento specifico nel tempo.

Accesso allo storico dei dati

Prima di scendere nel dettaglio del codice, se lavoriamo in ambiente enterprise abbiamo bisogno di un metodo di utility per accedere direttamente alla sessione di comunicazione attiva sul database, nascosta dall’interfaccia EntityManager. Ammettiamo quindi d’ora in poi di usare la classe JPAUtilityHelper sì fatta:

public class JPAUtilityHelper {
     public static Session getSessionFrom(EntityManager entityManager) {
          return ((JpaEntityManager) entityManager.getDelegate()).getActiveSession();
     }
}

ObjectLevelReadQuery nel passato

E’ possibile caricare lo stato di un intero oggetto con i valori che ha assunto ad un certo momento della sua vita. Come si fa? Ricordando l’entità UserData del post precedente, analizziamo il seguente codice:

...
ReadAllQuery historicalQuery = new ReadAllQuery(UserData.class);
AsOfClause asOfClause = new AsOfClause(timeStamp);
historicalQuery.setAsOfClause(asOfClause);
historicalQuery.dontMaintainCache();
Session activeSession = JPAUtilityHelper.getSessionFrom(entityManager);
List oldUserDataList = 
            (List) activeSession.executeQuery(historicalQuery);
...

Rispetto ad una normale query eseguita con un ObjectLevelReadQuery notiamo 2 elementi nuovi:

  • L’oggetto AsOfClause, istanziato in un punto specifico nel tempo (timeStamp). Questo oggetto sta ad indicare che vogliamo il risultato come era al tempo definito dal timeStamp. Ottengo lo stesso risultato anche tramite la query hint eclipselink.history.as-of
  • La chiamata al metodo historicalQuery.dontMaintainCache() è fondamentale per non sporcare la cache di primo livello di EclipseLink: se tale metodo non viene chiamato, viene sollevata una eccezione a runtime per preservare i valori della cache. Lo stesso risultato si ottiene se viene chiamato historicalSession.setShouldMaintainCache(false)

La query SQL che viene generata sarà della forma:

SELECT * FROM user_data ud 
  WHERE (ud.start_date < = timeStamp) 
  AND ((ud.end_date IS NULL) OR ud.end_date > timeStamp)

Expression Framework nel passato

Attraverso l’Expression Framework di EclipseLink è possibile costruire query a runtime molto complesse. Tra le varie funzioni disponibili è possibile impostare una data nel passato per ricavare il valore di un oggetto o di un suo attributo in quel momento. Questo metodo è molto interessante perché permette di effettuare query che combinano valori attuali con valori passati. Vediamolo all’opera: ad esempio voglio trovare chi ha cambiato dati personali nell’ultimo anno. Possiamo utilizzare il codice seguente:

 ...
   ExpressionBuilder userData = new ExpressionBuilder(UserData.class);
   ExpressionBuilder pastUserData = new ExpressionBuilder(UserData.class);
   pastUserData.asOf(new AsOfClause(pastTime));
   Expression address = userData.get("address");
   Expression pastAddress = pastUserData.get("address");
   Expression selectionCriteria = pastAddress.notEqual(address).and(
                                userData.equal(pastUserData));
 ...

In questo caso si lavora direttamente sull’ExpressionBuilder: ne vengono create due istanze in modo che una punti ai valori attuali e una a quelli risalenti a pastTime. Il selection criteria costruito, ovvero la nostra clausola WHERE, afferma quindi che stiamo selezionando quegli stessi oggetti UserData che non hanno più lo stesso indirizzo da un anno a questa parte, ammesso che pastTime sia stato impostato ad un anno indietro dalla data corrente. Rimando al Javadoc del metodo public Expression asOf(AsOfClause pastTime) per maggiori dettagli.

L’oggetto HistoricalSession

EclipseLink mette a disposizione un’istanza particolare dell’oggetto Session, leggero e in sola lettura, responsabile esclusivamente di gestire query nel passato. Per recuperare questo tipo di istanza basta chiamare:

...
   Session activeSession = JPAUtilityHelper.getSessionFrom(entityManager);
   AsOfClause asOfClause = new AsOfClause(pastTime);
   Session historicalSession = activeSession.acquireHistoricalSession(asOfClause);
...

che farebbe anche comodo inserire direttamente nella nostra classe JPAUtilityHelper. Sull’istanza historicalSession possono essere quindi eseguite le query come nel caso precedente: l’unica differenza è che in questo caso non è necessario disattivare la gestione della cache perché è il framework ad occuparsene automaticamente.

Una volta quindi recuperato un oggetto dal passato è possibile ripristinarlo (rendendolo quindi quello attuale) in modo semplice:

...
   //ho già recuperato l'istanza dalla history (userDataToRestore)
   Session activeSession = JPAUtilityHelper.getSessionFrom(entityManager);
   UnitOfWork unitOfWork = activeSession.getActiveUnitOfWork();
   unitOfWork.deepMergeClone(userDataToRestore);
   unitOfWork.commit();
...

Conclusioni

Se nel post precedente avevamo visto come configurare il servizio di gestione dello storico mentre in questo caso abbiamo illustrato le tecniche che il framework mette a disposizione per lavorarci. Fondamentalmente gli approcci sono abbastanza equivalenti e si adattano alle esigenze di ogni caso. Rimane, a mio avviso, il grosso problema che non è possibile eseguire una query in un range di date nel passato, ma solo ad un tempo specifico. In un altro post verrà proposta una possibile soluzione a questa limitazione come effetto collaterale di un altro problema che si verifica quando si usa la gestione dello storico insieme alla generazione automatica del database. Ma questa è un’altra storia…

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio . - -