The official Fatica Labs Blog! RSS 2.0
# Saturday, October 16, 2010
Continua da parte 5

In questa parte ci fermiamo un attimo ad analizzare meglio come e quando attivare la sessione (ISession) e quali siano le modalità più corrette per farlo. Vediamo per grandi linee cosa fa Session:

  1. Consente di recuperare salvare ed eliminare le entità.
  2. Mantiene una mappa delle entità organizzata come reference/ID Database
  3. Gestisce una cache ( first level cache ) che, tramite la mappa di cui sopra, ci evita di andare sul database per recuperare un entità che abbiamo già in memoria
  4. Tiene traccia delle entità modificate
  5. Gestisce le interazioni con il driver ADO.NET sottostante, comprese l’apertura chiusura delle connessioni.
  6. Ci consente di aprire e chiudere le transazioni.

Il punto 2 merita un commento: tra le difformità concettuali tra database e oggetti .NET vi è l’identità dell’oggetto. Nel database la relazione di identità è in ultima analisi garantita dall’ID della riga, mentre in .NET l’identità di due oggetti è in ultima analisi riconducibile al confronto fra due reference. NHibernate vuole evitare che ci siano queste ambiguità, e asserisce che ci debba essere una sola reference che punti ad un determinato oggetto persistent: anche se creiamo un oggetto clone di un oggetto persistent e cerchiamo, per esempio, di cancellarlo o di modificarlo, questo clone non sarà accettato da NHIbernate.

In pratica, quando ci serve di interagire con gli oggetti persistent, abbiamo necessita di lavorare nell’ambito di una sessione. Questo introduce spesso degli errori quando cerchiamo il momento opportuno per aprire la sessione:

Open Session: Pratiche da evitare
  1. Evitare di aprire la session per ogni singola interazione che si vuole avere con lo strato di persistenza. ( Session-per-operation Antipattern )
  2. Evitare di utilizzare una sola session per l’intera durata dell’ applicazione. ( Session-per-application Antipattern )

Già lo abbiamo detto, ma è meglio ricordarlo visto che siamo in zona di cose da evitare: se la session ha comunque una certa agilità di uso, la SessionFacory deve essere aperta una volta sola all’interno di un app domain. Non che ci sia qualcosa che non funziona, ma semplicemente la session factory è lenta a caricarsi.

Se si usa la prima strategia, ci si scontra violentemente con il fatto che NH non vuole “mixare” entità appartenenti a più sessioni,  si perde qualsiasi beneficio della cache di primo livello, e gli oggetti lazy tendono a produrre il famigerato errore “NHibernate.LazyInitializationException: illegal access to loading collection”, dovuto al fatto che la sessione che ha creato l’ogegtto ormai l’abbiamo chiusa, e NH non può andare a fare il recupero degli oggetti della collection.

La seconda strategia se possibile è anche peggiore: lavoriamo con dati in “stallo”, cioè possibilmente non aggiornati con i rispettivi oggetti persistiti, la session si appesantisce ( NH diventa sempre più lento se gli oggetti trattati da una session sono troppi ), non si assicura più una corretta politica di gestione della connessione al DB,che deve essere aperta  per il tempo strettamente necessario e quindi chiusa.

La soluzione consigliata è la Session per Business Transaction. Una business transaction è, tanto per fare un esempio: “leggo  la prima pagina dei clienti”, “Inserisco un nuovo ordine”, “cerco tutti i prodotti in magazzino che iniziano per XXX”, ecc. Quindi operazioni autocontenute, ma non triviali. Nelle applicazioni ci possono essere delle leggere differenze tra rich client e Web application, in queste ultime la realizzazione di questo pattern può convergere nella Session Per Request, che consiste nell’aprire una session quando si verifica l’evento Begin request, e di chiuderla al succesivo End. Questo approccio in generale è pressoche equivalente al Session Per business transaction, perchè di fatto in un applicativo web un ciclo request/response coincide con una business transaction.

Possiamo utilizzare per questo scopo il pattern Unit of Work. Nei sorgenti di esempio ne ho messo una versione semplice, che utilizza le ContextSession di NH. In pratica le context session ci consentono di condividere una sessione tra più funzioni del nostro applicativo, in relazione ad un contesto che può essere, per esempio il Thread corrente, oppure l’HttpContext della chiamata al web server, o qualunque altra cosa, configurabilmente. Vediamo i sorgenti che così è più chiaro:

   1: public class NHUow:IDisposable
   2: {
   3:     private NHUow()
   4:     {
   5:         if (CurrentSessionContext.HasBind(sessionFactory))
   6:             throw new InvalidOperationException("Nested unit of work not allowed.");
   7:         CurrentSessionContext.Bind(sessionFactory.OpenSession());
   8:     }
   9:     
  10:     static object lockObject = new object();
  11:     public static NHUow Open()
  12:     {
  13:         lock (lockObject)
  14:         {
  15:             if (sessionFactory == null)
  16:                 CreateSessionFactory();
  17:         }
  18:         return new NHUow();
  19:     }
  20:     public static ISession CurrentSession
  21:     {
  22:         get { return sessionFactory.GetCurrentSession(); }
  23:     }
  24:  
  25:     private static ISessionFactory sessionFactory;
  26:     private static void CreateSessionFactory()
  27:     {
  28:         Configuration cfg = CreateConfiguration();
  29:         sessionFactory = cfg.BuildSessionFactory();
  30:     }
  31:  
  32:     private static Configuration CreateConfiguration()
  33:     {
  34:         Configuration cfg = new Configuration();
  35:         cfg.Configure();
  36:         // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
  37:         // contiene customer
  38:         cfg.AddAssembly(typeof(Customer).Assembly);
  39:         return cfg;
  40:     }
  41:  
  42:  
  43:     #region IDisposable Members
  44:  
  45:     public void Dispose()
  46:     {
  47:         if (!CurrentSessionContext.HasBind(sessionFactory) )
  48:         {
  49:             throw new InvalidOperationException("Invalid current session");
  50:         }
  51:         var session = sessionFactory.GetCurrentSession();
  52:         CurrentSessionContext.Unbind(sessionFactory);
  53:         if (!session.IsOpen)
  54:         {
  55:             throw new InvalidOperationException("Session closed before UOW end");
  56:         }
  57:        
  58:         if ( null != session.Transaction && session.Transaction.IsActive )
  59:         {
  60:             if (session.Transaction.WasCommitted == false && session.Transaction.WasRolledBack == false)
  61:                 session.Transaction.Rollback();
  62:         }
  63:         session.Close();
  64:     }
  65:  
  66:     #endregion
  67: }

