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

continua da Parte2: Creare un progetto

Quando una entità del nostro modello ha una proprietà del tipo di una altra entità del dominio, cioè ha un riferimento ad una altra entità, possiamo esplicitare questo concetto nel file di mapping con il tag <many-to-one/>. Continuando ad arricchire il modello iniziato nella Parte2, decidiamo di inserire l’entità ordine, e poichè un ordine ha necessariamente un cliente, potremo vedere come esplicitare questo concetto. Nella vita l’entità ordine sarà più complessa, ma per il momento ci accontentiamo di vedere la relazione tra l’ordine e chi lo ha fatto.

Aggiungiamo un file di mapping così fatto:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHFromScratch.Entities" 
   3:                                                       assembly="NHFromScratch.Entities">
   4:   <class name="Order" table="Orders">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="OrderCode" type="AnsiString" />
   9:     <property name="OrderDate" type="DateTime"/>
  10:     <property name="DueDate" type="DateTime"/>
  11:     <many-to-one name="Customer" class="Customer" column="CustomerId"/>
  12:     <!-- details comes here -->
  13:   </class>
  14:   
  15: </hibernate-mapping>

Come si vede abbiamo aggiunto un paio di proprietà di tipo data, ma importante novità c’è il tag <many-to-one/> che ci dice in questa minimale definizione:

  • Che nome ha la proprietà con l’attributo “name”
  • Qual’è l’entità esterna,cioè il suo tipo ( l atabella non ci interessa più )
  • Opzionalmente il nome della colonna ( in pratica il nome della colonna che sarà una foreign-key nella tabell aorder VS la tabella customer ) con l’attributo “column”.

Se non esplicitassi column, il nome sarebbe lo stesso della proprietà. Per inciso: column applica anche alle proprietà semplici, con la medesima semantica.

Bene, ora ricordiamoci di mettere “embedded resource” al nuovo file hbm, che chiameremo fantasiosamente Order.hbm.xml, lanciamo la compilazione per ottenere una prima versione del file .cs order.generated.cs, che aggiungeremo al progetto e compileremo.

s1

Fatto questo possiamo lanciare gli unit test, e ricreare il database:

s2

Il test “vira al verde” e vediamo come la tabella order compaia nello script DDL, e come la foreign key che punta al Customer sia inserita opportunamente nella tabella order. I più attenti poi abvranno notato come la proprietà code sia stata definita come VARCHAR, mentre le altre colonne stringa nella tabella customer siano invece NVARCHAR, questo è provocato dalla scelta di utilizzare come tipo per la stringa OrderCode AnsiString anzichè string. Questo non ha nessun effetto nel codice C#, ma cambia la DDL del database, quindi scegliete la più opportuna. Altra cosa importante, la FK è nullabile, questo non è molto bello poichè abbiamo detto che obbligatoriamente un Order deve avere un Customer associato: basta usare l’attributo not-null = “true” aggiunto al tag <many-to-one/>.

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" not-null="true"/>

Questo modificherà la DDL come qui di seguito:

   1: create table Orders (
   2:        Id INT IDENTITY NOT NULL,
   3:       OrderCode VARCHAR(255) null,
   4:       OrderDate DATETIME null,
   5:       DueDate DATETIME null,
   6:       CustomerId INT not null,
   7:       primary key (Id)
   8:    )

 

Alla fine il nostro nuovo DB avrà questo schema:

Senza nome

A questo punto possiamo di creare qualche test, per provare il modello come funziona. Diciamo che vogliamo creare dei test ripetibili, quindi la nostra TestFixture ricreerà tutte le volte un DB da zero, così ogni test sarà indipendente. Il primo test interessante potrebbe essere la creazione di un ordine, con contestualmente un nuovo cliente. Questa volta vedremo meglio il dettaglio delle varie operazioni che andremo a fare. Ecco il codice del test che proveremo a fare:

 

 

   1: public void CreateAnOrderWithANewCustomer()
   2: {
   3:     using(ISession session = sessionFactory.OpenSession() )
   4:     using (ITransaction transaction = session.BeginTransaction())
   5:     {
   6:         Order order = new Order();
   7:         order.Customer = new Customer()
   8:         {
   9:             Active = true
  10:             ,
  11:             AddressLine1 = "xxxxxxx xxx xxxx"
  12:             ,
  13:             AddressLine2 = "xx xxxxxxx xxxxx"
  14:             ,
  15:             City = "Milano"
  16:             ,
  17:             Name = "Customer Name"
  18:             ,
  19:             ZipCode = "00000"
  20:         };
  21:         order.OrderCode="ORD00000";
  22:         order.OrderDate = DateTime.Now;
  23:         order.DueDate = order.OrderDate.Value.AddMonths(2);
  24:         session.SaveOrUpdate(order);
  25:         transaction.Commit();
  26:     }
  27: }

Come prima cosa creiamo una “Sessione”, qualcosa che assomiglia ad una connessione al DB, ma è qualcosa di più, è un unità di lavoro. All’interno di un unityà di lavoro le entità diventano degne di questo nome ed NHibernate ne traccia le modifiche e si occupa di garantirne l’unicità a parità di identificativo DB, e tante altre cose. Diciamo qualche buona norma sulla session:

  • Costa poco creare una session ( diversamente dal Session Factory )
  • Cercate di far durare poco le sessioni da un punto di vista temporale
  • Limitate il numero di entità con cui lavora una sessione
  • La session non è multithread.

Immediatamente dopo abbiamo aperto una transazione, perchè andremo alla fine a persistere qualcosa di nuovo nel database, e vogliamo fare tutte le operazioni nella nostra unità di lavoro ( UOW = unit of work ) in modo consistente ed atomico. In una sola UOW è possibile avere più transazioni.

