The official Fatica Labs Blog! RSS 2.0
# Saturday, 25 September 2010

Note: For visitors of your site, this entry is only displayed for users with the preselected language Romanian/română (ro)

Continuare la Partea a 3-a : <many-to-one/>

Continuând să lucrăm asupra comenzilor minimale pe care le analizăm acum, ne-ar putea fi folositor să ştim care comenzi sunt asociate unui anumit Customer. Lăsând modelul aşa cum e, o putem face, atât prin HQL cât şi prin ICriteria. Amintesc că acesta este cursul NHibernate de la *ZERO*, deci nu folosim LINQ to NH, şi nici un alt tip de API extern NHibernate –ului, nu pentru că acestea sunt sau nu eficace, ci doar pentru că vrem să analizăm comportamentul de bază. Pur şi simplu creăm cele două unit-teste, primul cu HQL:

   1: [Test]
   2: public void QueryCustomerOrdersWithHQL()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        var query = session.CreateQuery("from Order o join fetch o.Customer where o.Customer.Name=:customer")
   8:            .SetParameter("customer","Customer A");
   9:        foreach (NHFromScratch.Entities.Order order in query.List<NHFromScratch.Entities.Order>())
  10:        {
  11:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  12:        }
  13:    }
  14: }

Care lansat produce următorul output la console:

 

   1: NHibernate: select order0_.Id as Id3_0_, customer1_.Id as Id2_1_, order0_.OrderCode as OrderCode3_0_, order0_.OrderDate as OrderDate3_0_, order0_.DueDate as DueDate3_0_, order0_.CustomerId as CustomerId3_0_, customer1_.Name as Name2_1_, customer1_.AddressLine1 as AddressL3_2_1_, customer1_.AddressLine2 as AddressL4_2_1_, customer1_.City as City2_1_, customer1_.ZipCode as ZipCode2_1_, customer1_.Active as Active2_1_ from Orders order0_ inner join Customers customer1_ on order0_.CustomerId=customer1_.Id, Customers customer2_ where order0_.CustomerId=customer2_.Id and customer2_.Name=@p0;@p0 = 'Customer A'
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Observăm că în HQL am specificat oricum join fetch-ul în asociaţia o.Customer:asta tot pentru a evita antipattern-ul select N+1.

Acelaşi rezultat îl putem obţine cu API ICriteria, iată unit-testul:

   1: [Test]
   2: public void QueryCustomerOrdersWithCriteria()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var criteria = session.CreateCriteria<NHFromScratch.Entities.Order>()
   8:             .CreateCriteria("Customer")
   9:             .Add(Expression.Eq("Name","Customer A")
  10:             );
  11:  
  12:         foreach (var order in criteria.List<NHFromScratch.Entities.Order>())
  13:         {
  14:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  15:         }
  16:     }
  17: }

Acesta produce următorul output de trace:

 

   1: NHibernate: SELECT this_.Id as Id9_1_, this_.OrderCode as OrderCode9_1_, this_.OrderDate as OrderDate9_1_, this_.DueDate as DueDate9_1_, this_.CustomerId as CustomerId9_1_, customer1_.Id as Id8_0_, customer1_.Name as Name8_0_, customer1_.AddressLine1 as AddressL3_8_0_, customer1_.AddressLine2 as AddressL4_8_0_, customer1_.City as City8_0_, customer1_.ZipCode as ZipCode8_0_, customer1_.Active as Active8_0_ FROM Orders this_ inner join Customers customer1_ on this_.CustomerId=customer1_.Id WHERE customer1_.Name = @p0;@p0 = 'Customer A'
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Deci am putea spune că problema este rezolvată… dar din nefericire nu într-un mod object oriented. Ar fi mai bine să avem o colecţie de comenzi la care obiectul Customer poate accesa cu uşurinţă, ceva de genul customer.Orders. Pentru a face acest lucru ne vine în ajutor cel mai simplu tag de asociaţie pentru a exprima o colecţie de entităţi legate între ele:

<bag/>