L’oggetto implementa IDisposable, e ci permette di utilizzaro in questo modo:

   1: using (var uow = NHUow.Open())
   2: {
   3:     var transaction = NHUow.CurrentSession.BeginTransaction();
   4:     Customer c = new Customer();
   5:     c.Name = "FELIX";
   6:     c.AddressLine1 = c.AddressLine2 = "";
   7:     c.City = "XX";
   8:     c.ZipCode = "12060";
   9:     NHUow.CurrentSession.Save(c);
  10:     transaction.Commit();
  11: }

In pratica apro una unità di lavoro con using new… e, usciti dallo scope la uow si chiude. All’interno di questo scope, tutto il nostro codice potrà accedere alla session corrente usango la funzione statica NHUow.CurrentSession. I più attenti noteranno che la configurazione del session factory è fissa: questo è per semplificare la clase UOW, per renderla generale basta intervenire aggiungendo un SessionFactoryFactory, che UOW utilizzera per recuperare il session factory esterno.

Bene, se a questo punto lanciassimo lo unit test di prova, otterremmo la seguente eccezione:

No CurrentSessionContext configured (set the property current_session_context_class)!

Questo perchè il meccanismo di mantenimento delle sessioni di contesto di NH è configurabile, e noi non abbiamo specificato nulla. Come dice il messaggio di errore, bisogna aggiungere nel file di config la linea current_session_context_class ( linea 11 ):

   1: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   2:   <session-factory name="NHibernate.Test">
   3:     <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   4:     <property name="connection.connection_string">Server=.\SQLEXPRESS;initial catalog=NHFROMSCRATCH;Integrated Security=SSPI</property>
   5:     <property name="adonet.batch_size">10</property>
   6:     <property name="show_sql">true</property>
   7:     <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
   8:     <property name="use_outer_join">true</property>
   9:     <property name="command_timeout">60</property>
  10:     <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
  11:     <property name="current_session_context_class">thread_static</property>
  12:     <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
  13:   </session-factory>
  14: </hibernate-configuration>

Notate che non ho inserito un nome di classe, questo perchè ci sono degli shortcut per i context manager più usati. Tanto per elecare i vari possibili context manager:

  • NHibernate.Context.WebSessionContext – la sessione corrente è contenuta in HttpContext. L’alias è web.
  • NHibernate.Context.CallSessionContext - la sessione corrente è contenuta CallContext. L’alias è call.
  • NHibernate.Context.ThreadStaticSessionContext – la sessione corrente è nel context del thread. L’alias è thread_static.

Ovviamente, sempre nella filosofia della massima estendibilità, possiamo scrivere il nostro context manager.

Nei sorgenti di esempio, oltre alla classe Uow, c’è una classe di Unit Test NHFromScratch.Tests.Uow, che ne mostra l’utilizzo.

 

