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.
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à.
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:
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:
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:
Finita la build, vi trovate il file autogenerato per la entity su file system. Aggiungetelo al progetto.
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.
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.
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.
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è.
A me personalmente piace la barra verde di NUnit, per cui testiamo il progetto con NUnit, mettendo NUnit.exe come “start external program”.
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:
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
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:

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.
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.

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