Ce nu au programatorii in C#, dar au cei ce folosesc VB(.NET)


Desi am programat aproape trei ani in VB(.Net), nu credeam ca exista asa ceva (trecand peste diferentele de sintaxa si unele sintactic sugars).
Totusi, sa zicem ca avem urmatorul scenariu:
- un proiect dezvoltat mai intai in Visual Studio 2008, C# 3.0, .NET 3.5 si evident CLR 2.0, cu unit teste dezvoltate normal tot in C# 3.0/.NET3.5.
- vine Visual Studio 2010 si acel proiect e convertit automat de noul IDE, dar ramane targetat .NET Framework 3.5 (pana la o viitoare trecere pe .NET 4.0 si CLR 4.0)

Dupa trecerea la VS2010, proiectul ce contine unit teste targheteaza .NET 4.0. Merg frumos in project properties, incerc sa aleg .NET Framework 3.5 (Visual Studio de cateva versiuni incoace suporta multi-targetting fara probleme, nu?) - supriza, nu vrea:

Ma gandesc - o fi vreo limitare in Visual Studio 2010, asta e..

Totusi, se pare ca la capitolul asta cine s-a hotarat sa scrie unit testele in VB e ceva mai norocos:

iar pentru C#:

(asta in VS2010 Premium, deci nu se pune problema ca lipseste vreun template..)

Pe Connect, multa lume se plange de asta, dar Microsoft zice sec - "by design":
http://connect.microsoft.com/VisualStudio/feedback/details/453668/cant-change-target-framework-for-test-project-in-vs2010

"This was a conscious decision to not allow multi-targeting. The reasoning comes from the inability to users to run many of the features of unit testing on other frameworks. As a result of this and other factors, multi-targeting for this release is not supported."
http://connect.microsoft.com/VisualStudio/feedback/details/483939/unable-to-change-target-framework-version-on-unit-test-projects
sau chiar http://connect.microsoft.com/VisualStudio/feedback/details/527009/target-framework-for-unit-test-project-in-vs2010-beta-2-only-supports-net-4#
unde raspunsul e si mai "la obiect":
"Unit Tests do not support multi-targeting for VS2010. So unit tests will be affected by this decision. There are other ways to test this in VS 2010 such as web, load and coded UI testing. They are not unit tests, but do provide a testing mechanism for applications that require .NET 3.5 runtime."

În sfârșit - SQL Server a auzit de .. paginare