Torna alla parte 5
Download Sorgenti
Saturday, October 16, 2010 5:48:51 PM (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Monday, September 27, 2010
Continua da parte 4

In questa parte vedremo un altro tipo di collezione, e di come in generale possiamo collezionare oggetti che non sono entità, cioè oggetti che non gestiscono per conto proprio una identità, e che ha senso che esistano solo se collegati all’entità contenitrice. Per fare questo aggiungiamo al nostro modello l’entità Prodotto, che ci servirà comunque per altre cose, e supponiamo di avere un semplicissimo magazzino, organizzato a celle, contenenti la giacenza del prodotto in questione. Diciamo anche che per identificare la cella ci basta avere una stringa. Nella realtà un magazzino sarà molto più complesso, ma questa semplificazione è un pretesto per mostrare la strategia di collezione MAP, ed in una realtà molto piccola potrebbe anche avere un senso così come fatto.

Aggiungiamo quindi il mapping della entità Prodotto, con annessa la mappa delle locazioni di magazzino e relative quantità:

   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="Product" table="Products">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Code" type="AnsiString" />
   9:     <property name="Description" type="AnsiString"/>
  10:     <map name="Warehouse">
  11:       <key column="Product"/>
  12:       <index column="Cell" type="AnsiString"/>
  13:       <element column="Quantity" type="Int32"></element>  
  14:     </map>
  15:     <!-- details comes here -->
  16:   </class>
  17:   
  18: </hibernate-mapping>
  19:  

Vediamo come è definita <map/>, o meglio, un modo di definire una mappa, in questo caso una mappa indicizzata da una stringa che identifica la cella di magazzino, ed un numerico che indica la quantità del prodotto presente in tale cella. Il tag key è il riferimento, nella tabella che contiene la map, alla entità madre ( in questo caso Product ), poi occorre un indice, in questo caso è la “Cell” del magazzino, e il valore, in questo caso la Quantity del prodotto. Lanciando lo unit test di creazione del database otteniamo, per la parte che ci interessa, il seguente schema:

image

Se andiamo ad indagare meglio la DDL notiamo, nella parte di creazione della tabella warehouse, il seguente script:

clip_image001

La chiave primaria della tabella è quindi costituita dalla FK alla tabella parent ( Product ) combinata con la componente “Index” – in questo caso Cell –. No potremo avere, per un prodotto, due informazioni relative alla medesima cella, o locazione di magazzino.

Supponiamo che per localizzare la posizione in magazzino siano necessarie invece due informazioni, per esempio la riga e la colonna, e che oltre che la quantità ci interessi anche sapere la data a cui questa quantità è stata stoccata, possiamo modificare il mapping come di seguito:

   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="Product" table="Products">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Code" type="AnsiString" />
   9:     <property name="Description" type="AnsiString"/>
  10:     <map name="Warehouse">
  11:       <key column="Product"/>
  12:       <composite-index class ="WarehouseLocation">
  13:         <key-property name="CellRow" type="AnsiString" />
  14:         <key-property name="CellCol" type="AnsiString" />
  15:       </composite-index>
  16:       <composite-element class="StockInformation">
  17:         <property name="StockDate" type="DateTime"/>
  18:         <property name="Quantity" type="Int32"/>
  19:       </composite-element>
  20:     </map>
  21:     <!-- details comes here -->
  22:   </class>
  23:   
  24: </hibernate-mapping>

In pratica abbiamo sostituito “<index>” con “<composite-index>” e “<element>” con “<composite-element>”. Ovviamente possiamo avere anche scenari in cui ad essere composito è solo l’indice, o solo l’element. Appena lanciata la compilazione il nostro hbm2net genera qualche file in più:

clip_image001[5]

In pratica abbiamo una classe per rappresentare la “chiave” ( WarehouseLocation ) nella mappa e una per rappresentare l’”element” ( StockInformation ). La classe utilizzata per la chiave deve poter agire come tale, e pertanto deve effettuare opportuni ovveride dei metodi EqualsGetHashCode, pena comportamenti impredicibile del codice. Utilizzando hbm2net questi gener ail codice opportuno:

   1: public override bool Equals(object other)
   2: {
   3:     if (this == other)
   4:         return true;
   5:     WarehouseLocation rhs = other as WarehouseLocation;
   6:     if (rhs == null) 
   7:         return false; // null or not a WarehouseLocation
   8:     return _cellRow.Equals(rhs._cellRow) && _cellCol.Equals(rhs._cellCol);
   9: }
  10:  
  11: public override int GetHashCode()
  12: {
  13:     return _cellRow.GetHashCode() ^ _cellCol.GetHashCode();
  14: }

Aggiungendo queste due classi al progetto possiamo rigenerare il database, che risulta così modificato:

clip_image001[7]

Il diagramma classi diventa, per la parte product, il seguente:

clip_image001[9]

E’ interessante notare che dal punto di vista OR/M StockInformation e WarehouseInformation *non* sono entità. In questa parte del corso si è quindi totalmente smentità la presunta corrispondenza 1:1 tra entità e tabelle.

 

Ora scriviamo il solito unit test per vedere come si comporta NH durante il CRUD su questa collection. Non torneremo a ripetere il discorso “lazy” in quanto analogo a quello visto nella parte 4 per <bag/>.

Il codice dello unit test che lanceremo è il seguente:

   1: [Test]
   2: public void TestCrud()
   3: {
   4:    Console.WriteLine("Creating a product");
   5:    using (ISession session = sessionFactory.OpenSession())
   6:    using (ITransaction trns = session.BeginTransaction())
   7:    {
   8:        Product p = new Product();
   9:        p.Code = "P00000";
  10:        p.Description = " a sample product";
  11:        Console.WriteLine("Adding some Warehouse information");
  12:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  13:        p.Warehouse.Add(new KeyValuePair<WarehouseLocation, StockInformation>(l1, new StockInformation()));
  14:        p.Warehouse[l1].Quantity = 12;
  15:        p.Warehouse[l1].StockDate = DateTime.Now;
  16:        var l2 = new WarehouseLocation() { CellCol = "A", CellRow = "2" };
  17:        p.Warehouse.Add(new KeyValuePair<WarehouseLocation, StockInformation>(l2, new StockInformation()));
  18:        p.Warehouse[l2].Quantity = 42;
  19:        p.Warehouse[l2].StockDate = DateTime.Now.AddDays(-7);
  20:        Console.WriteLine("Persisting the Product");
  21:        session.SaveOrUpdate(p);
  22:        trns.Commit();
  23:    }
  24:    Console.WriteLine("Modifying warehouse information of a product");
  25:    using (ISession session = sessionFactory.OpenSession())
  26:    using (ITransaction trns = session.BeginTransaction())
  27:    {
  28:        Console.WriteLine("Retrieving product by code");
  29:        var p = session.CreateCriteria<Product>()
  30:            .Add(Expression.Eq("Code", "P00000"))
  31:            .UniqueResult<Product>();
  32:        
  33:        Console.WriteLine("Modify stored quantity on cell A1");
  34:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  35:        
  36:        p.Warehouse[l1].Quantity -=3;
  37:        p.Warehouse[l1].StockDate = DateTime.Now;
  38:        
  39:        Console.WriteLine("Persisting the Product");
  40:        session.SaveOrUpdate(p);
  41:        trns.Commit();
  42:    }
  43:    Console.WriteLine("deleting warehouse information of a product");
  44:    using (ISession session = sessionFactory.OpenSession())
  45:    using (ITransaction trns = session.BeginTransaction())
  46:    {
  47:        Console.WriteLine("Retrieving product by code");
  48:        var p = session.CreateCriteria<Product>()
  49:            .Add(Expression.Eq("Code", "P00000"))
  50:            .UniqueResult<Product>();
  51:  
  52:        Console.WriteLine("Modify stored quantity on cell A1");
  53:        var l1 = new WarehouseLocation() { CellCol = "A", CellRow = "1" };
  54:  
  55:        p.Warehouse.Remove(l1);
  56:  
  57:        Console.WriteLine("Persisting the Product");
  58:        session.SaveOrUpdate(p);
  59:        trns.Commit();
  60:    }
  61: }

Che produce il seguente risultato. Si sono evidenziati i punti in cui si vede come NH accede agli elementi della collezione:

image

Come si vede, map è molto efficiente ad accedere in update/delete a singoli elementi della collezione. Per contro, visto che la struttura è rappresentata in memoria tramite un dictionary, non è possibile ordinarla ( anche se si facesse la query con un order-by, il funzionamento interno del dictionary potrebbe mutare questo ordine ).

Download Sorgenti
Parte 4
Parte 6
Monday, September 27, 2010 5:09:49 PM (GMT Daylight Time, UTC+01:00)  #    Comments [0] - Trackback
hbm2net | NHibernate | NHibernate Tutorial

# Saturday, September 18, 2010

continua da Parte3: <many-to-one/>

Sempre continuando a lavorare sul sistema ordini minimale che stiamo analizzando, ci potrebbe tornare utile sapere quali ordini sono associati ad un particolare Customer. Lasciando così come è il modello, possiamo già farlo, sia tramite HQL che tramite ICriteria. Ricordiamo che questo è il corso NHibernate da *ZERO* per cui non si usa LINQ to NH, ne qualsiasi altra API esterna ad NHIbernate, non perchè queste siano o meno efficaci, solo perchè si vuole analizzare il comportamento base. Molto semplicemente facciamo i due nuovi unit test, il primo con 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: }

Che lanciato produce il seguente output a 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

Notiamo che nell’ HQL abbiamo comunque specificato il join fetch sull’associazione o.Customer: questo sempre per evitare l’antipattern select N+1.

Lo stesso risultato lo possiamo ottenere con la API ICriteria, ecco lo unit test:

   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: }

Questo produce l’output di trace seguente:

   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

Quindi il problema potrebbe dirsi già risolto… sfortunatamente in un modo non troppo object oriented. Meglio sarebbe avere una collezione di ordini facilmente accessibile dall’oggetto customer, qualcosa tipo customer.Orders. Per fare questo ci viene incontro il più semplice tag di associazione per esprimere una collezione di entità collegate:

<bag/>

Questo rende dal punto di vista object oriented ciò che nel database esiste già in forma implicita. Se nell’algebra relazionale una FK esprime di per sè una relazione bidirezionale, nell’ambito oggetti questa relazione va esplicitata. Per cui diventa necessario modificare il mapping, con la prima semplice aggiunta nella classe customer di un <bag/> di Ordini:

   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>

Facendo un rebuild della solution, otteniamo un diagramma di classi per le nostre entità come il seguente:

image

Che di fatto ci mostra la relazione bidirezionale tra il cliente e i suoi ordini. Vediamo con un altro unit test come possiamo lavorare con un customer ed ottenere gli ordini a lui collegati:

   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: }

Questo produce il seguente output a 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

Come si vede, la collezione è stata recuperata dallo storage solo quando necessaria. Anche sulla collection abbiamo un comportamento “lazy”, che è opportuno in molti casi, ma non sempre. Possiamo anche qui modificare il comportamento del lazy specificandolo nel mapping:

 
   1: <bag name="Orders" fetch="join">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 
Facendo rigirare il test di prima otteniamo un output leggermente diverso:
   1: Obtained customer instance
   2: Order:ORD00000 Customer:Customer A
   3: Order:ORD00001 Customer:Customer A

Nessuna query: In effetti durante il caricamento del customer è stata risolta anche l’associazione alla collection di ordini, la query è stata difatti la seguente:

   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 tutti i casi il bag che ci arriva è una lista potenzialmente disordinata. Possiamo chiedere ad NH di fare, o meglio di chiedere al DB di fare, un ordinamento per una particolare proprietà dell’entity collezionata. Nel nostro esempio potremmo volere un ordinamento per data di ordine, aggiungendo al mapping questo attributo:

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

ed ottenendo quindi la interrogazione qui di seguito:

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'

Con l’opportuna clausola id ordinamento. Vediamo ora cosa succede facendo dei test con HQL. Il primo:

   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: }
 
Vediamo che il numero di query ritorna ad un comportamento 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

Tuttavia anche qui possiamo specificare in HQL la politica di fetch, modificando l’HQL in: “from Customer c join fetch c.Orders where c.Name=:name”. Sfortunatamente il test fallisce miseramente:

image

Questo è un comportamento “strano” di NH. In effetti c’è un solo Customer nei nostri test che soddisfa la relazione richiesta, tuttavia effettuando la join i record restituiti sono più di uno ( in effetti n, dove n è il numero degli ordini ). Va detto che NH non crea istanze diverse degli oggetti, ma di fatto mette nella lista di output delle reference alla stessa entità.

Si può ovviare a questo problema con un “ResultTransformer”, ovvero un post-processor dei risultati di NH. Possiamo costruire dei post processor ( IResultTransformer ) per manipolare in modo che riteniamo opportuno i risultati di un ICriteria o di un HQL. NH fornisce dei transformer di utilità, uno dei quali ci torna utile in questo frangente:

   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:     ;

Questa trasformazione fa collassare le entità “identiche” in una sola, ed ecco che le righe ritornate sono quelle aspettate, e la query diventa una sola, avendo specificato il comportamento non lazy.

Cosa succede se proviamo a rimuovere degli ordini dal cliente? ( attenzione, nella realtà questo non accadrà quasi mai: ci saranno probabilmente delle strategie di soft-delete, qui stiamo facendo degli esempi ) Ecco un unit test per provare questo scenario:

   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: }

 

Se lanciamo questo test, otteniamo il seguente, famigerato errore:

   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.

In pratica NH cerca di “sganciare” l’ordine liberandolo dal Customer, ma noi abbiamo specificato che Customer non è nullabile, quindi il ragionamento è corretto. L’unico modo è dire che l’owner di questa associazione è Customer, inserendo inverse=”true” intendendo in questo modo che questo “lato” dell’associazione non è il responsabile della collezione, in questo modo:

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

Sfortunatamente il test continua a non funzionare, stavolta lamenta che nulla è stato cancellato:

image

Quello che possiamo fare è forzare la cancellazione con il paramtero “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> 

ed ecco finalmente il test funzionante:

image

Il cascade ha effetti simili anche per le operazioni di inserimento di nuovi ordini: essi verranno salvati automaticamente al salvataggio dell’entità Customer.

Prima di concludere questa parte vale la pena ancora dare un occhiata ad un comportamento di cui è utile essere a conoscenza per problematiche di ottimizzazione. Supponiamo di voler sapere quanti ordini ha un Customer, e di avere la associazione in modalità 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: }
Se andiamo a vedere le query, notiamo che per contare gli elementi della collezione Orders, NH la riempie. Questo potrebbe andare a discapito delle performance se ci interessa il comportamento lazy della collection, ecco quindi che ci viene incontro l’attributo lazy=”extra”:
Ecco il nuovo mapping:
   1: <bag name="Orders" lazy="extra" order-by="OrderDate" inverse="true" cascade="all-delete-orphan">
   2:   <key column="CustomerId"/>
   3:   <one-to-many class="Order"/>
   4: </bag> 

ed ecco la pulitissima query che viene eseguita:

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

Bene, anche questa parte è finita, non tutte le possibili combinazioni di attributi per <bag/> sono state trattate, ma è stato posto l’accento su alcuni problemi classici, il minimo per partire e sperimentare per proprio conto. Ultime due cose:

  • Fino ad ora la equivalenza classe-tabella si è sempre manifestata, ma non sarà sempre così.
  • Guardiamo alle query che genera NH inizialmente per capire cosa succede, ma sempre di più dobbiamo imparare a dare per scontato il comportamento e guardare le query per motivi di ottimizzazione.
Download Sorgenti
Parte 3
Parte 5
Saturday, September 18, 2010 10:24:26 PM (GMT Daylight Time, UTC+01:00)  #    Comments [1] - Trackback
NHibernate | NHibernate Tutorial

# 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

# Wednesday, September 01, 2010
continua da Parte1: Cosa ci serve

In questa parte costruiamo un progetto con NHibernate che ci consentirà di persistere una prima entità. In effetti la parte OR/M sarà tenuta il più semplice possibile, niente reference o collezioni o componenti: l’obiettivo è creare uno scheletro funzionante e testabile. Ovviamente tutto quanto detto servirà da spunto, alcune cose potranno sembrare ovvie, altre meno, fate come credete meglio, ma poichè questo è un corso “da zero” voglio soffermarmi su tutti i punti.

