Un pattern şi trei implementări (partea 3)

În precedentele două posturi (1, 2) am povestit despre cum se realizează eager loading în două framework-uri mai cunoscute: LLBLGen şi EntityFramework.

În final, hai să vedem cum NHibernate, probabil cel mai cunoscut O/R mapper open-source din lumea .NET, lucrează cu eager loading.
În NHibernate, se recomandă sa se folosească "lazy loading" - prima accesare a oricărui property sau colecţie mapată pe o relaţie către alta clasă, va aduce datele din baza de date, dacă nu au fost aduse deja.
NHibernate foloseşte pentru "eager loading" termenul de "immediate fetching". O variantă (care nu e chiar recomandata) de a specifica eager loading e sa se speciifce, in fisierul de mapare, pentru respectiva relatie de asociere, atributul lazy="false":

<set name="Purchases" lazy="false">
    <key column="CustomerId"/>
    <one-to-many class="Order"/>
</set>


Dezavantajul unei astfel de metode e că setarea e globală, deci indiferent de unde încărcam datele şi care e obiectul "rădacină" de la care plecăm, obiectul sau colecţia aferentă acelei asocieri va fi întotdeauna încărcată din baza de date, chiar dacă uneori nu avem nevoie de ea. Mai rău, dacă în fişierul de mapare se foloseşte lazy="false" pentru o mare parte din asocieri, riscăm să încărcăm în memorie o mare parte din baza de date la un simplu request, pentru un simplu obiect .

Alternativa e să se specifice în cod, de la caz la caz, care din obiectele "relaţionate" vrem să le încărcam in avans (în cazul în care folosim criteria API):

Person customer =
      (Person)session.CreateCriteria(typeof(Person))
                .CreateAlias("Purchases", "p")
                .SetFetchMode("Purchases", FetchMode.Join)
                .SetFetchMode("Company" ,FetchMode.Eager)
                .SetFetchMode("p.Product", FetchMode.Eager)

                .SetResultTransformer(new
                                DistinctRootEntityResultTransformer())

                .Add( Expression.Eq("PersonId", personId) )
                .UniqueResult();

