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

Creare relazioni con NHibernate

Come creare e gestire la persistenza delle relazioni tra entità
Come creare e gestire la persistenza delle relazioni tra entità
Link copiato negli appunti

NHibernate è un ORM, ovvero un sistema che ci permette di pensare alle informazioni memorizzate su database come a oggetti da gestire senza pensare al tipo di motore relazionale né alle interrogazioni.

Abbiamo già introdotto NHibernate in passato ed abbiamo già visto alcune realizzazioni con ASP.NET.

In questo articolo aggiungiamo alcuni elementi fondamentali sia alla trattazione, sia agli esempi. Parleremo di di come gestire il mapping delle relazioni e di come questo aspetto non possa prescindere da precise scelte che coinvolgono l'architettura del sistema.

Creare una relazione

Negli articoli già citati ci siamo occupati di come mappare semplici entità, ora osserviamo come come gestire le relazioni tra oggetti. Per semplicità riprendiamo l'esempio già proposto e aggiungiamo un nuovo oggetto chiamato ordine, in relazione con l'oggetto CustomerOne.

Figura 1. Relazione tra ordine e CustomerOne
Relazione tra l'entità ordine e l'oggetto CustomerOne

Nella notazione del Class Designer di Visual Studio, abbiamo definito, nella classe Order, una proprietà chiamata Customer che è di tipo CustomerOne.

La differenza con un normale dataset "strongly typed" è chiara, in quel caso avremmo inserito una semplice proprietà CustomerId, di tipo Int32, che conterrebbe l'id del cliente associato.

Ecco come si presenta il mapping di un ordine.

<class name="Order" table="Orders" lazy="false">
  
  <id name="id" unsaved-value="0" access="field" type="System.Int32">
    <generator class="native" />
  </id>

  <property name="Date" column="Date" type="System.DateTime"/>
  
  <many-to-one name="Customer"
               class="CustomerOne"
               column="CustomerId"
               not-found="exception"
               not-null="true" />
</class>

La particolarità è che la proprietà Customer viene mappata come many-to-one, ma d'altra parte questa non è una sorpresa, perché la relazione tra CustomerOne e Order è di tipo "molti a uno" in cui l'ordine è nella parte "molti".

Questa associazione, come una normale proprietà, possiede un set di attributi che permettono di specificarne il funzionamento. L'attributo class serve ad indicare a NHibernate il tipo di oggetto usato nella relazione e tramite column si indica la colonna usata per memorizzare la foreign-key.

NHibernate controlla se la classe usata per la relazione (ovvero CustomerOne) ha un id compatibile con il campo del db per vedere se la relazione è possibile. In questo caso CustomerOne ha una chiave di tipo Int32, la colonna CustomerId è intera per cui il mapping è compatibile con la struttura di database. Gli attributi not-found e not-null servono invece per specificare rispettivamente come comportarsi nel caso di una foreign-key orfana (id che non è presente in customer) e se possono esistere ordini orfani con la foreign-key pari a null.

Se si esegue Listato1 dell'esempio accluso, con il quale si inserisce semplicemente un ordine nel database, viene generata un'eccezione

Listato 1. Inserimento di un ordine

using (ISession session = NHSessionManager.GetSession() ) 
{
  CustomerOne gianmaria = new CustomerOne("Gian Maria", "Ricci");
  Order order1 = new Order(DateTime.Now, gianmaria);
  session.Save(order1);
  session.Flush();
}

Messaggio di errore

failed: not-null property references a null or 
        transient value: Domain.Entities.Order.Customer

Nel momento in cui l'oggetto order1 deve essere reso persistente grazie al metodo Save(), Nhibernate prepara la INSERT per la tabella orders (per il calcolo dell'id identity autogenerato dal DBMS), e per conoscere il valore del campo CustomerId, che rappresenta la foreign-key con la tabella Customer, esamina l'oggetto customer associato all'ordine.

A questo punto si verifica il problema, perché l'oggetto customer è ancora in stato transiente, dato che non è stato mai salvato con NHibernate. Questa situazione presenta due anomalie, in primo luogo NHibernate non conosce l'id dell'oggetto customer e non può quindi sapere cosa inserire nella colonna della foreign-key, in secondo luogo non è lecito salvare un entità quando esistono relazioni con altre entità che sono in stato transiente.