Asta ne redă din punct de vedere object oriented ceva ce există deja în baza de date în formă implicită. Dacă în algebra relaţională o FK exprimă de la sine o relaţie bidirecţională, când vorbim despre obiecte această relaţie trebuie explicitată. Deci este necesar să modificăm mapping-ul prin simpla adăgare în clasa Customer al unui <bag/> al Comenzii:

 

 

   1: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   2:                                                       assembly="NHFromScratch.Entities">
   3:   <class name="Customer" table="Customers">
   4:     <id name="Id" type="Int32">
   5:       <generator class="native"/>
   6:     </id>
   7:     <property name="Name" type="String" not-null="true"/>
   8:     <property name="AddressLine1" type="String" not-null="true"/>
   9:     <property name="AddressLine2" type="String" />
  10:     <property name="City" type="String" not-null="true"/>
  11:     <property name="ZipCode" type="String" not-null="true"/>
  12:     <property name="Active" type="Boolean" not-null="true"/>
  13:     <bag name="Orders">
  14:       <key column="CustomerId"/>
  15:       <one-to-many class="Order"/>
  16:     </bag> 
  17:   </class>
  18:   
  19: </hibernate-mapping>

 

Făcând un rebuild al soluţiei, obţinem o diagramă de clase pentru entităţile noastre:

 

clip_image002

Care ne arată de fapt relaţia bidirecţională dintre client şi comenzile sale. Să vedem acum cu un alt unit test cum putem lucra cu un Customer şi obţine comenzile ce aparţin acestuia:

 

   1: [Test]
   2: public void QueryCustomerOrdersWithBag()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     {
   7:         var customerA = session.CreateCriteria<Customer>()
   8:             .Add(Expression.Eq("Name", "Customer A")
   9:             ).UniqueResult<Customer>();
  10:         Console.WriteLine("Obtained customer instance");
  11:         foreach (var order in customerA.Orders)
  12:         {
  13:             Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  14:         }
  15:     }
  16: }

Aceasta produce următorul output la console:

 

   1: Obtained customer instance
   2: NHibernate: SELECT orders0_.CustomerId as CustomerId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.OrderCode as OrderCode3_0_, orders0_.OrderDate as OrderDate3_0_, orders0_.DueDate as DueDate3_0_, orders0_.CustomerId as CustomerId3_0_ FROM Orders orders0_ WHERE orders0_.CustomerId=@p0;@p0 = 1
   3: Order:ORD00000 Customer:Customer A
   4: Order:ORD00001 Customer:Customer A

După cum se observă, colecţia a fost recuperată de la storage doar în momentul necesar. Şi în collection avem un comportament „lazy”, care în multe cazuri este oportun, dar nu întotdeauna. Şi aici putem modifica comportamentul lazy specificându-l în mapping:

 

   1: <bag name="Orders" fetch="join">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Recompilând testul de dinainte obţinem un output uşor diferit:

 

   1: Obtained customer instance
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

 

 

Nici un query: o dată cu încărcarea customer-ului a fost încărcată şi asociaţia cu collection a comenzii, query-ul a fost de fapt următorul:

 

   1: SELECT this_.Id ... orders2_.CustomerId as CustomerId3_... FROM Customers this_ left outer join Orders orders2_ on this_.Id=orders2_.CustomerId WHERE this_.Name = @p0;@p0 = 'Customer A'

 

In toate cazurile, bag-ul care ne soseşte este o listă potenţial dezordonată. Putem cere NH-ului să facă, sau mai bine spus să ceară bazei de date să facă, o ordonare în baza unei anumite proprietăţi a entităţii colecţionate. In exemplul nostru am putea vrea o ordonare în funcţie de data comenzii, adăugând la mapping acest atribut:

 

   1: <bag name="Orders" fetch="join" order-by="OrderDate" >

 

Şi obţinând deci interogaţia următoare:

 

1: SELECTFROM Customers this_ left outer join Orders orders2_ on this_.Id=orders2_.CustomerId WHERE this_.Name = @p0

ORDER BY orders2_.OrderDate;

@p0 = 'Customer A'

 