Decidiamo di mettere le entità in una Class Library ( dll ). Questa è una scelta che probabilmente sarà conveniente fare anche per una soluzione vera da mandare in produzione. Creiamo quindi una nuova solution con Visual Studio. Distinguiamo il nome della soluzione con NHFromScratch.All, ed aggiungiamo all' interno un nuovo progetto di tipo class library, che andiamo a chiamare NHFromScratch.Entities.

t1

All’ interno della cartella radice della solution, creiamo una sottocartella “Lib”, atta a contenere le dipendenze della soluzione ( NHibernate e non ), ad esempio in aggiunta alle dipendenze di NH vedete la nunit.framework.dll, dipendenza che ci servirà a breve quando vorremo testare le nostre entità.

t2

Bene, il passo successivo è creare il mapping per la prima ed unica entità che vogliamo gestire. Ci sono una moltitudine di strategie per creare un mapping per NH, quella che presento qua è una soluzione “plain vanilla”, senza nessun fronzolo e a costo zero: con la pratica ognuno sceglie il metodo che meglio crede, ma per imparare è mia opinione che scrivere il file XML sia di aiuto. Aggiungiamo quindi una sottocartella “Mapping” nella class library appena creata, e in questo folder creiamo un nuovo file con il nome della entità ( Customer nel nostro caso ) con l’estensione .hbm.xml. Se abbiamo abilitato l’intellisense, come fortemente consigliato nella parte 1 l’unico tag che dobbiamo ricordare a memoria è <hibernate-mapping>, tutti gli altri saranno esposti via intellisense dopo che il namespace sarà correttamente specificato, come mostrato qui sotto:

 t4

Cosa importantissima da ricordare, ricordiamo di impostare “Embedded Resource” come build action sul file di mapping, altrimenti… la configurazione di NHibernate non funzionerà. Ad essere precisi questo si potrebbe evitare, ma per farlo dovremmo distribuire i file di mapping insieme all’applicazione, ed è un’opzione che francamente non mi piace e nemmeno ho mai visto praticare.

Fatto questo non rimane che scrivere difatto il mapping. In questo primo test useremo solo tre elementi di mapping: class,key e property, praticamente il minimo indispensabile per avere un entità al lavoro. Approfondiremo i vari aspetti nelle prossime parti.

   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="Customer" table="Customers">
   5:     <id name="Id" type="Int32">
   6:       <generator class="native"/>
   7:     </id>
   8:     <property name="Name" type="String" not-null="true"/>
   9:     <property name="AddressLine1" type="String" not-null="true"/>
  10:     <property name="AddressLine2" type="String" />
  11:     <property name="City" type="String" not-null="true"/>
  12:     <property name="ZipCode" type="String" not-null="true"/>
  13:     <property name="Active" type="Boolean" not-null="true"/>
  14:   </class>
  15:   
  16: </hibernate-mapping>
  17:  
  18:  
  19:  

Facciamo tuttavia due parole per dire a grandi linee cosa contiene il mapping della entità Customer. Innanzitutto è specificato il namespace e l’assembly che contiene l’entità mappata. Questo non è obbligatorio, ma ci consente di scrivere in maniera più leggibile il nome della classe più avanti. Il tag class indica appunto la classe che intendiamo utilizzare come entity. Ricordiamo che spesso non esiste un rapporto 1:1 tra tabelle ed entità, ms in questo semplice esempio così accadrà. Il tag class ci permette di definire il nome della classe, e a quale tabella è associata su DB. Il tag più sotto <id> è necessario: NHibernate vuole sapere come il database “distingue” le entità. Ci sono svariati modi per fare questo, e ll modo viene scelto dal sotto-tag generator: in questo esempio abbiamo specificato un generator “native”, che significa id univoco assegnato dal DB ( identity, auto-incrementale ) comunque esso si chiami nel DB target. Successivamente vengono le proprietà, con i loro bei nomi e tipi dato. Noterete che non ho specificato nessun nome per la corrispondente colonna sul DB: semplicemente se  il nome è uguale nessuno ci obbliga a farlo, non ripetiamo noi stessi. Anzi, diciamo anche che il tipo sarebbe un surplus se scrivessimo a mano la classe sottostante: NHibernate usa molto inferire le informazioni ovunque sia possibile. Noi però la classe non abbiamo voglia di farla a mano, per cui ci serviamo di hbm2net ( vedi la parte 1 ). Per scatenare questo tool, usiamo un pre build step di Visual Studio, come quello qui sotto:

t6

in pratica diciamo ad hbm2net di lavorare su tutti i file .hbm.xml che trova nella sottocartella mapping della solution, di mettere gli output ( ovvero le classi ) nella cartella del progetto. Sebbene hbm2net sia un editor template based che può generare un po’ qualsiasi artefatto partendo dall hbm, senza parametri si limita a generare le classi corrispondenti al mapping. Lanciando la build nell’output dovreste vedere il progress anche di hbm2net:

t7

Finita la build, vi trovate il file autogenerato per la entity su file system. Aggiungetelo al progetto.

t8

Date un’occhiata alla classe autogenerata, date un occhiata anche al mapping: se avete provato qualche altro tool per NH noterete che il lavoro fatto finora sembra molto semplificato. Stiamo scrivendo solo lo stretto indispensabile, quello che non ci serve lo lasciamo assolutamente da parte, perchè questo è NHibernate da ZERO.

t9

Bene, per completare la solution aggiungiamo la reference ad NHibernate: NHibernate e Iesi.Collection sono gli unici assembly che dobbiamo referenziare a tempo di build.

E’ finalmente l’ora di andare in test. Aggiungiamo alla solution un progetto di tipo Class Library, in cui inseriremo gli unit test.

t10

La solution di test, in modo simile ad un progetto che deve andare in produzione, necessita delle reference non statiche di NHibernate. Noi le abbiamo nella cartella Lib della solution, dobbiamo spostarli a fianco della dll di produzione. Possiamo scegliere di farlo con un post-build step, come illustrato qui sotto.

t11

Anche le reference di questo progetto sono un po’ più impegnative: ci serve Iesi.Collection & NHibernate come al solito, ma anche il nostro assembly con le entità, log4net per il logging, ed in più, ovvviamente, nunit.framework, va da sè.

t12

A me personalmente piace la barra verde di NUnit, per cui testiamo il progetto con NUnit, mettendo NUnit.exe come “start external program”.

t13