Le soluzioni possono essere due, la prima è chiamare Session.Save() anche sull'oggetto customer prima di salvare l'oggetto ordine, effettuandone quindi il passaggio nello stato persistente, la seconda è modificare il mapping di order in questo modo:

<many-to-one name="Customer" class="CustomerOne" column="CustomerId"
             not-found="exception" not-null="true" 
             cascade="save-update"/>

L'unico cambiamento è l'attributo cascade, che indica a NHibernate di percorrere la relazione propagando la persistenza agli oggetti correlati. Questa caratteristica nel mondo degli ORM e della programmazione con Domain Model si chiama: persistence by reachability, ovvero persistenza per raggiungimento. In pratica le operazioni che cambiano lo stato di persistenza di un oggetto vengono propagate percorrendo il grafo degli oggetti. In questo modo è possibile salvare un oggetto ed essere certi che anche tutti gli oggetti correlati vengano salvati in maniera automatica

Questa caratteristica è particolarmente importante se si utilizza la segmentazione in Aggregati [Evans].

Caricamento e Strategie di fetching

Vediamo ora cosa succede quando si recupera un ordine dal database. Supponendo che sia stato eseguito il Listato1, nel database esiste l'ordine con Id=1, che può essere recuperato con il metodo Get() della sessione.

Listato 2. Recuperare l'ordine dal DB

using (ISession session = NHSessionManager.GetSession())
{
  Order o = session.Get<Order>(1);
  Console.WriteLine("Order Id {0} customer name {1}", o.Id, o.Customer.Name);
}

La vera potenza di NHibernate si può capire dal fatto che, nonostante si sia chiesto il caricamento dell'oggetto ordine con id=1, si può accedere tranquillamente alla proprietà Customer e leggere i dati dell'oggetto correlato. La query che è stata generata è la seguente

NHibernate: SELECT order0_.id as id1_1_, order0_.Date as Date1_1_, order0_.CustomerId as CustomerId1_1_, customeron1_.id as id0_0_, customeron1_.Name as Name0_0_, customeron1_.Surname as Surname0_0_ 
            FROM Orders order0_ 
            	   INNER JOIN CustomerOne customeron1_ on order0_.CustomerId=customeron1_.id
            WHERE order0_.id=@p0; @p0 = '1'

Come si può osservare NHibernate effettua una query con join per recuperare i dati del cliente assieme a quelli dell'ordine. Questo comportamento però solleva un dubbio legittimo: Cosa accade se l'oggetto ordine ha molte relazioni? Cosa accade se l'oggetto Customer ha a sua volta altre relazioni? In pratica richiedendo un oggetto si porta in memoria tutto il grafo di oggetti da esso raggiungibile.

Chiaramente questa soluzione non è ottimale e per questa ragione NHibernate prevede modalità differenti per gestire il caricamento degli oggetti correlati.

Vedremo nella seconda parte dell'articolo come ottimizzare le query utilizzando metodologie come la modalità lazy.

Nella prima parte dell'articolo abbiamo osservato come, creare relazioni nel nostro modello, possa tradursi in un'inefficienza delle query. Vediamo ora quali strumenti NHibernate ci mette a disposizione per ottimizzare il colloquio col DBMS.

La modalità Lazy

Per prima cosa è necessario modificare la classe CustomerOne ed il suo mapping. La prima operazione da fare è mettere a true l'attributo lazy.

<class name="CustomerOne" table="CustomerOne" lazy="true" ...

In questo modo si indica a NHibernate che l'entità può essere usata in modalità lazy (Pigra). Se si esegue nuovamente il Listato2 viene però generato un errore, dovuto al fatto che la classe CustomerOne non può essere usata in modalità lazy perché non ha tutte le proprietà ed i metodi pubblici dichiarati come virtuali.

Per comprendere la ragione di questa richiesta è doveroso capire come viene implementato il caricamento lazy.

Il termine "Lazy" indica un'operazione che viene effettuata solo quando strettamente necessaria. Nel caso di "lazy load" (caricamento pigro), si effettua la query per recuperare i dati solamente quando si accede ad una proprietà e non prima.

