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 è:
  1. 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;
  2. “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:
  1. registriamo, in Castle, “qualcuno” che sia in grado di generare una nuova “identità”;
  2. registriamo anche il “listener” al fine di, senza saper ne leggere ne scrivere, iniettare le eventuali dipendenze;
  3. configuariamo NHibernate aggiungendo il nuovo listener, pescandolo dal framework di IoC;
Identità (che è anche un bel film :-))
public interface IHaveIdentity
{

}

public interface IHaveIdentity : 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 : 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