A questo punto creiamo il nostro unit test, con lo scopo di configurare NH, un po’ di log, e vedere se riusciamo a creare il Database. Sì, in questo tutorial il database non lo facciamo a mano, perchè anche se questo è NHibernate da ZERO, vogliamo scrivere il meno codice possibile.

Il codice dello unit test è qui di seguito:

   1: namespace NHFromScratch.Tests
   2: {
   3:     [TestFixture]
   4:     public class TestCustomer
   5:     {
   6:         static TestCustomer()
   7:         {
   8:             ConfigureLogForNet();
   9:         }
  10:  
  11:         ISessionFactory sessionFactory;
  12:         [SetUp]
  13:         public void Setup()
  14:         {
  15:             
  16:             CreateSessionFactory();
  17:         }
  18:  
  19:         [Test]
  20:         public void CanCreatedatabse()
  21:         {
  22:             SchemaExport export = new SchemaExport(CreateConfiguration());
  23:             export.Execute(true, true, false);
  24:         }
  25:  
  26:         [Test]
  27:         public void CanPersistCustomer()
  28:         {
  29:             Console.WriteLine("\n****** SAVE A CUSTOMER ********");
  30:             //save a customer
  31:             using (var session = sessionFactory.OpenSession())
  32:             using(var transaction = session.BeginTransaction())
  33:             {
  34:  
  35:                 Customer c = new Customer()
  36:                 {
  37:                     Active = true
  38:                      ,
  39:                     AddressLine1 = "xxxx"
  40:                      ,
  41:                     City = "Cuneo"
  42:                      ,
  43:                     Name = "Bill Gates"
  44:                      ,
  45:                     ZipCode = "12060"
  46:                 };
  47:                 session.Save(c);
  48:                 transaction.Commit();
  49:             }
  50:             Console.WriteLine("\n****** RETRIEVE A CUSTOMER ********");
  51:             //retrieve a customer
  52:             using (var session = sessionFactory.OpenSession())
  53:             
  54:             {
  55:  
  56:                 var customer = session.CreateCriteria<Customer>()
  57:                     .Add(Expression.Eq("Name", "Bill Gates"))
  58:                     .UniqueResult();
  59:  
  60:                 Assert.NotNull(customer);
  61:             }
  62:  
  63:             Console.WriteLine("\n****** MODIFY A CUSTOMER ********");
  64:             //modify a customer
  65:             using (var session = sessionFactory.OpenSession())
  66:             using (var transaction = session.BeginTransaction())
  67:             {
  68:  
  69:                 var customer = session.CreateCriteria<Customer>()
  70:                     .Add(Expression.Eq("Name", "Bill Gates"))
  71:                     .UniqueResult<Customer>();
  72:                 customer.ZipCode = "0000";
  73:                 Assert.NotNull(customer);
  74:                 
  75:                 transaction.Commit();
  76:             }
  77:  
  78:  
  79:             Console.WriteLine("\n****** VERIFY CUSTOMER IS MODIFIED ********");
  80:             //verify mod
  81:             using (var session = sessionFactory.OpenSession())
  82:             {
  83:  
  84:                 var customer = session.CreateCriteria<Customer>()
  85:                     .Add(Expression.Eq("Name", "Bill Gates"))
  86:                     .UniqueResult<Customer>();
  87:                 
  88:                 Assert.NotNull(customer);
  89:                 Assert.AreEqual("0000", customer.ZipCode);
  90:             }
  91:  
  92:  
  93:             Console.WriteLine("\n****** DELETE A CUSTOMER ********");
  94:             //delete a customer
  95:             using (var session = sessionFactory.OpenSession())
  96:             using (var transaction = session.BeginTransaction())
  97:             {
  98:  
  99:                 var customer = session.CreateCriteria<Customer>()
 100:                     .Add(Expression.Eq("Name", "Bill Gates"))
 101:                     .UniqueResult();
 102:                 session.Delete(customer);
 103:                 transaction.Commit();
 104:             }
 105:             Console.WriteLine("\n****** VERIFY CUSTOMER IS DELETED ********");
 106:             //verify delete
 107:             using (var session = sessionFactory.OpenSession())
 108:             
 109:             {
 110:  
 111:                 var customer = session.CreateCriteria<Customer>()
 112:                     .Add(Expression.Eq("Name", "Bill Gates"))
 113:                     .UniqueResult();
 114:                 Assert.IsNull(customer);
 115:             }
 116:         }
 117:  
 118:  
 119:  
 120:         private static void ConfigureLogForNet()
 121:         {
 122:             TraceAppender app = new TraceAppender();
 123:             app.Layout = new SimpleLayout();
 124:             //BasicConfigurator.Configure( app);
 125:         }
 126:  
 127:         private void CreateSessionFactory()
 128:         {
 129:             Configuration cfg = CreateConfiguration();
 130:             sessionFactory = cfg.BuildSessionFactory();
 131:         }
 132:  
 133:         private static Configuration CreateConfiguration()
 134:         {
 135:             Configuration cfg = new Configuration();
 136:             cfg.Configure();
 137:             // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
 138:             // contiene customer
 139:             cfg.AddAssembly(typeof(Customer).Assembly);
 140:             return cfg;
 141:         }
 142:  
 143:  
 144:     }
 145: }

Fatto questo lanciamo il test “CanCreateDatabase” e, come probabilmente sospettiamo, andiamo in rosso con il logger che dice qualcosa come qua sotto:

t14

In effetti, ci siamo “dimenticati” di configurare NHibernate, per cui a che DB potrebbe mai puntare ? Come vedete avere il log abilitato lascia pochi dubbi rispetto a quale sia l’errore. Tra l’altro manca persino il database…

Quindi creiamo prima un database vuoto, non serve fare nessuna tabella, lasciamo che sia NHibernate a fare il lavoro sporco

t16

 

Nell’esempio ho creato un database con nome NHFROMSCRATCH sull’SQLEXPRESS locale. Poi tornando alla  , quando si vedeva il contenuto dello ZIP, rocorderete la cartella Configuration_Templates: in questa cartella ci sono appunto dei template di configurazione, in cui basta sostituire un po’ di cose per essere pronti a lavorare con il db scelto:

t15

Nel nostro caso scegliamo MSSQL. Il contenuto del file, dopo le modifiche del caso è questo:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2:  
   3:  
   4: <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
   5:   <session-factory name="NHibernate.Test">
   6:     <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
   7:     <property name="connection.connection_string">Server=.\SQLEXPRESS;initial catalog=NHFROMSCRATCH;Integrated Security=SSPI</property>
   8:     <property name="adonet.batch_size">10</property>
   9:     <property name="show_sql">true</property>
  10:     <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
  11:     <property name="use_outer_join">true</property>
  12:     <property name="command_timeout">60</property>
  13:     <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
  14:     <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
  15:   </session-factory>
  16: </hibernate-configuration>

 

