Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

NHibernate e ASP.NET

Update degli oggetti e ciclo di vita della persistenza
Update degli oggetti e ciclo di vita della persistenza
Link copiato negli appunti

In un precedente articolo abbiamo visto come configurare NHibernate e come scrivere un mapping minimale; si sono apprese inoltre le basi del concetto di Persistenza e di ORM salvando alcuni semplicic oggetti in vari tipi di database. In questo articolo focalizzeremo invece l'attenzione sul concetto di "ciclo di vita" e soprattutto sulla gestione dell'oggetto NHibernate.Session.

Prima di procedere è necessario fornire i rudimenti sul caricamento degli oggetti da database, operazione che può essere fatta essenzialmente in tre modalità differenti: la prima consiste nel recuperare una istanza specifica conoscendo la sua chiave primaria [Session.Get(), Session.Load()], le altre due invece permettono di fare query complesse con condizioni, aggregazioni, join, etc.

L'aspetto interessante è che una volta che l'oggetto è stato recuperato dal database, ogni modifica alle sue proprietà viene propagata automaticamente, mantenendo quindi sempre sincronizzato l'oggetto con i dati presenti nelle tabelle correlate.

Questo comportamento è ciò che rende NHibernate veramente differente dal normale uso di ADO.Net: sebbene la sessione possieda un metodo Session.Update(), una volta che un oggetto è stato recuperato dalla sessione la sua persistenza viene gestita automaticamente.

Il metodo Session.Update() sembrerebbe quindi superfluo, ma è invece necessario per rendere nuovamente persistente un oggetto che è in stato Detatched. Cerchiamo di chiarire il funzionamento utilizzando uno schema.

Figura 1. Ciclo di vita degli oggetti per Nhibernate
Ciclo di vita degli oggetti per Nhibernate

Un'entità che non è mai stata salvata con un oggetto Session viene detta transiente, il suo stato non verrà mai propagato al DB e NHibernate la ignora completamente.

Una volta chiamati i metodi Save() o SaveOrUpdate() l'entità diviene persistente e da questo momento tutte le modifiche che verranno fatte alle sue proprietà mappate verranno automaticamente propagate al database.

Quando la sessione viene rilasciata chiamando Dispose(), Close(), oppure se si utilizza il metodo Session.Evict(Object), l'entità assume lo stato detatched. Questo particolare stato indica un oggetto che è stato precedentemente persistente, ma che ora è scollegato da qualsiasi sessione attiva; in questo stato quindi nessuna modifica verrà propagata al database. L'oggetto può essere comunque riportato nello stato persistente semplicemente tramite i metodi Lock(), Update() o SaveOrUpdate() dell'oggetto Session.

Il concetto importante è che tutti gli oggetti recuperati tramite Get(), Load(), HQL o ICriteria (questi ultimi due verranno visti successivamente) si trovano nello stato persistente.

Per comprendere meglio questi concetti è interessante eseguire gli esempi del progetto accluso che mostrano alcune situazioni comuni.

Esaminiamo un metodo statico (Listato1()) che esegue un inserimento su db di una entità. Subito dopo l'inserimento cambia valore ad una proprietà (Name) ed effettua il flush della sessione. Dal log si può vedere che le modifiche vengono propagate al db.

private static void Listato1()
{
  CustomerOne customer = new CustomerOne("Gian Maria", "Ricci");
  
  using (ISession session = NHSessionManager.GetSession() ) {
    session.SaveOrUpdate(customer);
    customer.Name += "-Changed";
    session.Flush();
  }
}

Questo dimostra come una entità, una volta salvata dalla sessione è in stato persistente fino a che la sessione è attiva.

Osserviamo anche un secondo esempio altrettanto interessante, in cui prima viene creata e salvata un'entità nel DB (InsertACustomer()), poi viene rilasciata la prima sessione con un Dispose(). A questo punto l'entità è in stato Detatched.

private static void Listato2()
{
  CustomerOne customer = InsertACustomer();
  
  // non effettuiamo modifiche
  // customer.Name += "changed"; 
	
  using (ISession session = NHSessionManager.GetSession())
  {
    session.SaveOrUpdate(customer);
    session.Flush();
  }
}

private static CustomerOne InsertACustomer()
{
  CustomerOne customer = new CustomerOne("Gian Maria", "Ricci");
  
  using (ISession session = NHSessionManager.GetSession()) 
  {
    session.SaveOrUpdate(customer);
    session.Flush();
  } // Dispose() implicito
  
  return customer;
}

Successivamente viene creata una nuova sessione e si rende l'oggetto nuovamente persistente chiamando il metodo SaveOrUpdate().

Ciò che è importante notare è che, sebbene l'oggetto non sia stato modificato, viene comunque generata una istruzione di update nel database. Per capire perché NHibernate generi questo update inutile, è necessario conoscere qualche dettaglio in più su come sono gestite internamente le operazioni di persistenza.