Cu oportuna clauză id ordonare. Să vedem acum ce se întâmplă făcând nişte teste cu HQL. Primul:

 

   1: [Test]
   2: public void QueryCustomerOrdersWithBagUsingHQL()
   3: {
   4:    CreateSomeOrdersAndCustomers();
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    {
   7:        IQuery query = session.CreateQuery("from Customer c where c.Name=:name")
   8:            .SetParameter("name", "Customer A");
   9:        
  10:        var list = query.List<Customer>();
  11:        Assert.AreEqual(1, list.Count);
  12:        var customerA = list[0];
  13:        foreach (var order in customerA.Orders)
  14:        {
  15:            Console.WriteLine("Order:{0} Customer:{1}", order.OrderCode, order.Customer.Name);
  16:        }
  17:    }
  18: }

 

Observăm că numărul de query revine la un comportament lazy:

 

 

   1: select customer0_.Id as Id2_, customer0_.Name as Name2_, customer0_.AddressLine1 as AddressL3_2_, customer0_.AddressLine2 as AddressL4_2_, customer0_.City as City2_, customer0_.ZipCode as ZipCode2_, customer0_.Active as Active2_ from Customers customer0_ where customer0_.Name=@p0;@p0 = 'Customer A'
   2: SELECT orders0_.CustomerId as CustomerId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.OrderCode as OrderCode3_0_, orders0_.OrderDate as OrderDate3_0_, orders0_.DueDate as DueDate3_0_, orders0_.CustomerId as CustomerId3_0_ FROM Orders orders0_ WHERE orders0_.CustomerId=@p0 ORDER BY orders0_.OrderDate;@p0 = 1
   3: Order:ORD00000 Customer:Customer A
   4: Order:ORD00001 Customer:Customer A

 

Cu toate acestea, putem specifica şi în HQL politica fetch, modificând HQL-ul in: “from Customer c join fetch c.Orders where c.Name=:name”. Din nefericire testul eşuează în mod jalnic:

 

clip_image002[5]

 

Acesta este un comportament „ciudat” al NH-ului. De fapt există un singur Customer în testele noastre care satisface relaţia cerută, totuşi efectuând join-ul, ne sunt restituite mai multe record-uri (n record-uri, unde n reprezintă numărul comenzilor). Trebuie menţionat că NH nu creează instanţe diferite ale obiectelor, ci pune în lista output-ului nişte references ale aceleiaşi entităţi.

Se poate remedia la această problemă cu un “ResultTransformer”, adică un post-processor al rezultatelor din NH. Putem construi nişte post processori (IResultTransformer ) pentru a manipula în modul pe care-l considerăm oportun rezultatele unei ICriteria sau cele ale unui HQL. NH furnizează anumiţi transformer de utilităţi, unul dintre aceştia ne este util în această situaţie complicată:

 

   1: IQuery query = session.CreateQuery("from Customer c join fetch c.Orders where c.Name=:name")
   2:     .SetParameter("name", "Customer A")
   3:     .SetResultTransformer(Transformers.DistinctRootEntity)
   4:     ;

Această transformare face ca entităţile „identice„ să devină una singură, şi iată că liniile întoarse sunt acelea pe care le aşteptam, iar query-ul devine unul singur, după ce am specificat un comportament non lazy.

Ce se întâmplă dacă încercăm să înlăturăm nişte comenzi de la client? ( atenţie, în realitate acest lucru nu se va întâmpla aproape niciodată: vor exista probabil anumite strategii soft-delete, aici facem doar nişte exemple ) Iată un unit test pentru a testa acest scenariu:

 

   1: [Test]
   2: public void RemoveOrderForCustomers()
   3: {
   4:     CreateSomeOrdersAndCustomers();
   5:     using (ISession session = sessionFactory.OpenSession())
   6:     using(ITransaction trns = session.BeginTransaction() )
   7:     {
   8:         var customerA = session.CreateCriteria<Customer>()
   9:            .Add(Expression.Eq("Name", "Customer A")
  10:            ).UniqueResult<Customer>();
  11:         Console.WriteLine("Obtained customer instance");
  12:         customerA.Orders.RemoveAt(0);
  13:         trns.Commit();
  14:     }
  15: }

