Poco tempo fa ho parlato di strategie di generazione del document identifier con NHibernate, quello che vogliamo ottenere è rendere trasparente all’utilizzatore il fatto che un “id”, ad esempio il numero fattura, debba essere generato e dipenda da fattori esterni all’entità.
In soldoni vogliamo limitarci ad una cosa del genere:
using( var dc = this.dataContextFactory.Create() )
{
using( var tx = dc.BegingTransaction( IsolationLevel.Serializable ) )
{
dc.Insert( myEntity );
tx.Commit();
}
}
nel post di cui sopra introducevo un elegante soluzione proposta da Fabio Maulo, oggi mi sono trovato davanti alla prima vera necessità e nell’ottica dello “sviluppo sostenibile” mi sono detto devi mettere il minor effort possibile per raggiungere il risultato desiderato, risultato che è:
- generare un identificatore univoco, non una PK okkio, ma un identificatore univoco per l’utente che potrebbe anche essere una PK o Unique ma non è un vincolo che lo sia;
- “nascondere”, per una serie di buoni motivi su cui non mi dilungo, questa logica di generazione;
Se avessi potuto mettere mano al db il tutto probabilmente sarebbe finito in una Stored Procedure e/o una sana User Defined Function… ma non posso mattere mano al db :-/
Event Listeners
Il tool è fantastico e ha un livello di pluggabilità abbastanza invidsioso :-), recentemente sono stati infatti introdotti i così detti NHibernate Events (prima c’erano, e ci sono ancora, gli interceptor, ma sono un filino più ostici da usare).
NHibernate espone una serie di “eventi”, non nel senso del tradizionale evento .Net, che possiamo intercettare; lo scopo quindi è quello di inserirsi nella pipeline di insert e fare li il lavoro sporco:
container.Register( Component.For<IIdentityGenerationService<MyEntity>>()
.ImplementedBy<MyEntityIdentityGenerationService>() );
container.Register( Component.For<MyEntityPreInsertEventListener>() );
var configuration = container.Resolve<Configuration>();
configuration.AddListener( e => e.PreInsertEventListeners, this.container.Resolve<MyEntityPreInsertEventListener>() );
Setup:
- registriamo, in Castle, “qualcuno” che sia in grado di generare una nuova “identità”;
- registriamo anche il “listener” al fine di, senza saper ne leggere ne scrivere, iniettare le eventuali dipendenze;
- configuariamo NHibernate aggiungendo il nuovo listener, pescandolo dal framework di IoC;
Identità (che è anche un bel film :-))
public interface IHaveIdentity
{
}
public interface IHaveIdentity<T> : IHaveIdentity
{
T Identity { get; }
}
la prima interfaccia sostanzialmente ci servirà da “marker interface”, definiamo poi il tizio che sa fare il lavoro sporco:
public interface IIdentityGenerationService
{
Object GenerateIdentity( IHaveIdentity entity, ISession session );
}
public interface IIdentityGenerationService<TEntity> : IIdentityGenerationService
{
}
In questo caso invece la seconda interfaccia ci serve per differenziare/univocizzare (teribbbbile :-P) la registrazione in Castle.
IPreInsertEventListener
finalmente :-)
public class MyEntityPreInsertEventListener : IPreInsertEventListener
{
readonly IServiceProvider container;
public MyEntityPreInsertEventListener( IServiceProvider container )
{
this.container = container;
}
public bool OnPreInsert( PreInsertEvent evt )
{
var ihi = evt.Entity as IHaveIdentity;
if( ihi != null )
{
var entityType = evt.Entity.GetType();
var svcType = typeof( IIdentityGenerationService<> ).MakeGenericType( entityType );
var svc = ( IIdentityGenerationService )this.container.GetService( svcType );
using( var innerSession = evt.Session.GetSession( evt.Session.EntityMode ) )
{
var newIdentity = svc.GenerateIdentity( ihi, innerSession );
var rh = ReflectionHelper.BoundTo<MyEntity>();
rh.GetProperty( e => e.Identity )
.SetValue( ihi, newIdentity, null );
int index = Array.IndexOf( evt.Persister.PropertyNames, rh.NameOf( e => e.Identity ) );
if( index != -1 )
{
evt.State[ index ] = newIdentity;
}
}
}
return false;
}
}
Urka, quanta robaccia :-), andiamo per gradi:
- abbiamo bisogno di intercettare solo le insert e abbiamo bisogno di essere notificati prima della insert;
- verifichiamo se l’entità che stiamo per inserire ci interessa: IHaveIdentity è li apposta :-)
- andiamo a recuperare il servizio di generazione delle identità;
- apriamo una nuova sessione, questo è molto importante perchè se riutilizzate la vostra finite in un loop infinito che porta ad una inevitabile StackOverflowException, e lo facciamo con IEventSource.GetSession( … ) in questo modo la child session si porta dietro tutto della parent session: stessa connection e stessa transazione;
- chiediamo al servizio di generare una nuova identità;
- via reflection, ricordate il minor effort possibile, settiamo il nuovo valore;
- dobbiamo ricordarci anche di aggiornare il valore dei parametri di NH che stanno viaggiando verso il db altrimenti l’entità è aggiornata ma il db non è consistente… non è vero… :-) funzionare funziona ma succede una cosa molto curiosa:
- nh fa la insert…
- si accorge che la entity però è diversa;
- subito dapo la insert fa un update…questo è poco bello perchè fa una operazione inutile ed evitabile aggiornando i valori dei parametri;
- alla fine ritorniamo “false” e questo concedetemelo ma fa veramente brutto… :-) ritorniamo false perchè alcuni listner, come quello che usiamo noi, possono “votare” se l’operazione deve essere eseguita o meno… falso: in realtà possono votare se l’operazione deve essere abortita o meno, ecco perchè false, ma secondo me è decisamente contro natura il ragionamento da fare;
Tralascio l’implementazione del servizio di generazione delle identità che si limita ad usare la session per eseguire una “query” (con ICriteria) e recuperare le informazioni che servono per generare una nuova identità, triviale insomma.
Il tutto è adesso completamente trasparente, che era esattamente quello che volevamo, e il codice di cui sopra funziona come ci aspettiamo e il fido NHProof ce lo conferma.
.m