Per capire quali oggetti siano effettivamente modificati tra tutti quelli persistenti, la sessione mantiene internamente una copia di ogni entità persistente. L'insieme di tutti questi oggetti va sotto il nome di Persistence Context, e non è altro che una cache di oggetti grazie alla quale la sessione può effettuare le operazioni di Dirty Checking, ovvero controllare quali proprietà sono state modificate e generare le query di update. Il contesto non è un entità con cui si interagisce direttamente, ma conoscere la sua esistenza è fondamentale per capire il comportamento di NHibernate e per evitare sorprese.

Adesso forse capiamo meglio perché NHibernate generi l'istruzione di update superflua in Listato2(): quando viene chiamato il metodo SaveOrUpdate() sul nuovo oggetto sessione, NHibernate esamina il Persistence Context e vede che quell'entità non è presente, a questo punto la sessione non può far altro che supporre che tutte le proprietà siano cambiate.

È molto meglio fare una update inutile, piuttosto che non farne una necessaria e perdere le modifiche. D'altra parte l'oggetto, durante il periodo in cui è in stato Detatched, può essere stato modificato e se si invoca la SaveOrUpdate() è chiaro che il chiamante sta chiedendo di propagare ogni possibile modifica al database.

NHibernate permette di gestire anche la situazione in cui il chiamante è sicuro che nessuna modifica sia stata mai fatta all'oggetto mentre era in stato detatched. Chiamando il metodo Lock(), invece del normale SaveOrUpdate(), l'entità viene portata nuovamente nello stato persistente, assicurando nel contempo alla sessione che l'oggetto non ha subito cambiamenti. Questo metodo è differente dal SaveOrUpdate(), anche perché necessita di un secondo parametro di tipo LockMode per indicare il livello di "lock" che si vuole imporre nel database.

private static void Listato3()
{
  CustomerOne customer = InsertACustomer();
  
  // Attenzione: questa modifica non verrebbe registrata
  // customer.Name += "changed"; 
			
  using (ISession session = NHSessionManager.GetSession())
  {
    session.Lock(customer, LockMode.Read);
    customer.Name += "changed";
    session.Flush();
  }
}

Nell'esempio è stato passato il valore LockMode.Read, in questo caso tutte le modifiche che sono state fatte all'entità durante lo stato detatched vengono perse, questo perché il metodo Lock() non fa altro che creare nel contesto una copia dell'oggetto nello stato corrente. Gli altri valori dell'enum LockMode sono utili per gestire la concorrenza e per ora si possono ignorare.

Il consiglio personale è quello di usare sempre SaveOrUpdate(), in questo modo si corre il rischio di fare qualche update inutile nel db, ma si è sicuri che nessuna modifica vada perduta.

Ora che si conoscono alcuni dettagli su come la sessione gestisce internamente la persistenza degli oggetti, è venuto il momento di esaminare come utilizzare efficacemente NHibernate in un progetto Web ASP.NET, ma prima di tutto è necessario spiegare a grandi linee il concetto di Unit Of Work.

Il pattern UnitOfWork risolve il problema della gestione di insiemi di operazioni effettuate in un'unica unità concettuale da gestire transazionalmente. La definizione data da fowler nel PoEAA è:

«Mantiene una lista di oggetti che fanno parte di una transazione di business e coordina la scrittura dei cambiamenti e la risoluzione dei problemi di concorrenza.»

Questo pattern viene solitamente implementato creando un oggetto che possieda le funzioni di Add(), Create(), Delete() etc. ed i metodi per effettuare la propagazione o l'annullamento in blocco delle operazioni impostate (Commit(), Rollback()).

Figura 2. Pattern Unit Of Work
Pattern Unit Of Work - definizione di Martin Flower

A questo punto il vostro intuito dovrebbe già avervi fatto notare che l'oggetto Session di NHibernate implementa questo pattern. A tutti gli effetti quindi la session è in grado di gestire una Unit Of Work.

La prima difficoltà che si incontra quando si lavora con NHibernate è decidere quando creare e distruggere la sessione, ovvero decidere l'ampiezza di una Unit Of Work. Per capire meglio le origini di questo problema, si pensi all'oggetto connessione standard di ADO.NET (DbConnection), il cui ciclo di vita è solitamente legato alla transazione, ma non è infrequente in un layer di accesso dati creare una nuova connessione ad ogni operazione.

Questo può essere fatto perché ADO.Net internamente utilizza un pool di connessioni ed evita quindi di ricreare ogni volta la connessione fisica al database, in questo modo le operazioni di creazione della connessione, solitamente abbastanza onerose in termini di tempo, sono invece molto veloci. Questo scenario non presenta quindi problemi, ogni qualvolta si vuole interagire con il database si crea una connessione, si effettua la query e la si chiude immediatamente per evitare leak.

Molto differente è invece la situazione in cui si debbano gestire un insieme di operazioni in modo transazionale, in questo caso infatti il problema maggiore è decidere dove iniziare e terminare la transazione senza complicare troppo il codice e soprattutto centralizzando la gestione in modo da portela cambiare se quella attuale si rivelasse inadatta.