Lanciamo il test e vediamno cosa succede nel text output di NUnit, per vedere cosa fa NH sul DB. Sfortunatamente il test “vira al rosso”:

s3

 

Questo è il famigerato errore:

“NHFromScratch.Tests.TestOrders.CreateAnOrderWithANewCustomer:
NHibernate.PropertyValueException : not-null property references a null or transient valueNHFromScratch.Entities.Order.Customer”

Significa in pratica che stiamo cercando di salvare un entità ( Order ) che ha una referenza ad un’altra entità, ma quest’ultima non è persistente ( cioè transitoria=Transient ). Per risolvere questo problema abbiamo due strade:

Salvare l’entità riferita ( se quest’ultima è ancora transitoria ) prima di salvare l’entità ordine:

   1: session.SaveOrUpdate(order.Customer);
   2: session.SaveOrUpdate(order);
   3: transaction.Commit();

Questo fa diventare il test “verde”, e queste sono le query che sono inviate al DB:

   1: ***** NHFromScratch.Tests.TestOrders.CreateAnOrderWithANewCustomer
   2: NHibernate: INSERT INTO Customers (Name, AddressLine1, AddressLine2, City, ZipCode, Active) VALUES (@p0, @p1, @p2, @p3, @p4, @p5); select SCOPE_IDENTITY();@p0 = 'Customer Name', @p1 = 'xxxxxxx xxx xxxx', @p2 = 'xx xxxxxxx xxxxx', @p3 = 'Milano', @p4 = '00000', @p5 = True
   3: NHibernate: INSERT INTO Orders (OrderCode, OrderDate, DueDate, CustomerId) VALUES (@p0, @p1, @p2, @p3); select SCOPE_IDENTITY();@p0 = 'ORD00000', @p1 = 11/09/2010 20.48.24, @p2 = 11/11/2010 20.48.24, @p3 = 1

L’altro modo è quello di agire sull’attributo “cascade” del tag <many-to-one/>, per esempio modificandolo così:

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

Nel test possiamo dinuovo limitarci a salvare Order, ci pensa NH, grazie all’attributo “cascade” a fare tutte le considerazioni per mantenere consistente l’operazione sul database.

Adesso ci spostiamo alla parte lettura, creiamo un test con un paio di clienti e qualche ordine, e proviamo a fare delle estrazioni di entità già persistite. Supponiamo di creare una base di entità, in maniera del tutto analoga a quella sopra, creando due clienti, Customer A e Custome B, rispettivamente con 2 ed 1 ordine ciascuno. Il primo test prevede il recupero delle entità persistite tramite la creazione di un ICriteria. Ecco il codice del test:

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

Come si vede c’è un bel foreach che stampa dei dettagli dell’ordine e del cliente ad esso associato. Se però vediamo le query che fa NH scopriamo un problemino:

nplusone

In pratica, la List forza l’esecuzione della prima query, ma tutte le volte che andiamo ad indagare la proprietà “Customer” ecco che viene sparata una nuova query al DB, per recuperare l’entità riferita. Malgrado NH sia così bravo da non effettuare la query per lo stesso customer 2 volte, quello che accade non è comunque un comportamento ideale. Questo accade perchè l’associazione many-to-one è per default “lazy”: questo può essere utile in tantissimi casi, ma se, come nel nostro caso, sappiamo in anticipo che vogliamo avere tutte le associazioni, stiamo ricadendo nell’ anti patter Select N+1. Fortunatamente possiamo evitarlo, in primo modo agendo sulle proprietà di ICriteria:

   1: var criteria = session.CreateCriteria<Order>()
   2: .SetFetchMode("Customer",FetchMode.Eager)
   3: ;

Cambiare il modo di fetch dell associativa cambia drammaticamente le query eseguite, che si riducono ad una:

   1: SELECT this_.Id as Id3_1_, ... FROM Orders this_ inner join Customers customer2_ on this_.CustomerId=customer2_.Id

Questa è un ottimizzazione che dobbiamo sempre cercare di fare. Potevamo ottenere lo stesso risultato solo aggiungendo l’attributo “fetch” nel tag <many-to-one/>:

   1: <many-to-one name="Customer" class="Customer" column="CustomerId" fetch="join" not-null="true" cascade="save-update"/>

Ovviamente, in uno scenario in cui volessimo cambiare il fetch-mode all’inverso, potremmo impostare via codice il SetFetchMode a select.

Lasciamo ora il fetch mode a join e proviamo a fare la stessa query con HQL:

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

Scopriremo che HQL ignora il tipo di fetch specificato dall’attributo fetch di <many-to-one/> ! Questo perchè HQL prevede la possibilità di specificare il fetch mode letteralmente:

   1: var query = session.CreateQuery("from Order o join fetch o.Customer");

Questa modifica ci porta dinuovo all’unica query per tutte e tre le istanze.

Con questo abbiamo finito la definizione di <many-to-one/> e, visto che questo è un corso da *zero* non abbiamo visto uno per uno tutti gli attributi di many-to-one, ma una quantità bastante a iniziare ad usarlo in modo efficace.

Diciamo in ultimo 2 cose:

  1. Mappare una FK di db con il corrispondente tipo chiave è un grave antipattern.
  2. Una FK nel database lega implicitamente 2 tabelle, qui abbiamo specificato uno dei lati dell’associazione, l’altro lato lo vedremo nella prossima parte.

 

Download Sorgenti
Parte 2

Parte 4

Saturday, September 11, 2010 10:36:29 PM (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
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 2012
Felice Pollano
Sign In
Statistics
Total Posts: 151
This Year: 11
This Month: 0
This Week: 0
Comments: 121
This blog visits
All Content © 2012, Felice Pollano
DasBlog theme 'Business' created by Christoph De Baene (delarou) and modified by Felice Pollano