Come si vede, le modifiche sostanziali sono nella proprietà connection string, e in show_sql, che ho messo a true, per visualizzare le query mentre NH le esegue: un trucco che ci permetterà non tanto di vedere come NHibernate fa le query, ma piuttosto quante ne fa, per fare performance tuning. Questa sessione di configurazione potrà essere messa nel file di configurazione dell’applicazione, o vicino al file binario che la usa in un file di nome hibernate.cfg.xml, soluzione scelta nell’esempio.

t17

Ricordiamoci che, dopo aver inserito il file hibernate.cfg.xml nel progetto, dobbiamo impostare il flag “copy if newer” cosicchè verrà copiato automaticamente nella cartella di uscita.

vediamo ora come è fatto il test “CanCreateDatabase”:

   1: [Test]
   2: public void CanCreatedatabse()
   3: {
   4:    SchemaExport export = new SchemaExport(CreateConfiguration());
   5:    export.Execute(true, true, false);
   6: }

 

Usiamo la classe di Helper SchemaExport: questa classe, partendo dalla Configurazione corrente, si occupa di effettuare la creazione dello schema del database. Ora lanciando il test questi si comporta meglio, e nella porzione console è possibile  vedere la DDL che è stata inviata al database.

 

t18

Tramite il flag della funzione Export, è possibile sottometere lo script al sistema database sottostante, nel nostro caso otterremo su DB l’unica tabella del nostro minuscolo modello di test:

t19

Quindi facciamo un breve recap fino a questo punto: con un solo file ( il mapping appunto ) abbiamo creato un database e il codice c#. Tutto questo grazie ad hbm2net, che è un code generator open source recuoperabile da NHContrib. Con un po’ di pazienza è possibil emodificare e/o scrivere template aggiuntivi per hbm2net per geenrare altri tipidi artefacts, ad esempio DTO o altro, ma questo sarà visto più avanti. In pratica possiamo vedere hbm2net come un generatore di codice programmabile, che di default produce il codice delle entity partendo dai file hbm.

t18b

 

Ora possiamo scrivere il codice di prova per il modello. Mettendo a “true” il valore di show_sql nella configurazione, NH manda in standard output le query, per cui in debug è possibile ispezionare cosa succede dietro le quinte. Il nostro semplice unit test crea/modifica/cancella una semplice istanza della entità customer, ecco il codice dello unit test che fa tutto questo:

   1: namespace NHFromScratch.Tests
   2: {
   3:     [TestFixture]
   4:     public class TestCustomer
   5:     {
   6:         static TestCustomer()
   7:         {
   8:             ConfigureLogForNet();
   9:         }
  10:  
  11:         ISessionFactory sessionFactory;
  12:         [SetUp]
  13:         public void Setup()
  14:         {
  15:             
  16:             CreateSessionFactory();
  17:         }
  18:  
  19:         [Test]
  20:         public void CanCreatedatabse()
  21:         {
  22:             SchemaExport export = new SchemaExport(CreateConfiguration());
  23:             export.Execute(true, true, false);
  24:         }
  25:  
  26:         [Test]
  27:         public void CanPersistCustomer()
  28:         {
  29:             Console.WriteLine("\n****** SAVE A CUSTOMER ********");
  30:             //save a customer
  31:             using (var session = sessionFactory.OpenSession())
  32:             using(var transaction = session.BeginTransaction())
  33:             {
  34:  
  35:                 Customer c = new Customer()
  36:                 {
  37:                     Active = true
  38:                      ,
  39:                     AddressLine1 = "xxxx"
  40:                      ,
  41:                     City = "Cuneo"
  42:                      ,
  43:                     Name = "Bill Gates"
  44:                      ,
  45:                     ZipCode = "12060"
  46:                 };
  47:                 session.Save(c);
  48:                 transaction.Commit();
  49:             }
  50:             Console.WriteLine("\n****** RETRIEVE A CUSTOMER ********");
  51:             //retrieve a customer
  52:             using (var session = sessionFactory.OpenSession())
  53:             
  54:             {
  55:  
  56:                 var customer = session.CreateCriteria<Customer>()
  57:                     .Add(Expression.Eq("Name", "Bill Gates"))
  58:                     .UniqueResult();
  59:  
  60:                 Assert.NotNull(customer);
  61:             }
  62:  
  63:             Console.WriteLine("\n****** MODIFY A CUSTOMER ********");
  64:             //modify a customer
  65:             using (var session = sessionFactory.OpenSession())
  66:             using (var transaction = session.BeginTransaction())
  67:             {
  68:  
  69:                 var customer = session.CreateCriteria<Customer>()
  70:                     .Add(Expression.Eq("Name", "Bill Gates"))
  71:                     .UniqueResult<Customer>();
  72:                 customer.ZipCode = "0000";
  73:                 Assert.NotNull(customer);
  74:                 
  75:                 transaction.Commit();
  76:             }
  77:  
  78:  
  79:             Console.WriteLine("\n****** VERIFY CUSTOMER IS MODIFIED ********");
  80:             //verify mod
  81:             using (var session = sessionFactory.OpenSession())
  82:             {
  83:  
  84:                 var customer = session.CreateCriteria<Customer>()
  85:                     .Add(Expression.Eq("Name", "Bill Gates"))
  86:                     .UniqueResult<Customer>();
  87:                 
  88:                 Assert.NotNull(customer);
  89:                 Assert.AreEqual("0000", customer.ZipCode);
  90:             }
  91:  
  92:  
  93:             Console.WriteLine("\n****** DELETE A CUSTOMER ********");
  94:             //delete a customer
  95:             using (var session = sessionFactory.OpenSession())
  96:             using (var transaction = session.BeginTransaction())
  97:             {
  98:  
  99:                 var customer = session.CreateCriteria<Customer>()
 100:                     .Add(Expression.Eq("Name", "Bill Gates"))
 101:                     .UniqueResult();
 102:                 session.Delete(customer);
 103:                 transaction.Commit();
 104:             }
 105:             Console.WriteLine("\n****** VERIFY CUSTOMER IS DELETED ********");
 106:             //verify delete
 107:             using (var session = sessionFactory.OpenSession())
 108:             
 109:             {
 110:  
 111:                 var customer = session.CreateCriteria<Customer>()
 112:                     .Add(Expression.Eq("Name", "Bill Gates"))
 113:                     .UniqueResult();
 114:                 Assert.IsNull(customer);
 115:             }
 116:         }
 117:  
 118:  
 119:  
 120:         private static void ConfigureLogForNet()
 121:         {
 122:             TraceAppender app = new TraceAppender();
 123:             app.Layout = new SimpleLayout();
 124:             //BasicConfigurator.Configure( app);
 125:         }
 126:  
 127:         private void CreateSessionFactory()
 128:         {
 129:             Configuration cfg = CreateConfiguration();
 130:             sessionFactory = cfg.BuildSessionFactory();
 131:         }
 132:  
 133:         private static Configuration CreateConfiguration()
 134:         {
 135:             Configuration cfg = new Configuration();
 136:             cfg.Configure();
 137:             // implicitamente carichiamo tutti i mapping che si trovano nell'assembly che
 138:             // contiene customer
 139:             cfg.AddAssembly(typeof(Customer).Assembly);
 140:             return cfg;
 141:         }
 142:  
 143:  
 144:     }
 145: }

