NHibernate: IPreInsertEventListener
In soldoni vogliamo limitarci ad una cosa del genere:
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 è:using( var dc = this.dataContextFactory.Create() ) { using( var tx = dc.BegingTransaction( IsolationLevel.Serializable ) ) { dc.Insert( myEntity ); tx.Commit(); } }
- 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;
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:
Setup: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>() );
- 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;
la prima interfaccia sostanzialmente ci servirà da “marker interface”, definiamo poi il tizio che sa fare il lavoro sporco:public interface IHaveIdentity { } public interface IHaveIdentity: IHaveIdentity { T Identity { get; } }
In questo caso invece la seconda interfaccia ci serve per differenziare/univocizzare (teribbbbile :-P) la registrazione in Castle.public interface IIdentityGenerationService { Object GenerateIdentity( IHaveIdentity entity, ISession session ); } public interface IIdentityGenerationService: IIdentityGenerationService { }
IPreInsertEventListener
finalmente :-)
Urka, quanta robaccia :-), andiamo per gradi: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; } }
- 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;
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