Chiaramente NHibernate deve avere un modo per intercettare quando si utilizza per la prima volta la proprietà di un oggetto e per questa ragione, al momento del caricamento dell'oggetto Order, il sistema non assegna un vero oggetto CustomerOne alla sua proprietà Customer, ma un istanza di una classe creata dinamicamente, che eredita da CustomerOne ed implementa il pattern proxy.

Data una classe X, un suo proxy non è altro che un'istanza di una classe Y che eredita da X e che al suo interno mantiene una istanza di X a cui delega tutte le chiamate fatte dall'esterno. Grazie a questo pattern è possibile associare in maniera trasparente funzionalità aggiuntive ad un oggetto esistente.

Figura 2. Il proxy sostituisce il vero customer fin quando è possibile
Il proxy sostituisce il vero customer fin quando è possibile

NHibernate, all'atto del caricamento dell'oggetto Order, effettua una SELECT sulla sola tabella order, da questa tabella recupera l'id dell'oggetto customer correlato (dalla colonna con la foreign-key), istanza un proxy di CustomerOne e gli assegna l'id trovato. Questo proxy internamente ha una variabile di tipo CustomerOne pari a null, ogni getter di ogni proprietà virtuale ha al suo interno un controllo che verifica appunto se questa variabile è null ed in caso affermativo la istanzia, previo caricamento dei dati dalla tabella Customer.

Il risultato è che i dati vengono caricati dal database solo quando si accede la prima volta ad una proprietà dell'oggetto proxy. (Se si è interessati ai dettagli di implementazione delle tecniche di caricamento lazy si può consultare il POEAA [Fowler])

Se l'oggetto Customer non avesse tutte le proprietà pubbliche virtuali, non si potrebbe creare l'oggetto proxy per ereditarietà dato che non si potrebbe intercettare con il metodo getter l'accesso alle proprietà stesse.

Non rimane altro da fare che rendere tutte le proprietà di CustomerOne virtuali ed eseguire Listato 2.

Se si osserva il codice SQL generato si nota che non viene più eseguita un'unica query con JOIN, ma due query SELECT, una sulla tabella orders ed una su CustomerOne. A questo punto è fondamentale usare il debugger ed eseguire una per una le istruzioni del Listato3.

Listato 3. Eseguire il Fetch di un ordine

Order o = session.Get<Order>(1);
Console.WriteLine(o.Customer.GetType().Name);
Console.WriteLine("Order Id {0} customer name {1}", o.Id, o.Customer.Name); 

Quando viene eseguita la prima istruzione viene effettuata la prima SELECT per recuperare i dati dalla sola tabella order. Quando si esegue la seconda istruzione si può osservare che la proprietà Customer dell'oggetto order contiene un oggetto di tipo:

CProxyTypeDomain_EntitiesCustomerOneEntities_NHibernate_ProxyINHibernateProxy1

Come detto precedentemente, per implementare il lazy load, NHibernate ha inserito un proxy al posto del vero oggetto Customer, ma dato che questo proxy eredita da Customer dal punto di vista dell'utilizzatore non cambia nulla. Quando si esegue la terza istruzione viene finalmente effettuata la SELECT per la tabella Customer e da questo momento in poi i dati sono in memoria.

Questa tecnica, conosciuta come Transparent Lazy Load, è una delle strategie di fetch possibile e probabilmente la più utile. Grazie ad essa si può senza problemi navigare un grafo di oggetti in maniera completamente naturale, lasciando a NHIbernate il compito di caricare i dati quando necessario.

Eager Fetch

Naturalmente il Lazy Load non è la panacea di tutti i mali, infatti in questo modo l'utilizzo del database è talvolta particolarmente inefficiente; esaminiamo quindi cosa succede nel Listato4.

Listato 4. Una query effettuata con NHibernate

IList<Order> orders = session.CreateQuery("from Order").List<Order>();

foreach(Order o in orders)
  Console.WriteLine("Order Id {0} customer name {1}", o.Id, o.Customer.Name);

