Come potrete immaginare, un oggetto del genere si presta ad innumerevoli applicazioni pratiche, una
delle quali (diciamo anche quella che più facilmente viene
in mente) è la realizzazione di un servizio di audit, che tenga
traccia di data e utente relativi alla creazione e alla modifica di
un oggetto del nostro dominio. Per far ciò, definiamo innanzitutto la
seguente interfaccia:
public interface IAuditable
{
DateTime DataCreazione { get; }
string UtenteCreazione { get; }
DateTime DataModifica { get; }
string UtenteModifica { get; }
}
Tramite NHibernate è possibile persistere automaticamente queste informazioni in ogni classe che
implementi questa interfaccia, senza preoccuparcene minimamente all'interno del
codice di business.
Il modo più elegante per riuscirci è realizzare una
classe che implementi IInterceptor o, più semplicemente, ereditare dalla classe EmptyInterceptor (che
altri non è che un Interceptor completamente vuoto), effettuando
l'override dei metodi utili al nostro scopo. OnFlushDirty, ad esempio, è
sicuramente uno di questi:
public override bool OnFlushDirty(object entity, object id, object[] currentState,
object[] previousState, string[] propertyNames, IType[] types)
Esso viene invocato in corrispondenza del Flush di ogni
entity marcata come dirty (cioé che ha subito modifiche dall'operazione di load)
e fornisce una serie di parametri per accedere ad una reference alla stessa,
conoscerne il suo stato precendente ed eventualmente apportarvi variazioni prima
dell'update (agendo sull'array currentState, entity e
previousState non dovrebbero mai essere modificati direttamente).
Nel nostro caso, ad esempio, si può scrivere:
public override bool OnFlushDirty(object entity, object id,
object[] currentState, object[] previousState,
string[] propertyNames, IType[] types)
{
IAuditable auditableEntity = entity as IAuditable;
if (auditableEntity == null)
return base.OnFlushDirty(entity, id,
currentState, previousState, propertyNames, types);
bool res = false;
for (int i = 0; i < propertyNames.Length - 1; i++)
{
if (propertyNames[i] == "DataModifica")
{
currentState[i] = DateTime.Now;
res = true;
}
if (propertyNames[i] == "UtenteModifica")
{
currentState[i] = username;
res = true;
}
}
return res;
}
Dopo aver verificato che la entity corrente implementi l'interfaccia
IAuditable, scorriamo l'array propertyNames alla ricerca delle
proprietà "DataModifica" e "UtenteModifica". Una volta
trovate, modifichiamo il corrispondente valore nell'array currentState
ed impostiamo il valore da restituire a true. Così facendo, NHibernate
rifletterà tali modifiche tanto nell'oggetto in memoria che nella query che sarà
inviata al database. Una cosa importante da notare è che ciò è possibile
nonostante le proprietà di IAuditable siano marcate come Readonly: avendo
infatti scritto nel file di mapping
<hibernate-mapping ..... default-access="field.camelcase">
</hibernate-mapping>
sarà NH stesso a preoccuparsi di utilizzare la strategy selezionata e quindi
di utilizzare i corrispondenti fields.
Ovviamente l''interfaccia IInterceptor mette a disposizione parecchi altri
metodi per personalizzare il comportamento standard di NHibernate e plasmarlo
secondo le nostre esigenze; alcuni di essi sono molto interessanti, parlo ad
esempio di
- FindDirty, che
permette di determinare se una entity è da considerarsi dirty o meno, in modo
che NHibernate possa eventualmente effettuarne la query di update in fase di
flush;
- Instantiate,
che consente di costruire "a mano" una nuova istanza di un'oggetto dopo che i
dati ad esso associati siano stati recuperati dal database;
- IsUnsaved, che lascia all'utente la facoltà di
discriminare, in corrispondenza di un SaveOrUpdate, se una entity è
transiente o detached, in modo da eseguire rispettivamente una Save o una
Update.
Nel mio caso, proprio quest'ultimo è stato di notevole aiuto per
risolvere una problematica relativa all'utilizzo di chiavi primarie di tipo
assigned, per le quali cioé non è possibile stabilire un unsaved-value,
ma scriverò prossimamente un post specifico a riguardo. Purtroppo la
documentazione sull'interceptor è piuttosto scarna, sia nella reference di
NHibernate che su quella di Hibernate3. Per chi volesse dare un'occhiata
approfondita alle API di quest'interfaccia consiglio la Hibernate API
Documentation, relativa alla versione 3.1 per Java ma applicabile con un po'
d'intuito anche al mondo .NET.
Anche per quest'articolo, come al solito, ho preparato una piccola
applicazione d'esempio che può essere scaricata a questo link.
Alla prossima!
Update: come mi fa giustamente notare Tommaso, EmptyInterceptor non esiste su NHibenate 1.0.2 o, meglio, esiste nel namespace NHibernate.Cfg ma è internal. L'esempio che ho fornito compila perfettamente perché è basato sulla versione 1.2.0 Alpha. BTW, quanto detto in questo articolo continua a valere anche per le versioni precedenti di NHibernate, a patto che si implementi direttamente IInterceptor. Chi volesse realizzare una propria EmptyInterceptor può guardare i sorgenti di NHibernate per avere un'idea del codice (veramente banale) di questo implementor.
powered by IMHO 1.3