NHibernate oferă un control mai fin asupra strategiei folosite pentru a încărca obiectele: în exemplul de mai sus, FetchMode.Join specifică nu doar faptul că vrem sa folosim eager loading, dar şi faptul că vrem să se încarce datele intr-un singur SELECT,  folosind un LEFT OUTER JOIN (alternativele ar fi un SELECT separat pentru fiecare colecţie/asociere, sau un sub-select).
In exemplul de mai sus, care e diferenţa între FetchMode.Eager si FetchMode.Join? (aparent, niciuna: http://bchavez.bitarmory.com/archive/2008/04/04/differences-between-nhibernate-fetchmode.eager-and-fetchmode.join.aspx).

O altă variantă oferită de NHibernate e folosirea HQL (Hibernate Query Language), care poate conţine unul sau mai multe joins:

        string sql = "from Person p" +
                     " left join fetch p.Company" +
                     " left join fetch p.Purchases as order" +
                     " left join fetch order.Product" +
                     " where p.PersonId=:personId and order.Isdelivered = false";

Dezavantajul metodei de mai sus, e că NHibernate va genera un singur SELECT cu mai multe JOINS, deci dacă acel client va avea foarte multe comenzi, detaliile clientului vor fi aduse de atâtea ori câte orders sunt returnate (http://ayende.com/Blog/archive/2007/03/17/Performance-Multiply-Collections-Fetch-With-NHibernate.aspx).

Alternativa? În NHibernate 2.0, putem specifica in HQL exact cum să fie structurate query-urile, folosind IMultiQuery (http://blogs.hibernatingrhinos.com/nhibernate/archive/2008/04/06/eager-loading-aggregate-with-many-child-collections.aspx), dar deja asta înseamnă că trebuie să mă gândesc eu cum să optimizez strategia de încărcare a datelor, în locul O/RM-ului (ceea ce nu e întotdeauna rău, deşi e un efort în plus); de asemenea, IMultiQuery nu e suportată decât pentru anumite database servers.

Mai multe detalii la: [http://nhforge.org/doc/nh/en/index.html#performance-fetching] sau [http://nhforge.org/wikis/howtonh/lazy-loading-eager-loading.aspx]

La NHibernate e oarecum logic să întâlnim acelaşi mic dezavantaj ca şi la EntityFramework - SetFetchMode sau HQL folosesc stringuri hard-codate, deci e mai mare riscul apariţiei erorilor de typing sau ca urmare a unui refactoring (schimbarea numelui unui property de ex.).
Şi la această problemă există soluţii, chiar dacă third-party: http://code.google.com/p/nhlambdaextensions/ . Se poate ca LINQ for NHibernate, sa rezolve si aceasta problema...


Şi câteva concluzii:

- chiar dacă folosim un O/RM, intr-un proiect real nu ne scuteşte să ne gândim la modul în care datele vor fi încărcate din baza de date, şi de necesitatea de a alege metoda potrivita în de la caz la caz

- eager loading, deşi ne oferă controlul asupra cantităţii de date încărcate şi asupra numărului de round-trips pana la database server, dacă nu e folosit cu discernământ poate duce cu uşurinta la încărcarea în memorie a unei cantităţi de date excesivă: de ex., in exemplul nostru, dacă un client are 5.000 sau 10.000 de comenzi (poate reprezintă o mare firma care e client de 10 ani încoace), încarcând in memorie in avans intreaga colecţie preson.Purchases, vom aduce pe client toate cele 10.000 de înregistrări, deşi e puţin probabil că user-ul chiar are nevoie să le vadă pe toate deodată/simultan.
În astfel de cazuri, e mai bine să ne gândim de doua ori si când avem de-a face cu relaţii 1-to-many, să încărcam colecţii de "childs" separat, fie paginate, fie filtrate după un criteriu bine ales.

- în aplicaţii reale, graful de obiecte care trebuie încărcat chiar si pentru un singur form/page poate ajunge cu uşurinţă extrem de stufos - daca lazy loading face acest lucru transparent, la eager loading codul care specifică explicit ce obiecte şi relaţii să fie încărcate poate deveni rapid un cârnaţ foarte mare şi greu de întreţinut (am avut cazuri de 50 - 70 linii de cod intr-o singură metodă). In astfel de cazuri, deja trebuie să ne gândim la cum putem modulariza şi sparge în bucăţi mai mici acest cod, bucăţi pe cât posibil reutilizabile.
Pentru a ţine sub control astfel de aspecte, nu ar strica să se sape mai mult in direcţia unor alte pattern-uri care să facă ordine in "haos", precum "aggregate" din DDD (http://dddstepbystep.com/wikis/ddd/blogged-aggregates-and-aggregate-roots.aspx).

- seria de posturi care tocmai am scris-o nu se vrea sa fie vreo "comparatie" intre cele trei O/RM mappere, si în general astfel de comparatii/benchmarks nu au sens cand e vorba de aplicati reale - fiecare foloseste ce cunoaste mai bine, frameworkul în care se simte mai confortabil si care corespunde mai buine cu "filozofia" proprie - testele de performantă sau compararea setului de "features" e cel mai adesea irelevantă in realitate - folosit cu cap, orice O/RM acoperă nevoie unei aplicatii obisnuite, iar problemele de performantă vin cel mai adesea din modul în care e gandit softul sau din utilizarea nepotrivita a acelui framework.

Published Tue, Nov 17 2009 3:58 PM by tudor.t

Comments

# Aurelian said on 17 November, 2009 05:14 PM

Mihai Lazar va incepe sa posteze pe RONUA despre DDD, soon.

# tudor.t said on 18 January, 2010 09:39 AM

Tot legat de eager loading in NHibernate, un articol cu mai multe datalii a aparut la:

ayende.com/.../eagerly-loading-entity-associations-efficiently-with-nhibernate.aspx