Добро пожаловать в NHibernate
Это статья поможет вам разобраться со следующими этапами изучения NHibernate:
Создайте ваш проект
Перед тем как мы начнем создавать наше приложение и бизнес объекты, мы должны создать пустой проект. Запустите Visual Studio и создайте новый Class Library проект. А теперь давайте посмотрим на что то более интересное - это Бизнес Объекты.
Если вы читаете это, то я надеюсь вы уже скачали NHibernate скачать и готовы начать работать с данной технологией.
Это статья поможет вам разобраться со следующими этапами изучения NHibernate:
- Установка NHibernate
- Создание простого бизнес класса
- Создание NHibernate mapping для загрузки и сохранения бизнес объектов
- Конфигурация NHibernate для доступа к вашей локальной Базе Данных
- Автоматическая генерация Базы Данных
- Написание простого CRUD кода используя Repository Pattern
- Использование Юнит-тестов для проверки корректности работы нашего созданного кода
Вот наша цель:
Но давайте рассмотрим всё по порядку.
Для начала давайте разберемся со скачанным нами ZIP архивом.
Установка NHibernate
Если вы скачали архив с NHibernate, то всё что вам нужно это распаковать его куда-нибудь. Я всегда создаю папку SharedLibs c:\Code\SharedLibs\NHibernate и распаковываю архив в эту папку. Но можете делать конечно как вам удобней. Это ваша SharedLibs папка из которой вы впоследствии добавите ссылки на сборки NHibernate и NUnit. Добавьте ссылки на NHibernate в оба ваших демо проекта.
На этом всё. NHibernate установлен. Далее мы поговорим как использовать NHibernate в Visual Studio. Но для начала посмотрим как создать проект. Ниже представленный код актуален для Visual Studio 2008 и .Net Framework 3.5.
Создайте ваш проект
Перед тем как мы начнем создавать наше приложение и бизнес объекты, мы должны создать пустой проект. Запустите Visual Studio и создайте новый Class Library проект. А теперь давайте посмотрим на что то более интересное - это Бизнес Объекты.
Создание простого бизнес класса
Давайте начнем с определения очень простого домена. На данный момент он включает только один класс Product. У Product есть три свойства Name, Category и Discontinued.
Добавьте папку 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. Строка в созданном классе будет соответствовать строке в таблице в БД. Для этого мы определим mapping между созданным нами классом и соответствуйщей таблицей в базе данных. Этот mapping может быть сделан посредством mapping файла (xml-документ) или можно сделать с тоже самое с помощью атрибутов. Я начну с mapping файла.
Создание NHibernate mapping для загрузки и сохранения бизнес объектов
Создайте папку Mappings в проекте FirstSample. Добавьте новый xml-документ в эту папку и назовите его Product.hbm.xml. Не забудьте "hbm" часть имени файла. Это соглашение имен используется в NHibernate для автоматического определения файла как mapping файла. Задайте "Embedded Resource" как Build Action для xml файла.
Найдите файл nhibernate-mapping.xsd в папке NHibernate'а и скопируйте его в SharedLibs папку. Теперь мы можем использовать эту xml схему для более удобного создания наших mapping файлов. VS будет нам предоставлять средства intellisense и validation при редактировании xml mapping документа. В VS добавьте схему в Product.hbm.xml файл
И так давайте начнем. Каждый mapping файл содержит корневой node <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>
В mapping файле когда мы ссылаемся на класс-домен вы всегда должны задать корректное полное имя класса (например FirstSample.Domain.Product, FirstSample).
Сейчас для начала нам надо задать primary key для класса product. Технически мы могли бы взять свойство Name и могли бы задать его как уникальное. Но в основном так не делается и мы сделаем по другому. Для этого мы добавим в наш класс новое свойство и назовем его Id. Будем использовать Guid тип, но также могут быть использованы вместо него и 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; }
}
}
Завершаем создание mapping файла
<?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 не поступает как мы, он делает много чего по умолчанию. Так если вы не задали имя колонки в "property", то имя будет взято из свойств. Или NHibernate может автоматически взять название таблицы из заданного класса. Как следствие - мой xml mapping файл не загроможден лишней информацией. Пожалуйста посмотрите документацию для более детального объяснения mapping. Вы можете найти её здесь.
Ваш solution explorer должен выглядеть так сейчас (Domain.cd содержит диаграмму классов вашего домена). Вы можете добавить папку Design и создать диаграмму классов самостоятельно, это просто хорошая практика для таких приложений, но данный вопрос не относится к рассматриваемой нами теме.
Конфигурация NHibernate для доступа к вашей локальной Базе Данных
Сейчас мы должны решить какую БД мы будем использовать и нам надо определить детали подключения в connection string. NHibernate поддерживает множество БД!
Добавьте новый xml файл в FirstSolution проект and назовите его hibernate.cfg.xml. Установите свойство "Copy to Output" в "Copy always". Так как мы в нашем примере будем использовать SQL Server Compact Edition то вам необходимо добавить следующую информацию в 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 (что эквивалентно connection string). Мы также определяем что нам надо видеть SQL который NHibernate генерирует и посылает в БД (это рекомендовано сделать, так это нам поможет в дальнейшем в дебаге в процессе разработки). Дважды проверьте чтобы у вас не было опечаток в коде!
Добавьте пустую БД в ваш FirstSample проект и назовите её FirstSample.sdf(выберите Local Database в шаблонах)
Нажмите Добавить и проигнорируйте Мастер создания Dataset, просто нажмите Cancel.
Тестируем наш Setup
Пришло время протестировать наш setup. Прежде всего проверьте что у вас есть соответствующие файлы в папке SharedLibs
Последние 8 файлов вы можете найти в директории "Microsoft SQL Server Compact Edition" в вашей папке со всеми программами.
Обратите внимание: System.Data.SqlServerCe.dll находится в подпапке Desktop.
Все остальные файлы вы можете найти в папке NHibernate
Добавьте ссылку на проект FirstSample в ваш тестовый проект. Также добавьте ссылки на NHibernate.dll, nunit.framework.dll и Systm.Data.SqlServerCe.dll (также не забудьте добавить ссылки на файлы находящиеся в вашей папке SharedLibs!). Обратите внимание что свойство "Copy Local" необходимо установить в true для сборки System.Data.SqlServerCe.dll так как по умолчанию это свойство false! Добавьте копию файла hibernate.cfg.xml в корневую папку вашего unit test проекта. Для взаимодействия с NHibernate в проекте NUnit необходим доступ к этому файлу.
Добавьте класс GenerateSchema_Fixture в ваш тестовый проект. Ваш тестовый проект теперь должен выглядеть так
Далее нам необходимо получить 7 файлов sqce*.dll в output директории. Мы можем добиться этого используя post-build event в VS.
Добавьте следующую команду в "Post-build event command line"
copy $(ProjectDir)..\..\SharedLibs\sqlce*.dll $(ProjectDir)$(OutDir)
Теперь добавьте следующий код в файл 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 в output директории. Это и есть чего мы хотели добиться когда создавали наш файл настроек.
На третьей строчке мы говорим NHibernate что можно найти mapping информацию в сборке, которая также содержит класс Product. На данный момент NHibernate сможет найти только один файл (Product.hbm.xml) как встроенный ресурс.
Четвертая строка кода использует NHibernate класс-помощник SchemaExport для того чтобы автоматически сгенерировать для нас схему в БД. SchemaExport создаст таблицу table в БД и каждый раз когда вы будете обращаться, она будет удалять таблицу и все её данные и заново пересоздавать всё.
Обратите внимание: С этим тестовым методом мы не желаем знать как NHibernate выполняет эту работы (мы можем быть конечно уверены что она это делает верно), но лучше конечно всё изначально настроить правильно. Однако посмотрите в БД и вы увидите новую таблицу 'product'.
Если у вас установлен TestDriven.Net вы можете кликнуть правой кнопкой по тесту и выбрать "Run Test(s)" для запуска тестов.
Если всё в порядке то вы должны увидеть такой результат в output окне
Если у вас установлен ReSharper вы можете запустить тест нажав на желто-зеленые кружки в левой стороне окна и выбрать Run.
В окне появятся следующие результаты
В случае возникновения проблем
Если ваши тесты выдают ошибку, то ещё раз проверьте файлы в вашей папке (здесь: m:dev\projects\FirstSolution\src\FirstSolution.Tests\bin\debug)
Дважды проверьте нет ли у вас опечаток в файле конфигурации (hibernate.cfg.xml) или в mapping файле (Product.hbm.xml). Ну и наконец проверьте где вы задали "Build Action" для mapping файла (Product.hbm.xml) в "Embedded Resource". Переходите к следующим действиям если только тесты завершились удачно.
Наши первые CRUD операторы
Теперь очевидно что наша система готова к запуску. Мы успешно создали наш Домен, mapping файлы и настроили NHibernate. В итоге мы можем использовать NHibernate для автоматической генерации схемы БД из нашего Домена(и нашего mapping файла).
Согласно DDD (смотрите Domain Driven Design by Eric Evans) мы создали репозитарий для всех crud операторов (create, read, update and delete). Интерфейс репозитария это часть домена где не находится никакой реализации! Реализация характерна для инфраструктуры. Нам же надо сохранить наш домен как "persistence ignorant (PI)".
Добавьте новый интерфейс к нашему проекту FirstSolution. Назовите его 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);
}
Сейчас мы можем реализовать тестовый метод для добавления новый записи 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 в классе репозитария. Давайте сделаем это. Но подождите, мы должны создать небольшой вспомогательный класс который позволит нам выполнять сессии объектов по требованию.
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();
}
}
}
Этот класс создает фабрику сессий только в первый раз когда клиенту нужна сессия.
Сейчас мы можем определить метод Add в ProductRepository так
public void Add(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Save(product);
transaction.Commit();
}
}
Второй запуск тестовом опять выдаст нам ошибку со следующим сообщением
Это потому что NHibernate по умолчанию настроена использовать ленивую загрузку для всех объектов. Эта рекомендация существенна и я бы не рекомендовал вам изменять данную настройку для максимальной гибкости вашего приложения.
Как мы решим данную проблему? Это просто - нам надо задать все свойства и методы обхектов домена как виртуальные для нашего класса 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.
Сейчас мы считаем что мы успешно добавили наш продукт в БД. Но давайте протестируем так ли это. Давайте доработаем наш тестовый метод
[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);
}
}
Запустите тест заново. Надеюсь всё прошло успешно...
Сейчас мы готовы реализовать остальные методы репозитория. Для тестирования было бы неплохо иметь репозиторий (который является таблицей БД) который уже содержит несколько продуктов. Ничего нет проще. Просто добавьте метол 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 (после вызова метода создания схемы) и всё будет готово. Теперь при каждом создании схемы БД в базу добавляются несколько записей.
Давайте протестируем метод Update репозитария с помощью следующего кода
[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 не реализован в репозитории. Обратите внимание: Это принцип TDD при первом запуске теста он всегда должен выдавать ошибку! Аналогично методу Add мы реализуем метод 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();
}
}
Запустите тесты заново и вы увидите что всё прошло удачно.
Реализация метода удаления описана ниже. При тестировании для того чтобы убедиться действительно ли запись удалена мы должны проверить значения метода get объекта session, значение должно быть равно 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);
}
}
и реализация метода Remove в репозитарии
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);
}
Реализовать данный метод можно двумя путями. Первое это использовать HQL (Hibernate Query Language) и второе это HCQ (Hibernate Criteria Query). Давайте начнем с HQL. HQL это объектно-ориентированный язык запросов похожий (но не идентичный) на SQL.
Что необходимо добавить: реализацию GetByName используя HQL. Реализацию HCQ, она работает как показано ниже и в итоге возвращает объект product.
В следующем примере я представил общую технику реализации для NHibernate. Она называется fluent interfaces. Как результат код компактен и прост для понимания. Вы можете увидеть что HQL запрос - это строка которая может содержать параметры. Префиксы этих параметров ':'. NHibernate определяет множество вспомогательных методов (таких как SetString, используется в примере) для связи значений разных типов с параметрами. В итоге используя UniqueResult я сообщаю NHibernate что мне необходимо вернуть только одно значение из БД. Если больше одной записи будет возвращено посредством запроса HQL то оно выдаст исключение. Для получения большой информации об HQL пожалуйста прочитайте онлайн документацию.
Вторая версия использует запрос с критериями для поиска запрашиваемой записи product. Вам необходимо добавить ссылку на NHibernate.Criterion в вашу страницу репозитария.
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 считают что это решение более объектно-ориентировано. С другой стороны составные запросы с синтаксисом содержащем критерии могут быть очень сложны для понимания.
Последний метод реализует GetByCategory. Этот метод возвращает список записей products. Тест может быть реализован так
[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;
}
}
Подведем итоги
В этой статье я показал как реализовать простой базовый домен, mapping файл к БД и как настроить NHibernate с возможностью сохранения объектов домена в БД. Я показал вам как создавать и тестировать CRUD методы для объектов домена. Я взял как простой пример БД MS SQL Compact Edition, но другие поддерживаемые БД также могут использоваться (для этого вам всего лишь надо изменить hibernate.cfg.xml файл соответственно). Мы не использовали никаких инструментов кроме БД и самой NHibernate (ну и .NET конечно, его не берем в расчет).
источник
Ещё посоветую пользоваться генераторами NHibernate. Ни один из них до конца верно не работает, но если генерировать и там и там а потом доделывать руками, то всё получится.
http://sourceforge.net/projects/ngennhibernatec/
http://nmg.codeplex.com/
Также посоветую посмотреть как соотносятся типы в маппинге с типа SQL Server
http://devio.wordpress.com/2012/08/08/nhibernate-3-data-type-mapping-for-sql-server/
Ещё посоветую пользоваться генераторами NHibernate. Ни один из них до конца верно не работает, но если генерировать и там и там а потом доделывать руками, то всё получится.
http://sourceforge.net/projects/ngennhibernatec/
http://nmg.codeplex.com/
Также посоветую посмотреть как соотносятся типы в маппинге с типа SQL Server
http://devio.wordpress.com/2012/08/08/nhibernate-3-data-type-mapping-for-sql-server/