Per l'oggetto sessione di NHibernate il discorso è analogo, si può infatti creare una sessione per ogni operazione e chiuderla immediatamente, ma questo modo di procedere è veramente poco efficiente; in un simile scenario infatti gli oggetti sono quasi sempre nello stato Detatched. La vera potenza di un ORM invece si percepisce quando il codice non deve assolutamente preoccuparsi delle operazioni di persistenza, nel codice si chiamano metodi e si cambiano le proprietà delle entità demandando ad un componente esterno la gestione della Unit Of Work.

Se qualcuno sta pensando di creare un'unica sessione per tutto il progetto, rimarrà deluso perché questo approccio non è perseguibile. In primo luogo la sessione non è Thread-Safe e quindi non si può utilizzare in ambienti multithreading come ASP.NET, in secondo luogo il Persistence Context tende a crescere troppo perché si tengono in memoria contemporaneamente tutte le entità persistenti.

Fortunatamente in ambiente Web la soluzione è più semplice del previsto, una pagina ASP.NET ha infatti un ciclo di vita ben definito che va dalla richiesta all'IIS, all'individuazione dell'handler corrispondente fino alla generazione dello stream di risposta. In questo scenario è naturale quindi che la Unit Of Work corrisponda alla Request di ASP.NET, portando cosi al pattern Session per Request.

Session Per Request

Nell'esempio accluso è presente una prima implementazione di Unit Of Work che internamente si appoggia all'oggetto System.Runtime.Remoting.Messaging.CallContext. Questo particolare oggetto infatti è quello usato internamente da HttpContext e permette di memorizzare informazioni associandole al percorso di esecuzione corrente. Il pattern Unit Of Work è quindi implementato da un HttpModule che elabora alcuni eventi standard del ciclo di vita della pagina ASP.NET:

In primo luogo la creazione della Sessione è Lazy, ovvero nel metodo GetCurrentSession() si controlla se nel CallContext è presente un oggetto Session e lo si crea se necessario. Questo significa che se nella pagina nessuno chiede mai una session essa non viene creata risparmiando risorse.

Nell'evento EndRequest viene poi verificato se è stata aperta una sessione nhibernate per la richiesta corrente, in caso affermativo si controlla se la Unit Of Work è Dirty, ovvero se il chiamante ha in qualche modo chiesto l'annullamento delle operazioni. In base a queste informazioni si effettua il rollback o la commit della transazione associata alla sessione e si chiama il dispose della sessione per evitare leak.

Nell'evento Error invece non si fa altro che marcare la Unit Of Work corrente come Dirty, dato che ogni eccezione non gestita molto probabilmente porta ad avere dei dati che potrebbero essere non consistenti.

Questo gestore può essere quindi usato per tenere traccia del concetto di Sessione corrente in maniera completamente trasparente al codice della pagina. Armati di questa nuova conoscenza si può ora procedere a realizzare una pagina banale che crea e modifica un oggetto di tipo CustomerOne, la pagina contiene due textbox ed un bottone, ed il code behind è il seguente

protected void Page_Load(object sender, EventArgs e) {
  CustomerOne c = null;
  
  if (IsInUpdateMode()) {
    c = UnitOfWork.GetCurrentSession().Get<CustomerOne>(
    Int32.Parse(Request.QueryString["CustomerOneId"]));
  }
  else {
    c = new CustomerOne();
  }
  if (!IsPostBack) {
    txtName.Text = c.Name;
    txtSurname.Text = c.Surname;
  }
  else {
    c.Name = txtName.Text;
    c.Surname = txtSurname.Text;
    if (!IsInUpdateMode()) {
      UnitOfWork.GetCurrentSession().SaveOrUpdate(c);
      Response.Redirect(Request.Url + "?CustomerOneId=" + c.Id );
    }
  }
}

La complessità maggiore deriva dal fatto che questa pagina gestisce sia l'inserimento sia la modifica, si aspetta quindi di trovare nella querystring, per la modifica, un parametro di tipo CustomerOneId che indica l'id dell'entità da modificare, se quest'ultimo non è presente si vuole allora un inserimento. Nella prima parte della funzione si controlla se è presente il parametro, in caso affermativo si recupera l'entità con un Session.Get, nella seconda parte si controlla invece se si è in GET o in POST.

Nel primo caso infatti si è nella prima chiamata alla pagina, per cui è necessario aggiornare i valori dei controlli con il contenuto della entità, mentre se si è in post si deve eseguire l'operazione inversa, ovvero aggiornare l'entità con i nuovi valori inseriti dall'utente. Come si può vedere l'interazione con la sessione è minima, e nel caso di aggiornamento non si deve assolutamente fare nulla, basta cambiare le proprietà dell'entità ed il gioco è fatto.

Conclusioni

Grazie ad un modulo di gestione della Unit Of Work e ad un ORM come NHibernate, l'interazione con il database diviene quasi trasparente e si limita solo all'inserimento delle nuove entità ed al recupero delle entità salvate. Questo approccio inoltre permette di svincolare il codice dalla struttura e dal tipo di database, dato che tutta l'interazione viene lasciata a NHibernate tramite la Sessione ed i file di mapping.


Ti consigliamo anche