In questo esempio abbiamo usato una query HQL, uno dei metodi che offre NHibernate per effettuare query sul Domain Model. La convenienza di usare HQL è che ha una sintassi molto simile a SQL, ma si usano i nomi di classi e proprietà, in questo modo si è completamente scollegati dallo schema del database, che è invece espresso solamente tramite i mapping.

Nel listato la query from Order non fa altro che selezionare tutti gli oggetti Order presenti nel db. Se si controlla il codice SQL generato si può vedere che il numero di query SELECT effettuate è N+1, dove N è il numero di oggetti orders. Questo comportamento è naturale, la prima query infatti recupera tutti gli ordini, poi ogni volta che si accede alla proprietà Customer di un ordine viene effettuata una ulteriore query per recuperare i dati di quel particolare Customer, d'altra parte è proprio questa la caratteristica del caricamento lazy.

Se si vuole essere più precisi bisogna osservare che, grazie alla Identity Map, una volta che un oggetto proxy è stato caricato, NHibernate utilizzerà sempre quello e non ne genererà altri, questo significa che in realtà il numero N indicato non è pari al numero degli ordini, ma è piuttosto il numero di clienti distinti per gli ordini caricati.

In questa situazione il programmatore sa che le operazioni da effettuare prevedono l'accesso alla proprietà Customer per tutti gli ordini, per cui la strategia di Lazy Load è controproducente in termini di prestazioni, dato che viene eseguito un numero elevato di interrogazioni al database. Anche in questo caso NHibernate ha la soluzione, basta cambiare la query in questo modo

"from Order o inner join fetch o.Customer"

Grazie alla clausola inner join fetch si chiede a NHibernate di recuperare tutti i dati con una join, proprio come se il lazy load fosse stato disattivato. Questa strategia di fetch, in cui si recuperano tutti i dati con una singola interrogazione al db, viene chiamata Eager Load, ovvero caricamento anticipato, ed è utile in tutti quei casi in cui si sa già in anticipo come verrà usato il grafo di oggetti.

Nel Listato 5 si può notare che grazie all'eager load non vengono nemmeno usati gli oggetti proxy. Dato che Nhibernate ha già tutti i dati necessari dalla tabella Customer è preferibile infatti creare un vero oggetto invece di sacrificare le performance con l'uso di proxy.

Da quanto detto emerge quindi che è conveniente che gli oggetti siano tutti mappati come lazy, in questo modo si può poi scegliere al momento della query se usare un caricamento lazy oppure eager per i vari rami del grafo di oggetti che si vuole usare.

Creazione di proxy espliciti

L'oggetto NHibernate.ISession presenta due metodi distinti per recuperare entità dal loro id: Get() e Load(). Il metodo Get(), utilizzato fino ad ora, recupera l'oggetto dal database e restituisce null se non esiste nessun record con l'id specificato, il metodo Load() ritorna invece un proxy senza eseguire nessuna query; in questo caso se non è presente un recrod con l'id specificato viene generata eccezione al momento del primo accesso ad una proprietà.

Il metodo Load() permette quindi la creazione di un proxy ed è utile per creare associazioni; supponiamo di voler associare il cliente con id 12 ad un ordine, in questo caso utilizzando la chiamata Session.Load<CustomerOne>(12) si crea un proxy che può essere assegnato alla proprietà Customer dell'ordine in questione. Il vantaggio di questo approccio è che il proxy permette di impostare la relazione senza dover veramente caricare l'oggetto dal database.

Conclusioni

NHibernate mostra la sua vera flessibilità quando entrano in gioco le relazioni tra oggetti. Grazie alla possibilità di usare differenti strategie di fetching, è infatti possibile gestire la persistenza di un grafo complesso di oggetti in maniera semplice, ma senza sacrificare le prestazioni.

Il lazy load in particolare è molto utile perché permette di navigare il grafo caricando gli oggetti solo al momento del primo utilizzo. L'eager fetch al contrario è utile quando si sa in anticipo che si utilizzeranno certe relazioni del grafo ed è quindi conveniente portare subito tutti gli oggetti in memoria.

Riferimenti

  • [EVANS]: Domain Driven Design: Tackling complexity in the heart of software (2004)
  • [Fowler]: Pattern of Enterprise application Architecture (2002)

Ti consigliamo anche