Il pattern LazyLoad è quasi sempre utilizzato in
NHibernate per il caricamento delle collection di childs in una relazione
Master/Detail. Non tutti sanno invece che è possibile applicare questo pattern a
tutti i tipi di relazioni, potendo così ottimizzare di molto la dimensione delle
query che vengono effettuate su DB e la mole di dati che viene recuperata.
Capita spesso di avere classi con parecchie
relazioni Many-To-One, vuoi perché si tratta di classi "dettaglio"
di qualcosa, vuoi perché se
creo la classe Articolo magari questa espone una proprietà Tipo di tipo
(scusate il gioco di parole) TipoArticolo, ecc.ecc... Se non si utilizza il
LazyLoad anche per questo tipo di relazioni, NHibernate effettua query con un gran numero
di Join che, come sappiamo, possono minare le performance delle
applicazioni.
Ho scritto una piccola applicazione di esempio,
scaricabile a questo link ,
che persiste sul DB un Articolo e un TipoArticolo e poi ne forza una lettura.
Analizziamo il mapping di Articolo:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0" default-lazy="false"
default-access="field.camelcase" >
<class name="NHLazyTest.Articolo, NHLazyTest" table="Articoli">
<!-- .... un po' di cose qui .... -->
<many-to-one name="Tipo" class="NHLazyTest.TipoArticolo, NHLazyTest" />
</class>
</hibernate-mapping>
Come si vede, c'è un riferimento many-to-one
alla classe TipoArticolo, che a sua volta ha il seguente mapping:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0"
default-lazy="false" default-access="field.camelcase" >
<class name="NHLazyTest.TipoArticolo, NHLazyTest" table="TipiArticolo" >
<!-- .... un po' di cose qui .... -->
</class>
</hibernate-mapping>
Non è che ci sia molto da commentare, se non quel
default-lazy=false per modificare il comportamento
standard di NHibernate 1.2 che tende a caricare tutte le entity in LazyLoad e
che, nel caso si utilizzi una versione precedente, è perfettamente inutile!
Se eseguiamo una lettura di un articolo dal
DataBase, tramite il seguente snippet di codice
using (ISession session = SessionHelper.GetSession())
{
articolo = session.Get<Articolo>(id);
}
il risultato sarà che NHibernate proverà
a recuperare tutti i dati da DB con una sola query, che sarà simile alla
seguente:
exec sp_executesql N'SELECT articolo0_.ID_Articolo as ID1_1_, articolo0_.Tipo as Tipo1_1_,
articolo0_.Descrizione as Descrizi2_1_1_, tipoartico1_.ID_TipoArticolo as ID1_0_,
tipoartico1_.Descrizione as Descrizi2_0_0_ FROM Articoli articolo0_
left outer join TipiArticolo tipoartico1_ on articolo0_.Tipo=tipoartico1_.ID_TipoArticolo WHERE
articolo0_.ID_Articolo=@p0',N'@p0 uniqueidentifier',@p0='B3D1E0B6-295D-452E-A834-4598052EA9C6'
Tutto ciò va benone nel caso in cui le
entity coinvolte siano poche, ma se Articolo avesse altre relazioni il calo di
performance potrebbe essere pesante, intanto perché parecchi join non fanno bene
alla velocità e poi perché magari stiamo recuperando delle informazioni che in
un certo contesto non ci interessano affatto.
Cosa fare allora? Per prima cosa modifichiamo
leggermente il mapping di TipoArticolo per dire a NHibernate che tutte le
istanze di TipoArticolo dovranno supportare il LazyLoad:
<class name="NHLazyTest.TipoArticolo, NHLazyTest"
table="TipiArticolo" lazy="true">
Ora
chiediamoci: come viene materialmente realizzato un LazyLoad? Beh, si crea un
proxy della classe che, al primo accesso ad uno qualsiasi dei suoi membri,
effettua il vero e proprio caricamento. Affinché però tutto funzioni, è
necessario
- che il tipo in oggetto abbia proprietà e metodi
dichiarati virtual o, in alternativa,
- utilizzare un'interfaccia in luogo di un tipo specifico, alla stessa stregua
di come accade per le collection.
Nel primo caso non dobbiamo far altro che adattare
un po' il codice del domain, pena un'eccezione in fase di creazione della
SessionFactory, nel secondo possiamo utilizzare l'attributo proxy per
specificare l'interfaccia da utilizzare. In entrambi i casi, comunque, una volta
ricompilato il programma, eseguendo lo stesso snippet di prima, NHibernate
esegue sul DB una select parecchio più semplice che, come si nota, coinvolge la
sola tabella Articoli:
exec sp_executesql N'SELECT articolo0_.ID_Articolo as ID1_0_, articolo0_.Tipo as Tipo1_0_,
articolo0_.Descrizione as Descrizi2_1_0_ FROM Articoli articolo0_ WHERE
articolo0_.ID_Articolo=@p0',N'@p0 uniqueidentifier',@p0='A460678E-1611-4394-A7BA-236F6F68DBBA'
Indagando meglio, si può notare che il TipoArticolo
ritornato è un'istanza di una classe il cui nome è all'incircaCProxyTypeTipoArticoloNHLazyTest_INHibernateProxy1
e che, all'invocazione di uno qualsiasi dei suoi membri, carica il vero e proprio
TipoArticolo, purché l'oggetto sia persistente, cioé collegato ad una
Session.
Due ultime considerazioni: innanzi
tutto va da sé che tutto ciò è perfettamente integrato
con il sistema di caching di NHibernate, il che vuol
dire che il seguente snippet di codice
using (ISession session = SessionHelper.GetSession())
{
// carico i due articoli dello stesso tipo
articolo = session.Get<Articolo>(id);
articolo1 = session.Get<Articolo>(id1);
// questa forza l'eventuale lazy load del TipoArticolo corrispondente
string s = articolo.Tipo.Descrizione;
// questo non dovrebbe eseguire un'altra query perché TipoArticolo
// è già stato caricato dalla session (cache di primo livello)
string s1 = articolo1.Tipo.Descrizione;
}
per recuperare l'istanza di TipoArticolo
esegue una sola query sul DB, perché, come scritto nei
commenti, al fetch su articolo1 l'engine si
accorge che il medesimo TipoArticolo è già presente nella cache di primo
livello, avendolo caricato in seguito all'esecuzione dello statement
string s = articolo.Tipo.Descrizione;
Inoltre se avessimo voluto comunque forzare il
caricamento diretto di TipoArticolo, a prescindere dalla sua dichiarazione di
fetching Lazy, sarebbe stato sufficiente modificare il mapping del many-to-one
su Articolo in questo modo
<many-to-one name="Tipo"
class="NHLazyTest.TipoArticolo, NHLazyTest"
fetch="join" />
powered by IMHO 1.3