Multora titlul de mai sus li se va parea exagerat - oricine stie ca paging s-a putut implementa si pe SQL Server de multa vreme, folosind diverse solutii mai mult sau mai putin elegante (stored procedures, temp tables, row_number/over, cursoare etc..), mai mult sau mai putin eficiente (http://www.codeproject.com/KB/aspnet/PagingLarge.aspx )
(in acest context: paging - posibilitatea de a obtine, pe database server, un subset al rezultatelor ce satisfac un anumit query, dandu-se o ordine bine definita, incepand de la rezultatul cu numarul 'n' pana la cel cu numarul 'n'+'p', unde 'p' e 'dimensiunea paginii', fara a aduce pe client toate rezultatele returnate de query - procedeu intalnit cel mai adesea in aplicatii web, mai rar desktop).

Desi in SQL Server 2005/2008 metoda cea mai des folosita e cea ce foloseste ROW_NUMBER/OVER, si se obtine o sintaxa ceva mai "umana", pentru cine trebuie sa implementeze singur paginarea pentru SQL Server, mai ales pentru solutii generice ce trebuie sa mearga si la select-uri netriviale, si care trebuie sa mearga si pe versiuni mai vechi de SQL Server (7.0, 2000) - implementarea unei solutii de paginare a ramas ceva suficient de obositor..

Vestea buna e ca, in sfarsit in ceasul al 12-lea cineva din echipa SQL Server CE (Compact Edition , cunoscut sub numele 'SQL Server Everywhere' inainte de release, descendentul lui SQL Server for Windows CE and SQL Server Mobile Edition, care in ciuda numelui se poate utilza fara probleme ca un enbedded db. si in aplicatii desktop)
si-a dat seama ca e un scenariu suficient de comun, care sa merite o sintaxa dedicata la fel ca pe alte database servers (MySQL, Firebird etc..), deci incepand cu SQL Server CE 4.0 putem face asta:

SELECT * FROM Customers ORDER BY [Customer ID] OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;

Sursa: http://blogs.msdn.com/b/sqlservercompact/archive/2010/07/07/introducing-sql-server-compact-4-0-the-next-gen-embedded-database-from-microsoft.aspx

Sper ca nu se opresc aici, si aceeasi sintaxa va aparea si in versiunile viitoare ale SQL Server-ului "mare".



Avantajul? Trecand peste sintaxa ne-eleganta (care odata rezolvata se poate incapsula intr-o metoda comuna), vreau sa las DB Server-ul sa-si bata capul cu cea mai eficienta executie pentru asa ceva, si sa nu mai vina db. admin-ul la programator sa ma intrebe ce vreau sa fac cu acel WITH, subselect sau temp. table pe care l-a vazut cu profiler-ul.. :-)
Un alt avantaj e ca e o sintaxa SQL standard (desi aparuta doar in standardul SQL:2008, si suportata de foarte putine db. servere - PostgreSQL din cate stiu mai suporta aceeasi sintaxa: http://www.postgresql.org/docs/8.4/static/sql-select.html#SQL-LIMIT ) (sursa: http://troels.arvin.dk/db/rdbms/#select-limit-offset )

Normal, va dura ceva vreme pana diversele framework-uri si O/RM-uri vor fi updatate pentru a suporta/genera si noua sintaxa, dar cum majoritatea au un fel de "provider model", nu va fi greu..

 

P.S. Screenshot-ul de mai sus e din MS Web Matrix Beta + SQL Server Compact 4.0 Tool CTP1 , care se pot instala prin Web Platform Installer 3.0 - deocamdata singura metoda pe care am gasit-o de a executa un query pe un db. SQL CE 4.0 CTP 1 (fara a scrie cod .NET, normal). SQL Server Management Studio 2008 inca nu stie decat de SQL CE 3.5, pare-se..

Extensii si addinuri pentru VS2010 - o lista proprie..

As fi curios sa aflu ce extensii si addinuri folosesc altii in lucrul de zi-cu-zi in Visual Studio 2010.

Lista mea ar fi (la momentul actual) - intr-o ordine oarecare, nu neaparat in ordinea importantei:

  • GhostDoc (gratis) - foarte util pentru cei mai lenesi in a scrie XML comments-urile; nu il vad un substitut la necesitatea de a scrie comentarii cu adevarat utile, acolo unde nu e evident scopul unei clase sau metode, ci doar ca un template de pornire
  • Resource Refactoring Tools 2010 (gratis) - foarte util pentru a muta cu usurinta un string din cod intr-un .resx; sugereaza si un nume pentru resource key si permite alegerea .resx-ului destinatie. Ceva similar ofera si Resharper-ul, dar numele sugerat nu e asa elegant. Dezavantaje: in rare ocazii inceteaza sa mai mearga; nu permite setarea comment-ului la resursa adaugata; nu permite setarea unui naming convention custom pentru numele resurselor.
  • Help Viewer Power Tool (beta) (gratis) - pentru cine nu e multumit cu noul mod de vizualizare a helpului in VS2010 (si prefera sa aiba helpul instalat local), si vrea sa aiba Index la help
  • Go To Definition (gratis) - util pentru cine uita shortcut-ul aferent Smile, e satul sa caute in context menu si vrea sa aiba un goto to definition la un simplu Ctrl-Click
  • Visual Studio 2010 Pro Power Tools (gratis) - o gramada de extensii la VS2010 oferite de MS - cele mai utile: posibilitatea de a organiza mult mai bine tab-urile in editor (setarile implicite coloreaza tab-urie in functie de proiect si alte criterii - un pic prea multe culori dupa gustul meu :), add project reference cu search, align assignments on equals si colun guides (pentru cazurile cand vreau sa gasesc perechea unei acolade, pe verticala, situata la mare distanta..)
  • TFS 2010 Power Tools (gratis) - diverse tooluri pentru lucrul cu VS2010 - mai utile mi se par Windows Shell Extension (lucrul cu TFS direct din Windows Explorer context menu).
    Ce lipseste: posibilitatea de a monitoriza modificarile dintr-un folder (TFS local working folder) a.i. fisierele create/sterse de alte softuri decat Visual Studio sa fie automat adaugate ca pending add/removes/changes in TFS source control (Tortoise SVN ofera asa ceva).
    MS se pare ca a 'uitat' ca mai sunt si alte tooluri care adauga/modifica/sterg fisiere intr-un proiect in afara de Visual Studio (ex.: generatoare de cod), si e obositor sa adaugi/faci check-out manual la fiecare fisier modificat din Source Control Explorer sau din command-line...
  • Jetbrains Resharper (pe bani) - probabil cel mai faimos addin la Visual Studio pentru intellisense, refactorings, static code analysis si nu numai... Unii il iubesc si nu pot fara el, altii il urasc.. Consuma ceva resurse (procesor, memorie etc.), dar cel putin in combinatia VS2010, Resharper 5.0, Windows 7 64 bits, un quad core si 3 GB RAM se misca acceptabil..
    Cine vrea sa il foloseasca complet, cam trebuie sa fie dispus sa isi schimbe stilul de lucru in VS.

    Principala alternativa si competitor la Resharper: DevExpress DxCore/CodeRush/RefactorPro (de asemenea, nu e gratis). Totusi, DxCore care e gratis in combinatie cu multele add-inuri de VS care il folosesc, poate fi o alternativa partiala pentru cine nu are banii necesari pentru Resharper sau CodeRush..

Desigur, acestea sunt addinurile si toolurile care le folosesc momentan - ar mai fi multe de care as avea nevoie in functie de context (TestDriven.Net, Tortoise SVN etc..).

As zice ca e o chestie de gust, stil de lucru si de genul de proiecte in care lucreaza fiecare.
Orice addin sau extensie vine cu un overhead mai mic sau mai mare in Visual Studio, si mai ales la cele mai obscure nu e neglijabil riscul diverselor bug-uri care pot merge pana la crashuri in VS, deci e si un risc asumat...

Daca cineva doreste sa impartaseasca lista de addinuri proprie, e binevenit.

Alternative in lucrul cu Entity Framework 4.0

Intr-un post mai vechi promiteam ca o sa revin cu mai multe detalii despre ce aduce nou LLBLGen 3.0.

Cum ziceam si atunci, odata cu aparitia LINQ2SQL si mai apoi ADO.NET Entity Framework, ambele gratis si incluse in .NET Framework, mai toti producatorii de O/RM-uri si-au dat seama ca trebuie sa se miste daca vor sa aiba noi clienti in continuare.

Si iata ca in 23 aprilie a aparut (doar pentru clientii existenti sau noi) o versiune de LLBLGen 3.0 Beta (update 5) care permite generarea de cod care targheteaza Entity Framework (1.0 sau 4.0). Cel mai bine e exemplificat procesul respectiv cu un video postat de autori: http://www.llblgen.com/pages/news.aspx

Totusi, cine si de ce ar vrea sa dea bani sau sa foloseasca asa ceva? Ar fi cateva categorii:
- firmele care actualmente folosesc designer-ul si runtime-ul LLBLGen, dar vor sa treaca din diverse motive la Entity Framework 4, dar totusi vor sa lucreze in continuare cu un designer si generator de cod familiar.
- programatorii care vor mai mult control asupra codului generat (template-urile folosite pentru generarea de cod se pot edita usor, existand si un editor de template-uri) - desi acest lucru e posibil si folosind template-uri T4 in VS2010: http://thedatafarm.com/blog/data-access/customizing-edm-code-gen-in-ef4/ )
- programatorii nemultumiti de designer-ul sau de codul generat de Visual Studio pentru Entity Framework.
- firmele care vor sa aiba un singur generator de cod, cu suport comercial, care sa poata targeta in proiecte diferite, atat LLBLGen, cat si LINQ2SQL, EF sau NHibernate.

Daca as folosi personal asa ceva (LLBLGen-->EF4) in acest moment? (sau cand apare versiunea finala?) Deocamdata, probabil ca nu: intr-un proiect comercial, e riscant sa se foloseasca un produs proaspat aparut, pana nu e validat de suficiente persoane, mai ales avand in vedere complexitatea EF4. La asta, se adauga si learning curve-ul aferent invatarii a doua tehnologii noi (EF4 si designer-ul/sistemul de templates LLBLGen 3.0).
L-as folosi cu siguranta doar daca ar fi vorba de un proiect nou, in care e alocat timp pentru 'experimente' si presiunea de timp nu e asa mare..
In versiunea actuala (Beta 1 update 5) inca mai scartaie ([http://www.llblgen.com/tinyforum/Messages.aspx?ThreadID=17804&StartAtMessage=0&#99616]) dar probabil vor rezolva problemele pana la sfarsitul lui mai, cand se spera sa apara versiunea finala.

Desigur, exista si alte produse care ofera ceva similar (generatoare de cod pentru Entity Framework), precum
DevArt Entity Developer

Cateva screenshots "in premiera":

Când Visual Basic si C# se întâlnesc

Acum cateva zile am incercat sa folosesc intr-un program (scris in C#) un assembly implementat in Visual Basic (.NET, desigur).
Cum aveam codul sursa, dupa mai multe cautari am gasit intr-o clasa (.vb) un property care arata cam asa:

(normal, in realitate aveam de a face cu o clasa mult mai complexa..)

Grabit, sar inapoi in codul meu C# si incerc sa o folosesc:

Lightning Hmm - atunci imi amintesc ca C# inca nu suporta asa ceva - nu e un array, nu e un indexer obisnuit..

E ceea ce in VB se cheama non-default indexed property.
Totusi, .NET-ul se lauda ca putem folosi un assembly indiferent in ce limbaj a fost implementat.

Dupa mai multe explorari in lista de membri expusi de intellisense, vine si solutia:

Idea Desigur, in realitate clasa avea peste 50 de membri, deci nu a fost asa usor de gasit...

Arata ca un apel de functie? Normal - in cele din urma un property cu doar un getter asta e - sintactic sugar ce genereaza o functie..

Sapand putin in MSDN, la capitolul "Design guidelines for class libraries' , se gaseste si recomandarea aferenta, cumva logica:
"Do not use non-default indexed properties.
Some compilers, such as the C# compiler, enforce this guideline. Non-default indexed properties are not supported in all programming languages. If you use them, some developers will not be able to access these members
"

Totusi, in afara de sintactic sugar, de ce au cei din 'lumea' VB nevoie de asa ceva?
COM, ca de obicei (nu va ganditi la cine stie ce ActiveX de pe vremea bunicii, think Excel, Word etc..).

Chiar daca multi din noi avem doar rar de folosit componente COM direct, inca mai exista sunt folosite o gramada de componente COM, dovada ca in C# 4.0 s-a inclus suport built-in pentru accesarea index properties non-default (doar accesare, nu se vor putea declara in C#), deci vom putea scrie:
int val = vbClass.MyFancyProperty["keyValue"];
fara 'get_' ...
Mai multe despre asta si despre motivul pentru care nu se pot declara in C# 4.0, la: http://blogs.msdn.com/kirillosenkov/archive/2009/10/20/indexed-properties-in-c-4-0.aspx

Adevarul e ca, daca cineva vrea musai sa realizeze o clasa in C# care sa expuna un property de acest gen, o poate face daca 'MyFacyProperty' are ca tip nu un 'int', ci o alta clasa care la randul ei expune un default indexed property ( ... this[...] ) - cam peste mana, dar..
Altfel, daca intr-o clasa avem un singur indexed property si vrem sa oferim un nume mai sugestiv, putem folosi aceeasi solutie folosita de cei de la MS pentru clasa String:

        [System.Runtime.CompilerServices.IndexerName("Chars")]
        public extern char this[int index] {
            // ...
            get;
        }

si ca urmare din VB se poate apela astfel:

            Dim s As String = "sadasdas"
            Dim c As Char = s.Chars(5)

Chiar daca pentru multa lume obisnuita doar cu C# asa ceva pare doar sintactic sugar, sa nu uitam ca si property-urile simple, nelipsite din C#, sunt privite in alte limbaje (C++, Java) ca o constructie care nu are ce cauta la nivel de limbaj (Delphi (Object Pascal) a fost printre primele limbaje ce au introdus conceptul de properties, C++ Builder l-a urmat folosind o extensie non-standard la limbaj).

Pentru cine e pasionat de 'maruntaiele' C#, ii recomand blogul excelent al lui Eric Lippert: http://blogs.msdn.com/ericlippert/default.aspx

 

LLBLGen 3.0 beta - nou si vechi


Probabil multi din cei ce cititi acest blog va intrebati - de ce m-ar interesa ce a aparut nou intr-un O/RM comercial/proprietar de care am auzit doar in treacat, cand eu folosesc fara probleme Entity Framework, NHibernate, LINQtoSQL sau alt mapper?

Eu cel putin, sunt curios din mai multe motive: sa vad cum altcineva abordeaza o problema comuna (partea de persistenta in acest caz), sa vad o solutie poate diferita si nu in ultimul rand pentru ca o perspectiva mai larga ma ajuta sa aleg cea mai potrivita solutie dintre mai multe alternative, de la caz la caz.
Ca in multe alte domenii, nu exista "cel mai bun" tool - totul depinde de specificul aplicatiei dezvoltate, de metodologia folosita, de experienta echipei, de gusturile individuale..


LLBLGen 3.0 Beta 1 e proaspat aparut (23 ian.) si e disponibil pentru download doar pentru clienti (care au un username/password de access la zona de download), deci pe net se gasesc inca putine informatii. De ce nu exista inca un trial care sa-l poata downloada oricine, e greu de spus (probabil fiindca include si codul sursa)..

Revenind la "What's new", avem de a face cu un domeniu in care e greu sa mai apara ceva cu adevarat "revolutionar" in ce priveste esenta unui O/R mapper - la urma urmei SQL-ul e de cand lumea, obiectele si clasele la fel, odata cu LINQ s-a rezolvat in mare parte problema descrierii la nivel de limbaj a unui query, strongly typed si fara a recurge la structuri de date foarte complexe. Normal, intotdeauna vor mai fi inbunatatiri de performanta in ce priveste SQL-ul generat..

1. Ca urmare, si in ce priveste LLBLGen, modificarile majore se concentreaza pe alte aspecte care pot face viata programatorului mai usoara: daca in cazul unor O/RM-uri, precum NHibernate, multi programatori prefera sa isi creeze clasele manual, una cate una, si sa isi defineasca maparile direct intr-un XML (desi exista generatoare de cod third-party care ajuta mult pe partea asta), in cazul altor O/RM-uri putini programatori s-ar simti "confortabil" fara a avea "la pachet" un generator de cod si un designer ma mult sau mai putin "vizual".
Atat Entity Framework, LINQtoSQL cat si LLBLGen, pe langa runtime vin si cu un generator de cod si un designer.

In schimb, "problema" cea mai desc criticata de unii la aceste generatoare de cod e ca reduc controlul programatorului asupra codului generat, si ca (pana recent), majoritatea promovau un stil de dezvoltare "database-driven", in care mai intai trebuie definita structura bazei de date, dupa care generatorul crea un set de clase pornind de la tabelele, view-urile sau stored procedure-urile din baza de date.
Desi e o metoda foarte productiva, vine cu cateva dezavantaje:
- implicit, clasele vor avea o structura foarte apropiata de structura bazei de date
- programatorul e tentat sa gandeasca mai putin in termeni OOP, si mai mult in termeni "relationali"
- usurinta de a crea cate o clasa pentru absolut fiecare tabela, chiar si acolo unde nu e cu adevarat necesar
- si in primul rand, dificultatea de dezvolta o aplicatie "test-driven" (TDD), cu unit-teste etc. - deseori clasele generate nu vor expune o interfata si vor contine o gramada de cod generat, folosit pentru partea de persistenta..

Desi toate acestea se pot depasi si evita cu vointa si "disciplina", atat Entity Framework in viitoarea versiune 4.0, cat si LLBLGen in ver. 3.0, vor suporta probabil cel mai cerut feature: "code first"/"domain driven" development (pe langa mopdelul existent, "database first"):
- clasele sunt create primele pornind de la cerintele functionale ale aplicatiei (ideal scriind un set de unit-teste mai intai)
- partea de persistenta e privita ca un serviciu ce tine de infrastructura
- diferentele intre clase (domain model) si structura bazei de date devin mai explicite
- atunci cand e posibil, structura bazei de date poate fi generata/updatata pornind de la structura claselor (si pe baza maparilor), nu neaparat invers (metoda care se poate aplica mai mult la aplicatii noi si baze de date cate nu sunt folosite si de alte aplicatii mai vechi).

Conceptual, acest mod de lucru e ilustrat in documentatia LLBLGen astfel:


In cuvinte, principiul e destul de simplu: programatorul, poate incepe prin a defini "abstract entity model-ul" (pentru cine nu e familiar cu terminologia, oarecum e un concept oarecum similar cu "logical/conceptual schema" din Entity Framework). La modul concret, poate fi privit ca un "domain model", pe baza caruia, pe de o parte se pot genera clasele intr-un limbaj de programare (C#, VB.Net etc.), si care, pe de alta parte, poate fi mapat pe o structura a baze de date relationale.
Normal, in principiu exista si posibilitatea de a se genera "abstract entity model-ul" pornind de la un set de clase existente, dar aceasta posibilitate nu va fi disponibila in LLBLGen (cu exceptia unui caz particular, de care voi aminti mai incolo).

Odata definit entity model-ul in designer (vizual sau de la tastatura), se va putea fie defini maparile cu o baza de date existenta, fie se va putea genera automat structura bazei de date pornind de la entiy model.
Pentru cineva obisnuit sa inceapa o aplicatie cu crearea bazei de date, si dat fiind ca multa lume a ajuns sa faca asta destul de rapid intr-un DBMS, daca e sa "schimbe macazul" si sa inceapa cu un entity model, vor simti nevoia unui tool care sa permita definirea de entitati si a relatiilor intre ele intr-un mod cat mai rapid si productiv, fara a fi obligati sa jongleze cu mouse-ul intr-un designer si sa caute comenzi in meniuri contextuale.
Pentru asta in LLBLGen 3 exista asa-numitul "QuickModel" designer - care poate fi inteles doar privind un demo:
[http://weblogs.asp.net/fbouma/archive/2009/11/25/llblgen-pro-v3-0-model-first-with-quickmodel-and-more.aspx] si e probabil una din cele mai interesante folosiri a unui DSL (domain specific language).

                                                                          


2. Ar fi timpul sa amintesc de cea de a doua noutate cu care vine LLBLGen 3.0: odata ce si-au dat seama ca e greu sa mai apara chestii inovative strict legate de engine-ul pe care e bazat un O/RM, si cum aveau deja un designer si generator de cod bazat pe template-uri de buna calitate, si-au dat seama ca il pot folosi pentru a genera cod si a edita maparile si pentru alte O/RM-uri: LINQtoSQL, NHibernate si Entity Framework. Normal, codul generat, cat si codul scris de programator va folosi tot framework-urile respective (scopul ne fiind crearea unui wrapper care sa le abstractizeze - oricum un astfel de wrapper e ceva destul de utopic in practica).

Ce inseamna asta pentru cineva care foloseste Visual Studio pentru a genera clasele si a defini maparile pentru Entify Framework de exemplu? Insemana ca are o alternativa - cel putin in ver. EF 1.0, designer-ul fiind foarte "pagubos" la modele cu peste 100 de clase (ceea ce nu e un numar mare in aplicatii reale).
Pentru cineva care foloseste NHibernate, alternativele sunt mult mai diverse (generatoare de cod precum CodeSmith, MyGeneration, Moregen, NConstruct etc.).
                                                                          

3. LLBLGen 3.0 stocheaza (in sfarsit..) definitiile entitatilor si maparile intr-un fisier XML, in loc de unul binar - pare ceva simplu (NHibernate, LINQtoSQL si Entity Framework fac asta de multa vreme), dar nu e deloc simplu daca se doreste ca acel XML sa poata fi editat de mai multi developeri in acelasi timp, si sa poata fi "merge"-uit de un tool de source control fara dureri prea mari. Un XML e cvasi-inutil intr-o echipa, daca o modificare minora duce la re-aranjarea a 50% din liniile fisierului XML - LLBLGen promite ca modificarile vor fi cat mai localizate.
                                                                          

4. Grouping - in majoritatea proiectelor reale, e destul de greoaie mentinerea tuturor claselor din domain model intr-un singur proeict/assembly - LLBLGen permite gruparea claselor si generarea de proiecte separate pentru acestea. Desigur, asta e posibil doar daca nu exista referinte circulare intre clasele din proiecte diferite (referintele circulare intre assembly-uri nefiind posibile).
                                                                          

Cate screenshot-uri "in premiera":







O sa revin cu mai multe detalii in curand (http://ronua.ro/CS/blogs/tudort/archive/2010/05/02/LLBLGen-Alternative-lucrul-Entity-Fremework-4_5F00_0.aspx).

P.S.: am renuntat deocamdata la sh, tz, î, ă, â - too painfull.. :)

 

Câteva gânduri despre LINQ, performanță şi optimizări

Odată cu apariţia LINQ ('to objects'/in-memory - LINQ to XML sau LINQ to SQL sunt alta poveste), şi pe măsură ce trecea timpul mi-am dat seama că foloseam din ce în ce mai des LINQ pentru diverse căutări sau sortări pe colecţii de obiecte, in memorie.

În ce priveşte eleganţa si expresivitatea codului, e clar ca un

var finalState = (from o in myCollection
                  where o.StateId == (int)(OrderState.Final)
                  select o).FirstOrDefault();

e mai elegant decât o căutare implementată aşa cum am învăţat în şcoală, cu un while sau folosind altă metodă "imperativă".

Intuiţia îmi zicea că această eleganţă e normal să vină si cu un cost in ce priveşte performanţa, aşa că am încercat să folosesc LINQ to Objects doar când era vorba de colecţii mici de obiecte, şi unde performanţa nu era ceva critic.

Totuşi, în cele din urmă mi-am făcut timp şi am vrut să verific această "intuiţie" la modul concret. Pentru că într-un blog nu aveam loc să pun o situaţie reală în care folosesc LINQ to objects, am luat un caz mult simplificat: căutarea liniară a unui număr anume într-o listă neordonată de numere întregi (dacă era ordonată deja se punea problema folosirii a diverşi algoritimi de căutare optimizaţi, gen binary search).
Cum nici LINQ nu ştie dacă lista e ordonată sau nu, putem genera lista în cel mai simplu mod posibil:

        private static List<int> GenerateList()
        {
            int listSize = 80000000;
            List<int> numbers = new List<int>(listSize);
            for (int i = 0; i < listSize; i++)
            {
                numbers.Add(listSize - i);
            }
            return numbers;
        }


deci 80 de milioane de numere (suficiente ca să scoată la iveala eventuale diferenţe de performanţă), puse în ordine descrescătoare într-o listă.

Mai întâi, metoda de căutare "clasică", liniară - la fel; ca în clasa "a 5-a", plictisitor de implementat, dar foarte simplă:

        private static bool FindNumber(int number, List<int> numbers)
        {
            bool found = false;
            int i = 0;
            int count = numbers.Count;

            while (!found && (i < count))
            {
                if (numbers[ i ] == number)
                {
                    found = true;
                }
                else
                {
                    i++;
                }
            }

            return found;
        }

Pentru o colecţie neordonată, nu ştiu dacă există o altă metodă mai optimă (cât timp rămânem pe un singur thread şi un singur procesor).

Folosind LINQ to objects, codul devine brusc mai elegant şi mult mai simplu:

        private static bool FindNumber(int number, List<int> numbers)
        {
            return numbers.Any(i => i == number);
        }

În final, am creat două aplicaţii consolă, care nu fac altceva decât să apeleze cele două metode intr-un loop (de mai multe ori pentru a elimina intr-o oarecare măsură diferenţele aleatoare aferente unei singure execuţii), şi să calculeze cât mai precis timpul scurs în fiecare din cazuri:

        static void Main(string[] args)
        {
            List<int> numbers = GenerateList();

            Stopwatch stopWatch = new Stopwatch();
           
            stopWatch.Start();

            for (int i = 0; i < 10; i++)
            {
                bool found = FindNumber(i + 200, numbers);
            }

            stopWatch.Stop();

           
            TimeSpan ts = stopWatch.Elapsed;

            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
                ts.Hours, ts.Minutes, ts.Seconds,
                ts.Milliseconds / 10);

            Console.WriteLine("Number found.");
            Console.WriteLine(elapsedTime, "RunTime");
        }

(O mica paranteză: Stopwatch este, pentru cine nu ştie, o clasă din System.Dyagnostics, apărută în .NET 2.0, ce permite măsurarea intervalelor de timp cu o precizie mai mare decât dacă am folosi DateTime.Now, în măsura in care hardware-ul şi versiunea de Windows ne permit - e un wrapper ce foloseşte aşa-numitul high-performance counter, printr-un apel la functia QueryPerformanceCounter din Windows API. QueryPerformanceCounter nu este nici el o metodă absoluta de masurare a timpului, putând apărea probleme dacă thread-ul e mutat de pe un procesor pe altul sau dacă CPU-ul îşi modifică dinamic frecvenţa, precum 'Cool&Quiet' de la AMD).

După ce am asamblat cele doua programaşe de test, le-am compilat in Release mode, să vedem rezultatele:
* Căutare liniară "clasică" cu un while:
Clasic search
* Căutare, tot liniară, dar folosind LINQ to objects:
LINQ search

Rezultatele sunt destul de concludente:
- "clasic" search: 4,53s
- LINQ search: 12,52s


Nu e un test de performanţă riguros sau ştiinţific, şi cel mai sigur rezultatele vor diferi de la un calculator la altul.

Pentru cine are răbdare să se uite cu un Reflector sau cu ILDASM, şi ştie câte ceva despre LINQ, rezultatele nu sunt surprinzătoare: Any (la fel ca restul de extension methods din LINQ to Objects), operează pe un IEnumerable<T>, deci chiar dacă ar vrea, nu ar putea să parcurgă colecţia cu un simplu while sau for, ci trebuie să folosească un foreach. Foreach induce automat un oarecare overhead (în unele cazuri).
De asemenea, deşi elegant, acel
i => i == number
se "traduce" la fiecare apel într-o noua instanţă de System.Func<Int32, bool>.

Pentru a transmite 'number', compilatorul va genera dinamic o clasă de genul:

.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret
    }

    .method public hidebysig instance bool <FindNumber>b__0(int32 i) cil managed
    {
        .maxstack 8
        L_0000: ldarg.1
        L_0001: ldarg.0
        L_0002: ldfld int32 LinqSearch.Program/<>c__DisplayClass1::number
        L_0007: ceq
        L_0009: ret
    }


    .field public int32 number

}


şi va crea o instanţă a ei la fiecare apel..

Concluzia din toată povestea asta?
Să nu folosim LINQ to objects pe motive de performanţă? În nici un caz - aş zice mai degrabă să-l folosim de la caz la caz, pe volume moderate de date, şi acolo unde performanţa nu e critică (oricum, în cazurile în care avem un volum mic de date şi totuşi orice gram de performanţă contează, probabil se ajunge la unmanaged code sau chiar assembly :).

Privind puţin lucrurile din exterior, când cineva ajunge să facă căutări sau sortări in memorie, pe mii/milioane de înregistrări, în aplicaţii uzuale (ex.: aplicaţii de gestiune, aplicaţii web), cel mai adesea e un semn de 'code smell', şi deseori e mai bine ca acele date să vină gata filtrate/sortate din database server.

Pentru cine vrea sa sape mai mult în directia asta, câteva linkuri utile:

http://blogs.msdn.com/csharpfaq/archive/2009/01/26/does-the-linq-to-objects-provider-have-built-in-performance-optimization.aspx
http://davepeck.org/linq-collection-performance/

http://www.pluralsight.com/community/blogs/scottallen/archive/2008/07/14/optimizing-linq-queries.aspx

.NET Framework 4.0 (b2) - WinForms - ceva nou?

Pare ciudat că la sfârşitul lui 2009, când peste tot se vorbeşte de Silverlight, WPF, Azure si alte delicatese, cineva se mai gândeşte la bătrânul Windows Forms.. :-)  Cu toate astea, încă se dezvoltă o puzderie de aplicaţii folosind WinForms, şi nu doar aplicaţii legacy.

De ce? Aş zice ca e în natura omului să prefere unealta care o cunoaşte mai bine, care a încercat-o în fel şi chip şi s-a dovedit rezistentă. Pe de altă parte, deşi WPF a apărut de mai bine de 3 ani - 2006 (şi la care Microsoft începuse să lucreze încă de prin 2000-2001 iar primul CTP de Avalon a apărut prin octombrie 2003) - încă sunt destui programatori care fie nu au apucat să se familiarizeze suficient cu WPF, fie nu se simt destul de curajoşi să "sară" in barca WPF...
Nu e nimic nou in asta - şi după apariţia .NET şi WinForms in 2002, încă pentru mulţi ani, şi chiar şi în ziua de azi, unii au continuat să folosească ActiveX/COM, MFC, VCL sau alte alternative..

Şi iată că am ajuns in 2009, a apărut .NET Framework beta 2 - după câteva căutări pe Google dacă a apărut ceva nou în WinForms "4.0", aparent nimic nou, cel mult câteva bug-fixuri. Nici in MSDN aparent nimic nou (http://msdn.microsoft.com/en-us/library/ms171868%28VS.100%29.aspx#client). Nici nu mă aşteptam la altceva - WinForms e deja o tehnologie matură, stabilă şi, din fericire, Microsoft mai lasă şi alte firme producătoare de componente pentru WinForms (Telerik, DevExpress etc.) să câştige o pâine cinstită.. :-)

Totuşi, fiindcă vroiam să mă conving cu ochii mei care sunt diferenţele intre WinForms 2.0 si WinForms 4.0, am încercat să compar assembly-urile, folosind NDepend şi metoda descrisă in blogul lui David Morton: [http://blog.codinglight.com/2009/05/future-of-winforms-whats-changed-in.html] - din păcate fără success - se pare ca versiunea trial de NDepend nu permite compararea de assembly-uri (doar versiunea professional).. :(

Următoarea încercare: Framework Design Studio [http://code.msdn.microsoft.com/fds] - un toll free, pe CodePlex, realizat de o mică echipă, printre care si Krzysztof Cwalina (http://blogs.msdn.com/kcwalina/about.aspx), unul din program managerii echipei de .NET de la MS. După ce am aşteptat o veşnicie să compare cele două versiuni de System.Windows.Forms, rezultatele nu au fost prea utile, având în vedere numărul imens de clase.
A treia alternativă ar fi Reflector Diff Add-in (http://www.codingsanity.com/diff.htm), însă şi acesta îmi arată doar o comparaţie la nivel de cod sursă, destul de inutilă în acest caz pentru cineva care nu are zeci de ore la dispoziţie să înoate în sursele de WinForms.


După toate aceste eforturi, aflu ca autorul NDepend a făcut comparaţia deja (pentru intreg .NET Framework-ul): [http://codebetter.com/blogs/patricksmacchia/archive/2009/05/21/a-quick-analyze-of-the-net-fx-v4-0-beta1.aspx], chiar dacă doar in beta 1.
Şi iată ceva ce pare nou: System.Windows.Forms.DataVisualization.dll - de fapt nu e chiar aşa nou - e de fapt assembly-ul ce conţine Microsoft Chart Controls for WinForms, care era disponibil sub forma unui installer separat pentru .NET 3.5 SP1: [http://www.microsoft.com/downloads/details.aspx?FamilyId=130F7986-BF49-4FE5-9CA8-910AE6EA442C&displaylang=en] (care include si versiunea pentru ASP.NET).
Cautând  în toolbar-ul in VS2010 beta2, pentru o aplicatie WinForms, "noua" componentă apare în grupul "Data":


care poate fi inclusa uşor pe un form:



Puţină istorie..
Normal, MS a mai realizat alte componente de charting şi în trecut, precum MS OLE Chart (MSChart20.ocx) pe vremea VB 5/6: [http://www.vb123.com/toolshed/99_graphs/msolechart.htm] sau MS Graph din Excel (http://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.chart.aspx).
Cum se face că Microsoft s-a gândit tocmai acum, după atâta timp, să includă o componentă pentru charts/diagrame in .NET? Se pare că e mai degrabă un efect secundar: odată cu apariţia SQL Server Reporting services, era clar că Microsoft va avea nevoie de o componentă de charting pentru rapoarte. În loc să reinventeze roata, în 2007 au preferat să folosească o componenta consacrată: Dundas Chart Controls ver. 5.5 (http://www.dundas.com/Components/Products/index.aspx), de la o companie care vinde componente de charting încă din 2002 şi pe baza acesteia să realizeze o versiune proprie ceva mai "light": [http://blogs.msdn.com/alexgor/archive/2008/11/07/microsoft-chart-control-vs-dundas-chart-control.aspx], cu ajutorul arhitectului de la Dundas (Alex Gorev), care acuma lucrează la Microsoft :).

Ulterior, cum Reporting Services oricum era oferit gratis împreună cu SQL Server Express, cei de la MS probabil s-au gândit ca nu au ce pierde dacă oferă componenta respectiva separat pentru ASP.NET si WinForms.
Documentaţie pentru noua componentă se găseşte cu uşurinţă pe net, pe lângă ce se găseşte in MSDN (http://www.microsoft.com/downloads/details.aspx?FamilyId=EE8F6F35-B087-4324-9DBA-6DD5E844FD9F&displaylang=en), de ex.:  [http://blogs.msdn.com/alexgor/default.aspx], [http://code.msdn.microsoft.com/mschart] (samples), [http://www.4guysfromrolla.com/articles/072209-1.aspx]
Oricum, cine a avut până acum nevoie de charts in .NET nu a dus lipsă: plecând de la variantele gratis, precum cea realizata de Carlos Aguilar Mares (http://www.carlosag.net/Tools/WebChart/samples.aspx) pe care am folosit-o in ASP.NET, si terminând cu diversele variante pe bani de la Nevron, Dundas, Infragistics etc, preferabile pentru aplicaţiile care trebuiau să redea chart-uri mai complexe..

În ce priveste bugfixes/improvements in WinForms 4.0, nu stiu încă prea multe, dar se pare că unele din cele mai importante issues din Connect, precum cel legat de suportul pentru visual inheritance din clase generice, în designer-ul de WinForms (https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=105876) e încă nerezolvat (cu statusul postponned) la 5 ani de când a fost raportat..

 

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.

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


După cum spuneam în postul precedent, fiecare O/RM are propria metodă de a descrie graful de obiecte care se doreşte a fi încărcat la eager loading.

ADO.NET Entity Framework (EF), principalul O/RM mapper produs de Microsoft, a fost lansat după apariţia LINQ (ca şi feature al limbajului), deci era logic ca suportul pentru eager loading să fie "integrat" intr-un query LINQ.

In Entity Framework (1.0) daca se doreşte eager loading, termenul folosit este "query path". Un query path în EF e specificat folsind metoda Include, din clasa ObjectQuery<T> (implementarea IQueryable specifica Entity Framework).
ObjectQuery<T>.Include primeşte ca argument un query path sub forma de string, şi întoarce ca result tot un ObjectQuery, a.î. apelurile la Include pot fi "cascadate" cu uşurinţa.

In cazul nostru (descris în postul anterior), query path-ul în EF se va specifica cam aşa:

var customer = (from p in context.Person
                                    .Include("Company")
                                    .Include("Purchases.Product")

                where p.PersonId == personId

                select p).FirstOrDefault();


Ce conţine de fapt un query path? Conţine numele unuia sau a mai multor "navigation properties" la care se doreşte să se "pre-încarce" obiectul sau colecţia referenţiată (deci nu numele clasei sau al entităţii referenţiate trebuie specificat, ci numele property-ului).


După cum probabil aţi intuit deja, modul în care EF (cel puţin prima versiune) permite specificarea unui query path are o serie de neajunsuri:

- path-ul nu e 'strongly typed', deci e uşor să se strecoare greşeli de typing care nu pot fi depistate la compilare, ci doar eventual la execuţie. Cu toate astea unii spun ca astfel de erori ar trebui prinse de un set cat mai complet de unit-teste.

- nu am găsit pana acuma un document "oficial" pe MSDN care să specifice în detaliu sintaxa unui query path

- după toate aparentele, codul SQL generat va fi un mega-select cu o grămada de JOIN-uri, care, deşi aduce toate datele "in one go", are dezavantajul ca, în cazul unor relaţii one-to-many, valorile field-urilor entităţii "părinte" vor fi aduse de 'n' ori, pentru fiecare obiect child ([http://thedatafarm.com/blog/data-access/the-cost-of-eager-loading-in-entity-framework/]. Poate un DBA ştie mai bine, dar as zice ca pentru un obiect "părinte" cu 'n' childs, ar fi mai optim să se genereze doua SELECT-uri - unul care să aducă doar părintele, şi al doilea toţi copii, fără atributele părintelui - mai optim cel puţin în ce priveşte cantitatea de date transportată pe "sârmă" către client.

- în anumite cazuri, Include e ignorat (nu e un bug, e cumva logic): http://wildermuth.com/2008/12/28/Caution_when_Eager_Loading_in_the_Entity_Framework

- nu permite (din cate ştiu) definirea de filtrări la nivelul obiectelor relaţionate (precum LLBLGen)

Era normal că multă lume a încercat, şi unii au şi reuşit să implementeze diverse extension methods care să permită specificarea unui query path intr-un mod "strongly typed" şi în Entity Framework:
[http://www.codeproject.com/Articles/35130/Typed-Eager-Loading-Using-Entity-Framework-What-is.aspx]
sau [http://msmvps.com/blogs/matthieu/archive/2008/06/06/entity-framework-include-with-func-next.aspx]
sau [http://blogs.msdn.com/alexj/archive/2009/07/25/tip-28-how-to-implement-include-strategies.aspx] (thanks Cristi pt. link)
- se pare că nici următoarea versiune de Entity Framework (4.0) nu va include suport "built-in" pentru asta.

Dacă nu vrem să folosim extension methods făcute de alte persoane (poate nu sunt testate în toate cazurile), şi nici nu ne place Include() cu stringuri hard-codate, o alternativă ar fi folosirea de "nested queries" într-un query LINQ (soluţie care se poate aplica şi la alte O/RM-uri, precum LLBLGen sau LINQ to SQL: http://weblogs.asp.net/fbouma/archive/2008/03/07/developing-linq-to-llblgen-pro-part-14.aspx), caz în care query-ul de mai sus ar arata cam aşa:

var customer = (from p in context.Person
                where p.PersonId == personId
                select new {
                  // person fields
                  p.Name, p.City,
                  // ...
                  // ...
                  p.Company,

                  Purchases = from o in p.Purchases
                              where o.IsDelivered == false
                              select new {
                                 // order fields
                                 o.OrderId, o.Date,
                                 // ...
                                 o.Product
                              }
                }).FirstOrDefault();

(scris din memorie, s-ar putea să nu meargă din prima..)
Oricum, deja metoda asta ne aduce o mulţime de anonymous types, deci nu mai avem de-a face cu clasele din domain model-ul nostru.

Cum nu am multă experienţă cu Entity Framework (just learning), s-ar putea să existe metode mai bune de a realiza eager loading în EF, deci feel free să veniţi cu sugestii.

Next, eager loading în NHibernate şi nişte concluzii.

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

Când vine vorba de a accesa o baza de date in .NET, in particular folosind un O/R mapper, multă lume a auzit de "lazy loading" - dându-se un obiect "rădăcina", obiectele sau colecţiile care le referenţiază vor fi încărcate din baza de date doar la prima accesare, nu odată cu acel obiect. O astfel de soluţie are şi avantaje - datele sunt încărcate doar când e nevoie de ele, şi acest proces e transparent pentru programator, dar şi dezavantaje - aplicaţia va face numeroase apeluri separate la baza de date, fiecare cu un overhead in plus.

Alternantiva? "Eager loading" - altfel spus, dacă ştim că, într-un anumit "context" (indiferent că e un unit-of-work, comandă, metodă, form/view etc. - depinde de unde privim) vom avea nevoie cu mare probabilitate de un anumit set de obiecte (pentru a le afişa, edita, prelucra etc.), poate e mai bine sa îi spună O/R mapper-ului (sau in general DAL-ului) în avans de ce obiecte vom avea nevoie sa fie încărcate din baza de date.
Avantaje? Încărcăm toate datele necesare cu un singur drum la baza de date, lăsând O/RM-ul sa optimizeze modul de acces. Dezavantaje? Riscăm să încărcăm mai multe date decât vom avea nevoie, şi să încărcăm memoria cu obiecte care nu vor fi folosite decât mai târziu.

Cum s-ar putea face asta? În SQL, "specificăm" ce date vrem să fie încărcate folosind unul sau mai multe SELECT-uri (care probabil vor include joins, proiecţii, unions etc..). În lumea claselor şi a obiectelor, nu mai avem o lume "tabelara", ci o mulţime de clase (unii prefera sa le numească entităţi), deseori legate între ele prin asocieri (1:n, n:1, m:n..). Concret, aceste asocieri se materializează într-o clasă prin property-uri de tip referinţă (la alte clase sau colecţii de obiecte). Indiferent de modul cum implementam asta, conceptual clasele dintr-o aplicaţie  se poate spune că formează un graf (uite că e bună şi teoria aia din facultate la ceva..). Dacă pornim de la o clasa anume (nod), cel mai adesea putem specifica restul obiectelor de care vom avea nevoie sub forma unui sub-graf (finit) ce porneşte de la acea clasă.

Ok, gata cu teoria. Să iau un exemplu concret: să zicem că intr-o aplicaţie (web sau desktop, nu contează), trebuie  sa afişăm (poate să şi editam) un client (Person class), împreună cu datele firmei (Company) care o reprezintă, lista comenzilor (Orders) făcute de acel client, şi pentru fiecare comanda, câteva detalii despre produsul comandat (Product) - presupunem că un order corespunde unui singur produs (in aplicaţii reale cel mai adesea avem ceva gen order / order lines / product). Avem deci un caz destul de clasic şi banal, care intr-un class diagram arata cam aşa:

(exemplu facut la repezeala, nu ma impuscati daca nu e UML ca la carte, si nici daca nu e modelat cum trebuie :)
Am inclus în diagramă şi câteva clase in plus - in orice aplicate reala graful format de diverse clase e mult mai stufos.

Ca să complicăm puţin problema, să spunem că doar comenzile care nu au fost livrate (IsDelivered) încă trebuie afişate (sau editate) - normal, intr-o aplicaţie reala treaba asta nu e modelata doar printr-un singur flag, dar pentru un exemplu e ok. In final, din toate cele "20" de atribute care le poate avea un produs, să zicem că nu trebuie afişate decât câteva esenţiale: Name (string), Color (string), Barcode (string) - suficiente pentru a identifică un produs.

Ajungând in acest punct, întrebarea e - cum putem descrie (intr-o structura de date sau altfel) acest "sub-graf". Din păcate, la acest capitol, deşi asemănătoare, fiecare O/RM are alta soluţie.
Dacă la problema "cum descriem o interogare/filtru", in lumea .NET soluţia a apărut in cele din urma (LINQ), şi majoritatea O/RM-urilor oferă suport pentru LINQ mai mult sau mai puţin, când vine vorba de "descrierea grafului de obiecte ce va fi pre-incărcat", nu exista încă o metoda unitara (sau nu ştiu eu să existe).

Voi lua ca exemplu 3 framework-uri dintre cele mai cunoscute: ADO.NET Entity Framework (Microsoft), LLBLGen Pro (Solution Design bv, Hague, Olanda) şi NHibernate (open source, LGPL).


In LLBLGen Pro (v. 2.6), sub-graful care se doreşte să fie pre-incărcat e descris printr-un aşa-numit "prefetch path", şi in cazul nostru va arata cam aşa:

///////////////////////////////////
// clasa de la care pornim, nodul "rădăcina"
IPrefetchPath2 prefetchPath = new PrefetchPath2((int)EntityType.PersonEntity);

prefetchPath.Add(PersonEntity.PrefetchPathCompany); // încarcă company

// vrem doar acele orders care nu au fost livrate încă
IPredicateExpression ordersPredicate = (OrderFields.IsDelivered == false); // operator overloading

// specificăm ce property-uri din clasa Product for fi încărcate
// (doar field-urile corespondente vor fi incluse in clauza SELECT generata)
IncludeFieldsList productIncludedFields = new IncludeFieldsList();
productIncludedFields.Add(ProductFields.Name);
productIncludedFields.Add(ProductFields.Color);
productIncludedFields.Add(ProductFields.Barcode);

// "încarcă toate elementele din colecţia Purchases, care satisfac predicatul"
// 0 - all, >0 - TOP n ...
prefetchPath.Add(PersonEntity.PrefetchPathPurchases, 0, ordersPredicate)
       .SubPath.Add(OrderEntity.PrefetchPathProduct, 0, null, null, null, null,
                    productIncludedFields);
// o grămadă de parametrii opţionali, toţi la valoarea default :)

// încărcăm obiectul customer, de tip PersonEntity, din baza de date
PersonEntity customer = new PersonEntity(personId);
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
   adapter.FetchEntity(customer, prefetchPath);
}
///////////////////////////////


Destul de complicat, dar destul de logic.
Pentru cine se simte mai acasa in lumea LINQ si a lambda-expressions, incepand cu versiunea 2.6, LLBLGen permite speciifcarea unui prefetch sub o forma alternativa, cand e vorba de query-uri LINQ.
Ce va genera LLBLGen-ul din cârnaţul de cod de mai sus? - logic, fie o succesiune de SELECT-uri, fie niste JOIN-uri, după cum au crezut că e mai optim.

Cum LLBLGen nu e doar un O/RM ci şi un generator de cod (la fel ca Entity Framework), modul in care e specificat eager loading-ul e strongly typed (deci fara a folosi string-uri hard-codate, ci folosind diverse elemente ce descriu metadatele: PersonEntity, PrefetchPathCompany, ProductFields etc.), toate generate odată cu clasele ce compun domain model-ul.

Pe buna dreptate, cineva s-ar putea întreba: pentru a descrie calea de la Person pana la Product, nu era suficient ceva de genul: Person -> Order -> Product?
Din păcate, in general nu e suficient: de ex., intre clasa Person şi clasa Order, exista doua relaţii de asociere: un Person poate fi clientul care a făcut comanda, dar poate fi şi angajatul care a preluat comanda. In termenii bazelor de date: intr-o tabela (orders) pot exista doua foreign-keys diferite către alta tabela (Person) - dacă pornim de la Person, care din cele doua relaţii o vom folosi pentru a încărca colecţia de Orders ?
Tocmai din acest motiv, un "prefetch path" in LLBLGen e de forma: [EntityName].[Property/Collection name]Prefetch.
Mai multe detalii, la: [http://www.llblgen.com/documentation/2.6/Using%20the%20generated%20code/Adapter/gencode_prefetchpaths_adapter.htm]

In "episodul" următor, vom arunca o privire la modul in care se realizează eager loading in ADO.NET Entity Framework şi NHibernate.