Ed ecco uno screenshots di come NH opera dietro le quinte:

t20

Nella prossima parte vedremo meglio i dettagli del file di mapping e il tag <many-to-one> insieme a <bag>.

 

 

Download Sorgenti

Parte 1

Parte 3

Wednesday, September 01, 2010 8:11:59 PM (GMT Daylight Time, UTC+01:00)  #    Comments [2] - Trackback
NHibernate | NHibernate Tutorial

# Saturday, August 28, 2010
Vai alla parte 2: Creare un progetto

Mi è ventuta l’idea di produrre una serie di post che consentano a chi vuole iniziare a lavorare con NHibernate, o almeno a provare per vedere se questo OR/M è la scelta giusta, di creare in modo ordinato un ambiente di test pulito, sapendo bene cosa serve, evitando il percorso “pasticciato” che spesso si fa all’inizio.

Bene, come partenza bisogna scaricare NHibernate. Il posto giusto dove farlo è il progetto su sourceforge, e il modo giusto di farlo è procurare i file binary necessari. Scaricare i sorgenti per il momento può essere tranquillamente tralasciato. Puntando al link di download, vi trovate qualcosa del genere:

s1

Come vedete ci sono più versioni, ed anche una ultima release, che in questo caso è la 3.0.0 Alpha2. Io ho cerchiato la versione 2.1.2 GA che è quella che useremo per questa serie di articoli, ma ho anche cerchiato GA: “general availability”. Dunque, perchè non ho scaricato l’ultima versione ? Cosa significa GA ? NHibernate ha una politica di rilascio ben consolidata, e marca con GA le versioni “da produzione”. Quindi, se non siete a conoscenza di qualche funzionalità particolare che c’è solo nella alpha di turno, se non siete dell’idea di vedervi cambiate sotto il naso funzioni e comportamenti, scaricate le versioni GA più recenti che trovate. Non esistono al momento “Setup” di NHibernate, mi risulta che ci fosse qualcosa in passato, comunque anche se trovaste qualcosa, vi suggerisco di evitarlo: NH consente di fare un private deploy, e avere traccia di quello che serve ci sarà utile in fase di consegna dell’applicativo sulle macchine di produzione. Scarichiamo quindi il file NHIbernate-2.1.2.GA-bin.zip. Scaricato il file il layout del contenuto è quello schematizzato qui sotto:

s2

Nella root, oltre ad un po’ di paccottiglia varia, troviamo due folder importanti che sono quelli che ci servono per mettere NHibernate all’opera: Required_Bins, and Required_For_LazyLoading.Tutte le dll contenute nel file required bin devono essere rilasciate insieme all’applicazione ( oltre a servire ovviamente  in fase di sviluppo :) ), mentre dalla cartella Required_For_LazyLoading si può scegliere quale folder distribuire in base a quale proxy generator si sceglie. Questo è un tema che si vedrà più avanti, per adesso basta sapere che NHibernate può derivare internamente alcune delle nostre classi quando decidiamo di avere delle entità “Lazy”, e questo è ottenuto tramite delle librerie esterne di generazione di classi “proxy”. Quale proxy generator usare dipende dalla configurazione, e quindi dalla stessa configurazione dipende quale folder distribuire. Negli esempi useremo LinFu. Diciamo che questa scelta è di scarso impatto sulla vita del vostro progetto, salvo in situazioni particolari, per cui per il momento possiamo andare avanti.

Importantissimi in fase di sviluppo sono i file nhibernate-mapping.xsd e nhibernate-configuration.xsd. Questi due file vi consentono di avere l’intellisense abilitato per i file di mapping e per la configurazione, e quindi sono assolutamente indispensabili. Malgrado siano finiti nella cartella required_bin, sono invece praticamente inutili in fase di distribuzione. Per attivare l’intellisense occorre copiare i due file suddetti nella cartella apposita di visual studio:

s3

Attenzione: può succedere che imporvvisamente Visual Studio smetta di fornire l'intellisense: tipicamente questo accade perchè ci sono più versioni dello stesso schema XML associate. Date un occhiata a questo post che spiega come risolvere il problema.

Inoltre, per sviluppare e per seguire gli esempi in questo tutorial occorrerà NUnit. Se ce lo avete già usate quello, altrimenti recuperate l’ultima versione dal sito ufficiale. Io di solito scarico il binario, non il setup, e lo scompatto sotto il folder programmi. Anche NUnit funziona con il semplice deploy Xcopy.

Ultima cosa prima di partire, lavorando con NHibernate ci sono alcune attività ripetitive che sarebbe bello evitare: consiglio di scaricare anche il tool hbm2net che ci consentirà di generare automaticamente le classi per nel corso del tutorial. Il tool si trova nel progetto NHContrib. Scaricate e scompattate lo zip in un folder, per esempio in C:\hbm2net. Il tool è un compilatore linea di comando, e sarà lanciato automaticamente da Visual Studio, ma perchè ciò avvenga occorre aggiungere la cartella in cui avete messo hbm2net nella variabile di ambiente PATH. per fare questo raggiungete le “proprietà del Sistema”:

s5

e poi aggiungere il path suddetto alla variabile PATH:

s6

 

Nella Parte2 vediamo come organizzare un progetto con NH.

Saturday, August 28, 2010 12:57:29 PM (GMT Daylight Time, UTC+01:00)  #    Comments [7] - 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 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