首個基於NHibernate的應用程式 Your first NHibernate based application 原文地址:http://www.nhforge.org/wikis/howtonh/your-first-nhibernate-based-application.aspx 本文涉 ...
首個基於NHibernate的應用程式
Your first NHibernate based application
原文地址:http://www.nhforge.org/wikis/howtonh/your-first-nhibernate-based-application.aspx
定義領域模型
讓我們開始通過定義一個非常簡單的領域模型。目前它是由一個稱為產品的實體。該產品具有 3 個屬性:名稱、 類別和中止。
添加一個文件夾 Domain 到您的解決方案的 FirstSample 項目。到此文件夾中添加一個新類 Product.cs。該代碼是非常簡單,使用自動屬性 (C# 3.0新的特征)
namespace FirstSolution.Domain
{
public class Product
{
public string Name { get; set; }
public string Category { get; set; }
public bool Discontinued { get; set; }
}
}
現在我們想要能夠持久化相關資料庫中此實體的實例。我們選擇了 NHibernate來完成這一任務。
領域模型中實體的一個實例對應資料庫表中的行。所以我們必須在資料庫中定義實體和相應的表之間的映射。此映射可以是另外定義一個映射文件 (一個xml 文檔) 或裝飾的實體和屬性,可以完成此映射。隨後,我將開始映射文件的定義。
譯者的話:裝飾的實體是什麼?目前我們知道除了xml文件映射,還有Fluent NHibernate的Mapping和特性(attribute,類似Java中註解@),裝飾可能是指他們的統稱吧。
定義映射
創建一個文件夾 Mappings 到 FirstSample 項目中。併在該文件夾中添加一個新的 xml 文檔並命名為 Product.hbm.xml。請註意"hbm"是文件名稱的一部分。這是一項約定,這個約定用於NHibernate 自動識別這個文件為一個映射文件。右鍵此 xml 文件點擊屬性,在生成操作一項定義"嵌入的資源"。
在 Windows 資源管理器中找到 nhibernate mapping.xsd,它在 NHibernate 的 src 文件夾中,並將其複製到您的 SharedLibs 文件夾中。編輯 xml 映射文件時,在VS菜單中的XML-架構中導入此xsd文檔。VS 然後將智能感知和驗證。
回到在 VS 將架構添加到 Product.hbm.xml 文件
讓我們從現在開始。每個映射文件都須定義一個 <hibernate-mapping> 根節點。
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="FirstSolution"
namespace="FirstSolution.Domain">
<!-- more mapping info here -->
</hibernate-mapping>
在映射文件引用領域模型類時你一定要提供的類的完全限定的名稱(如 FirstSample.Domain.Product , FirstSample)。若要使 xml 不那麼繁瑣,你可以定義程式集名稱和領域模型類的命名空間,到根節點的兩個屬性:assembly 和namespace。它是類似於使用 C# 中的聲明。
現在,我們必須先為產品實體定義一個主鍵。技術上我們可以拿產品的名稱屬性作為主鍵,因為此屬性必須定義,並且必須是唯一的。但通常會使用代理鍵代替它成為主鍵。因此我們將添加一個名為Id的屬性到我們的實體。我們使用 Guid 作為 Id 的類型,但也可以是 int 或 long。
using System;
namespace FirstSolution.Domain
{
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public bool Discontinued { get; set; }
}
}
完整的映射文件
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="FirstSolution"
namespace="FirstSolution.Domain">
<class name="Product">
<id name="Id">
<generator class="guid" />
</id>
<property name="Name" />
<property name="Category" />
<property name="Discontinued" />
</class>
</hibernate-mapping>
NHibernate 不會以我們的方式,比如,它定義了很多合理的預設值。所以,如果您不顯式地提供屬性的列名,它將按屬性名去對應列名。或 NHibernate從類的定義中,可以自動推斷的表名或列名。因此我的 xml 映射文件不會被堆滿冗餘信息。有關於映射文件更詳細的解釋請參閱線上文檔。你可以在這裡找到它。
你解決方案資源管理器現在應該看起來像這樣 (Domain.cd 包含我們簡單的領域模型類圖)
譯者的話:
配置 NHibernate
我們現在必須告訴 NHibernate 我們想要使用哪個資料庫產品,並提供詳細的鏈接信息,以連接字元串的形式。NHibernate 支持許多資料庫產品 !
向 FirstSolution 項目中添加一個新的 xml 文件,並命名為 hibernate.cfg.xml。將其屬性"複製到輸出目錄"設置為"始終複製"。由於我們引用了SQL Server Compact Edition資料庫在first sample項目中,所以輸入以下信息到xml 文件中。
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="dialect">NHibernate.Dialect.MsSqlCeDialect</property>
<property name="connection.driver_class">NHibernate.Driver.SqlServerCeDriver</property>
<property name="connection.connection_string">Data Source=FirstSample.sdf</property>
<property name="show_sql">true</property>
</session-factory>
</hibernate-configuration>
使用此配置文件,我們告訴 NHibernate 我們想要使用 MS SQL Server Compact Edition作為我們的目標資料庫和資料庫的名稱是 FirstSample.sdf (= 連接字元串)。我們同時也定義了希望看到NHibernate生成併發送到資料庫的 SQL語句 (在開發過程中調試時強烈推薦啟用此定義)。仔細檢查你的代碼中有沒有錯別字 !
添加一個叫FirstSample.sdf的空的資料庫,到 FirstSample 項目 (選擇本地資料庫作為模板)
單擊添加並忽略數據集創建嚮導 (就是點擊取消)。
譯者的話:我們不一定安裝過MS SQL Server Compact Edition資料庫,我將在Demo中把它替換成SQLite和相應的配置,這樣我們就不需要為了這個快速入門而去專門找一個資料庫了。
測試設置
是時候來測試我們的安裝了。首先驗證您的 SharedLibs 文件夾中有以下文件
您可以找到Microsoft SQL Server Compact Edition在你的程式文件夾中目錄最後 8 個文件。
註︰ System.Data.SqlServerCe.dll 位於子文件夾中的桌面。
所有其他文件可以在NHibernate 文件夾中找到。
在您的測試項目中添加對 FirstSample 項目的引用。另外測試項目引用 NHibernate.dll、 nunit.framework.dll 和 Systm.Data.SqlServerCe.dll (記得要引用位於 SharedLibs 文件夾中的文件 !)。要註意為設置屬性"複製本地"為 true 為 System.Data.SqlServerCe.dll, 因為在預設情況下它設置為 false !
譯者的話:現在VS2012以上都有自帶的單元測試項目,也非常好用。所以無需引用nunit.framework.dll,同樣System.Data.SqlServerCe.dll也可以替換成System.Data.Sqlite.dll。
在測試項目中添加一個類,命名為 GenerateSchema_Fixture。
現在將下麵的代碼添加到 GenerateSchema_Fixture 文件
using FirstSolution.Domain;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using NUnit.Framework;
namespace FirstSolution.Tests
{
[TestFixture]
public class GenerateSchema_Fixture
{
[Test]
public void Can_generate_schema()
{
var cfg = new Configuration();
cfg.Configure();
cfg.AddAssembly(typeof (Product).Assembly);
new SchemaExport(cfg).Execute(false, true, false, false);
}
}
}
測試方法的第一行創建 NHibernate 配置類的一個新實例。此類用於配置 NHibernate。在第二行,我們告訴 NHibernate 配置本身。NHibernate 將留心配置信息,因為我們沒在測試方法中提供任何信息。所以 NHibernate 將搜索輸出目錄中的 hibernate.cfg.xml 文件來調用。這正是我們為什麼要在這個文件中這麼設置的原因。
在第三行的代碼,我們告訴 NHibernate 它可以發現並包含Product類的程式集的映射信息。它將在嵌入的資源中只找到一個(Product.hbm.xml)這樣的文件。
第四行代碼使用NHibernate中 SchemaExport 的工具類,為我們在自動生成資料庫中的架構。
註︰ 我們先不用去理解此測試方法中NHibernate 如何工作 , 但應當關註是否正確地安裝。
如果你有安裝的 TestDriven.Net 你可以現在只是右鍵點擊裡面的測試方法並選擇"運行 Test(s)"來執行測試。
譯者的話:VS2012以上版本的單元測試可以不用TestDriven.Net和NUnit,微軟有自帶的。
如果每一件事是好的你應該看到下麵的結果,在輸出視窗
譯者的話:
如果你有安裝 ReSharper 你可以開始測試通過單擊黃色綠色圓圈的左邊框,選擇運行。
其結果是,如下所示
譯者的話:原文沒圖,我們還是用VS自帶的吧,如下圖
在出現問題時
如果你測試失敗,請仔細檢查你的目標目錄,在其中找到下列文件 (即︰ m:dev\projects\FirstSolution\src\FirstSolution.Tests\bin\debug)
仔細檢查NHibernate 配置文件 (hibernate.cfg.xml) 中或在映射文件 (Product.hbm.xml)中是否有錯別字,最後檢查映射文件 (Product.hbm.xml)是否設置為"嵌入的資源"的"生成操作"。如果測試成功,才繼續。
我們第一次的 CRUD 操作
現在很明顯我們的系統已是準備好開始了。我們成功地實現了我們的領域模型,定義映射文件和配置 NHibernate。最後我們使用 NHibernate 從我們的領域模型 (和我們映射文件) 自動生成資料庫架構。
在 DDD (參考Eric Evans的《領域驅動設計》) 的精神,我們為所有的 crud 操作(創建、 讀取、 更新和刪除)定義了Repository。Repository介面是領域模型不實現的一部分!執行是特定的基礎設施。我們要保持我們的領域模型和持久化無關 (PI)。
譯者的話:這一段我不知道該如何去翻譯它,但我可以解釋它的意思。它的大致意思是根據DDD的思想,領域模型Domain裡面不應該有和持久化有關的東西,比如我們的Product中不該包含資料庫CRUD操作,而這些CRUD的基礎操作該在倉儲Repository介面中實現。
到我們的 FirstSolution 項目的Domain文件夾中添加一個新的介面。把它叫做 IProductRepository。讓我們定義以下介面
using System;
using System.Collections.Generic;
namespace FirstSolution.Domain
{
public interface IProductRepository
{
void Add(Product product);
void Update(Product product);
void Remove(Product product);
Product GetById(Guid productId);
Product GetByName(string name);
ICollection<Product> GetByCategory(string category);
}
}
添加一個類 ProductRepository_Fixture 到測試項目下,並添加下麵的代碼
[TestFixture]
public class ProductRepository_Fixture
{
private ISessionFactory _sessionFactory;
private Configuration _configuration;
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
_configuration = new Configuration();
_configuration.Configure();
_configuration.AddAssembly(typeof (Product).Assembly);
_sessionFactory = _configuration.BuildSessionFactory();
}
}
在 TestFixtureSetUp 方法的第四行,我們創建一個session factory。這是一個開銷很大的過程,因此程式運行期間應該只執行一次。這就是為什麼把它放到這種測試期間只執行一次的方法的原因。
要保持我們測試方法無副作用,每個測試方法執行之前,我們重新創建我們的資料庫架構。因此我們添加下麵的方法
[SetUp]
public void SetupContext()
{
new SchemaExport(_configuration).Execute(false, true, false, false);
}
譯者的話:NHibernate3.0中,只有3個參數。new SchemaExport(cfg).Execute(false,true,false);
現在我們可以實現向資料庫中添加一個新的Product實例的測試方法。添加一個新的文件夾名為Repositories到 FirstSolution 項目。到此文件夾下添加一個類 ProductRepository。使 ProductRepository 實現 IProductRepository 介面。
using System;
using System.Collections.Generic;
using FirstSolution.Domain;
namespace FirstSolution.Repositories
{
public class ProductRepository : IProductRepository
{
public void Add(Product product)
{
throw new NotImplementedException();
}
public void Update(Product product)
{
throw new NotImplementedException();
}
public void Remove(Product product)
{
throw new NotImplementedException();
}
public Product GetById(Guid productId)
{
throw new NotImplementedException();
}
public Product GetByName(string name)
{
throw new NotImplementedException();
}
public ICollection<Product> GetByCategory(string category)
{
throw new NotImplementedException();
}
}
}
操作數據
現在回到ProductRepository_Fixture測試類和實現第一個測試方法
[Test]
public void Can_add_new_product()
{
var product = new Product {Name = "Apple", Category = "Fruits"};
IProductRepository repository = new ProductRepository();
repository.Add(product);
}
首次運行的測試方法將失敗,因為我們的倉儲類未實現 Add 方法。讓我們實現它。但是,等一等,我們必須首先定義一個小的Helper類提供我們NHibernate session對象上的需求。
using FirstSolution.Domain;
using NHibernate;
using NHibernate.Cfg;
namespace FirstSolution.Repositories
{
public class NHibernateHelper
{
private static ISessionFactory _sessionFactory;
private static ISessionFactory SessionFactory
{
get
{
if(_sessionFactory == null)
{
var configuration = new Configuration();
configuration.Configure();
configuration.AddAssembly(typeof(Product).Assembly);
_sessionFactory = configuration.BuildSessionFactory();
}
return _sessionFactory;
}
}
public static ISession OpenSession()
{
return SessionFactory.OpenSession();
}
}
}
運行期間,不管客戶端何時需要一個新的session,此類只創建session factory第一次。
現在我們可以定義 Add 方法在 ProductRepository 中,如下所示
public void Add(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Save(product);
transaction.Commit();
}
}
第二次運行的測試方法會再次失敗並顯示以下消息
譯者的話:
這是因為 NHibernate 是預設情況下配置為使用延遲載入的所有實體。這是推薦的方法,我強烈建議不要更改,為了最大的靈活性。
我們怎樣才能解決這個問題?很容易,讓領域模型中所有屬性 (方法) 加上Virtual關鍵字即可。讓我們為我們的Product類加上這個。
public class Product
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual string Category { get; set; }
public virtual bool Discontinued { get; set; }
}
現在再次運行測試。它應該會成功,我們得到以下輸出
譯者的話:
請註意NHibernate輸出的 sql 語句。
現在我們已經成功地向資料庫插入一個新的Product。但讓我們測試它是否真的是這樣。讓我們來擴展我們的測試方法
[Test]
public void Can_add_new_product()
{
var product = new Product {Name = "Apple", Category = "Fruits"};
IProductRepository repository = new ProductRepository();
repository.Add(product);
// use session to try to load the product
using(ISession session = _sessionFactory.OpenSession())
{
var fromDb = session.Get<Product>(product.Id);
// Test that the product was successfully inserted
Assert.IsNotNull(fromDb);
Assert.AreNotSame(product, fromDb);
Assert.AreEqual(product.Name, fromDb.Name);
Assert.AreEqual(product.Category, fromDb.Category);
}
}
再次運行測試。希望它會成功......
現在我們準備也實現repository中的其他方法。為了測試這我們寧願要一個repository (即資料庫表) 已經包含了一些產品。沒有什麼比這更簡單。只是添加 CreateInitialData 方法,如下所示添加到測試類
private readonly Product[] _products = new[]
{
new Product {Name = "Melon", Category = "Fruits"},
new Product {Name = "Pear", Category = "Fruits"},
new Product {Name = "Milk", Category = "Beverages"},
new Product {Name = "Coca Cola", Category = "Beverages"},
new Product {Name = "Pepsi Cola", Category = "Beverages"},
};
private void CreateInitialData()
{
using(ISession session = _sessionFactory.OpenSession())
using(ITransaction transaction = session.BeginTransaction())
{
foreach (var product in _products)
session.Save(product);
transaction.Commit();
}
}
(在創建架構調用後) 從 SetupContext 方法調用此方法。現在每次資料庫架構創建資料庫後填充一些產品。
讓我們測試用下麵的代碼庫的更新方法
[Test]
public void Can_update_existing_product()
{
var product = _products[0];
product.Name = "Yellow Pear";
IProductRepository repository = new ProductRepository();
repository.Update(product);
// use session to try to load the product
using (ISession session = _sessionFactory.OpenSession())
{
var fromDb = session.Get<Product>(product.Id);
Assert.AreEqual(product.Name, fromDb.Name);
}
}
第一次運行時此代碼將失敗,因為Update方法尚未在Repository中實現。註︰ 這是預期的行為,因為在 TDD 第一次運行測試時它應該總是失敗 !
譯者的話:這篇快速開始的入門教程水有點深,又是DDD,又是TDD,嚇死人了,沒接觸過的人可以忽略。同時也可見NHibernate更多是面向一些資深的面向對象程式員,可悲的是很多程式員未入門時就接觸到了它。嘆息!
類似於 Add 方法我們實現Repository中的 Update 方法。唯一的區別是我們調用NHibernate session對象的update 方法而不是Save方法。
public void Update(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Update(product);
transaction.Commit();
}
}
再次運行測試希望它成功。
Delete 方法是直截了當。測試是否真的已刪除記錄時,我們只是斷言由會話的 get 方法返回的值是等於 null。這裡是測試方法
[Test]
public void Can_remove_existing_product()
{
var product = _products[0];
IProductRepository repository = new ProductRepository();
repository.Remove(product);
using (ISession session = _sessionFactory.OpenSession())
{
var fromDb = session.Get<Product>(product.Id);
Assert.IsNull(fromDb);
}
}
Repository中刪除方法的實現
public void Remove(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Delete(product);
transaction.Commit();
}
}
查詢資料庫
我們仍然必須執行查詢的資料庫對象的三個方法。我們先從最容易的一個,GetById。我們首先編寫測試
[Test]
public void Can_get_existing_product_by_id()
{
IProductRepository repository = new ProductRepository();
var fromDb = repository.GetById(_products[1].Id);
Assert.IsNotNull(fromDb);
Assert.AreNotSame(_products[1], fromDb);
Assert.AreEqual(_products[1].Name, fromDb.Name);
}
然後完成測試的代碼
public Product GetById(Guid productId)
{
using (ISession session = NHibernateHelper.OpenSession())
return session.Get<Product>(productId);
}
現在,那很簡單。為以下兩種方法,我們使用session對象的新方法。讓我們開始用 GetByName 方法。像往常一樣我們先寫測試
[Test]
public void Can_get_existing_product_by_name()
{
IProductRepository repository = new ProductRepository();
var fromDb = repository.GetByName(_products[1].Name);
Assert.IsNotNull(fromDb);
Assert.AreNotSame(_products[1], fromDb);
Assert.AreEqual(_products[1].Id, fromDb.Id);
}
GetByName 方法的實現可以通過使用兩個不同的方法。第一使用 HQL (Hibernate Query Language) 和第二個 HCQ (Hibernate Criteria Query)。讓我們開始使用 HQL。HQL 是面向對象的查詢語言 SQL 類似 (但不是等於)。
譯者的話:他指的第一種方法HQL是這個樣子的。
在上面的示例中我介紹了常用的技術使用 NHibernate 時。它被稱為fluent介面。作為結果的代碼是簡練也更易於理解。你可以看到一個 HQL 查詢是一個字元串,它可以具有嵌入 (命名) 參數。參數使用首碼 ':'。NHibernate 定義很多的helper方法 (如示例中使用 SetString),將各種類型的值分配給這些參數。最後通過使用 UniqueResult 我告訴 NHibernate 希望只有一條記錄返回。如果多個然後引發異常,HQL 查詢將返回一條記錄。要獲取更多的信息 HQL 請閱讀線上文檔。
第二個版本使用criteria query來搜索請求的Product。
public Product GetByName(string name)
{
using (ISession session = NHibernateHelper.OpenSession())
{
Product product = session
.CreateCriteria(typeof(Product))
.Add(Restrictions.Eq("Name", name))
.UniqueResult<Product>();
return product;
}
}
NHibernate 的許多用戶認為這種做法是更多面向的對象。在另一方面編寫的criteria語法複雜查詢可以迅速成為難以理解。
實現的最後一個方法是 GetByCategory。此方法返回Product的列表。測試可以實現,如下所示
[Test]
public void Can_get_existing_products_by_category()
{
IProductRepository repository = new ProductRepository();
var fromDb = repository.GetByCategory("Fruits");
Assert.AreEqual(2, fromDb.Count);
Assert.IsTrue(IsInCollection(_products[0], fromDb));
Assert.IsTrue(IsInCollection(_products[1], fromDb));
}
private bool IsInCollection(Product product, ICollection<Product> fromDb)
{
foreach (var item in fromDb)
if (product.Id == item.Id)
return true;
return false;
}
方法本身可能包含下麵的代碼
public ICollection<Product> GetByCategory(string category)
{
using (ISession session = NHibernateHelper.OpenSession())
{
var products = session
.CreateCriteria(typeof(Product))
.Add(Restrictions.Eq("Category", category))
.List<Product>();
return products;
}
}
摘要
在這篇文章中我已經給你如何實現基本示例領域模型,定義映射到資料庫以及如何配置 NHibernate 能夠持久化領域對象在資料庫中。我給你展示瞭如何通常編寫和測試您的領域對象的 CRUD 方法。我拿MS SQL Compact Edition 作為示例資料庫,但可以使用任何其他受支持的資料庫 (你只需要相應地更改 hibernate.cfg.xml 文件)。我們沒有依賴於外部框架或工具以外的資料庫和 NHibernate 本身 (.NET 當然從來沒有計算在內)。
譯者的話:終於翻譯完了,這篇快速開始非常適合初學者,因為提供的例子是可以被實現的,而且可以同時入門DDD和TDD,看得出作者非常用心。而我也在其中加入了批註和補充了顯示不了的圖片。
花了很多時間,想想我花時間在翻譯的時候,一個跟我長得很像的老司機開著滴滴又開直播地賺錢,我忽然感慨,有些人也許永遠都是在燃燒自己。