Dacă lansăm acest test, obţinem următoarea eroare:

   1: NHFromScratch.Tests.TestOrders.RemoveOrderForCustomers:
   2: NHibernate.Exceptions.GenericADOException : could not delete collection rows: [NHFromScratch.Entities.Customer.Orders#1][SQL: UPDATE Orders SET CustomerId = null WHERE CustomerId = @p0 AND Id = @p1]
   3:   ---->; System.Data.SqlClient.SqlException : Cannot insert the value NULL into column 'CustomerId', table 'NHFROMSCRATCH.dbo.Orders'; column does not allow nulls. UPDATE fails.
   4: The statement has been terminated.

Practic NH încearcă să „desprindă” comanda eliberând-o de Customer, dar noi am specificat faptul că Customer-ul nu nu poate fi null, deci logica e corectă. Singurul mod este să spunem că owner-ul acestei asociaţii este Customer, introducând inverse=”true” cu intenţia de a spune că „partea”asociaţiei nu este responsabilă de colecţie, astfel:

 

 

   1: <bag name="Orders" fetch="join" order-by="OrderDate" inverse="true">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Din păcate testul continuă să nu funcţioneze, de această dată ne zice că nimic nu a fost anulat:

clip_image004

Ceea ce putem face este să forţăm anularea cu parametrul „cascade”:

 

   1: <bag name="Orders" fetch="join" order-by="OrderDate" inverse="true" cascade="all-delete-orphan">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

Şi iată că în sfârşit testul funcţionează:

clip_image006

Parametrul cascade are efecte similare şi pentru operaţiunile de introducere a noilor comenzi: acestea vor fi salvate automat în momentul salvării entităţii Customer.

Înainte de a încheia această parte merită să aruncăm o privire asupra unui comportament pe care este bine să-l cunoaştem date fiind problematicile de optimizare. Să presupunem că vrem să ştim câte comenzi are un Customer, şi că avem asociaţia în modalitatea lazy:

 

   1: [Test]
   2:    public void OrdersCount()
   3:    {
   4:        CreateSomeOrdersAndCustomers();
   5:        using (ISession session = sessionFactory.OpenSession())
   6:        {
   7:            var customerA = session.CreateCriteria<Customer>()
   8:               .Add(Expression.Eq("Name", "Customer A")
   9:               ).UniqueResult<Customer>();
  10:            Console.WriteLine("Obtained customer instance");
  11:            Assert.AreEqual(2, customerA.Orders.Count);
  12:        }
  13:       
  14: }

si uitaţi ce query curat se execută:

 

NHibernate: SELECT count(Id) FROM Orders WHERE CustomerId=@p0;@p0 = 1

 

 

Bun, şi această parte s-a terminat, nu au fost tratate toate combinaţiile posibile de atribute pentru <bag/>, dar s-a pus accentul pe anumite probleme clasice, minimul pentru a putea începe experimentarea pe cont propriu. Două ultime lucruri:

  • Până în acest moment echivalenţa clasă-tabel s-a manifestat întotdeauna, dar nu va fi mereu aşa.
  • Să ne uităm la query-urile pe care le generează NH iniţial pentru a înţelege ce se întâmplă, dar tot mai mult trebuie să învăţăm să dăm ca sigur comportamentul şi să ne uităm la query-uri pentru motive de optimizare.

Descarca Sursa
Parte 3
Saturday, 25 September 2010 11:54:55 (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

My Stack Overflow
Contacts

Send mail to the author(s) E-mail

Tags
profile for Felice Pollano at Stack Overflow, Q&A for professional and enthusiast programmers
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2019
Felice Pollano
Sign In
Statistics
Total Posts: 157
This Year: 0
This Month: 0
This Week: 0
Comments: 127
This blog visits
All Content © 2019, Felice Pollano
DasBlog theme 'Business' created by Christoph De Baene (delarou) and modified by Felice Pollano