Причем решил пойти по не самому простому пути. Вначале я решил как можно ближе ознакомится с более зрелой ORM, которая широко признана в ALT.NET сообществе - NHibernate.
В своем багаже знаний хочется иметь понимание того как строится уровень доступа к данным в наших приложениях при использовании как минимум двух ORM - NHibernate и Entity Framework. Это позволит, в случае необходимости, более взвешенно принимать решение в пользу того или иного ORM. Более того, каждый из этих ORM предполагает несколько вариантов их использования.
Например у Entity Framework 4.0 существуют как минимум три подхода: Database first, Model first, Code only. Причем можно использовать либо не использовать POCO объекты.
Довольно много информации, включая ссылки, можно почерпнуть из поста Саши Entity Framework 4.0: выходим на зрелый уровень.
В различных блогах я довольно часто встречал мнение о том, что порог вхождения в NHibernate выше чем в Entity Framework. Так же довольно часто противопоставляют Anemic Data Model у Entity Framework и Rich Data Model у NHibernate. Например в посте Ivan'а Старые песни о главном: роль ООП при работе с данными... количество комментариев перевалило за 90. :)
Что же я хотел получить от NHibernate? Вот список важных для меня моментов:
- полная изоляция от базы данных во всех юнит-тестах. В первую очередь рассматривал mockинг DAO/Repository. Еще витали мысли об использовании предварительно подготовленной SQLite базы, но я отказался от этой идеи;
- покрытие тестами уровня доступа к данным. Возможность написания интеграционных тестов на живую БД, которые покрывают все DAO/Repository;
- хотел получить Persistance Ignorance как можно меньшей кровью;
- иметь возможность использовать Linq для написания запросов, а так же какой-нибудь API для вызова хранимых процедур.
В начале я пробовал описать конфигурацию NHibernate в Application configuration file, потом описывал конфигурацию императивно в коде и в конечном счете остановился императивной конфигурации в коде с помощью библиотеки Fluent NHibernate.
Конфигурация выглядит достаточно просто и очень легко читается:
var session = Fluently.Configure()
.Database(SQLiteConfiguration.Standard
.ShowSql()
.UsingFile(@"D:\projects\DotNET\NHibernatePlayground\DB\northwindEF.db"))
.Mappings(m => m.FluentMappings.AddFromAssemblyOf())
.BuildSessionFactory()
.OpenSession();
Так же, по мере изучения configuraion API у NHibernate, я обнаружил возможность трассировки всех SQL-запросов в лог с помощью конфигурирования трассировщика в log4net. Пока это не опробовал, но планирую это сделать. Это довольно удобно для отладки и этого очень сильно не хватало при работе с Entity Framework. Там была возможность написать свой механизм трассировки, но такого готового решения, как у NHibernate я у EF не обнаружил. Буду очень рад, если узнаю что EF это умеет и что я просто недостаточно хорошо искал.
Далее я пришел к выводу, что у NHibernate-решения существует по крайней мере три подхода для работы с данными:
- “канонический” подход, который пришел с Hibernate. Программист описывает объектную модель в виде набора POCO-объектов. Если со стороны БД у нас есть связи, то со стороны .NET у нас будут navigation-свойства с типизированными коллециями. Меппинг между .NET и БД описывается в специальных *.hbm.xml-файлах. Вполне возможно, этот подход можно было бы назвать удобным, если бы у него была мощная поддержка в Visual Studio в виде визуального дизайнера;
- проект Castle ActiveRecord. Однако я эти проекты не рассматривал, т.к. ActiveRecord влияет на мою доменную модель. При его использовании мы должны помечать свои классы и свойства атрибутами, которые описывают меппинг к БД, а это нарушает Persistence Ignorance. В любом случае, этот подход вполне имеет право на жизнь в небольших проектах;
- описание меппинга с помощью Fluent NHibernate. Мне этот вариант понравился больше всего.
Кроме того, у Fluent NHibernate есть две killer-фичи - это Auto Mapping + Conventions и Persistence specification testing.
Auto Mapping я пока не использовал и пошел по пути “медленно но верно”. В качестве исходной БД я взял базу Northwind. По мере описания меппинга, я удивился насколько мощными возможностями обладает NHibernate. Например, NHibernate умеет круто меппить иерархии классов - table per class hierarchy, table per subclass, table per concrete class. Не знаю, насколько Fluent NHibernate покрывает возможности, заложенные в hbm.xml-меппинге, но он помог мне описать все нужные мне правила, не смотря на то, что я ни в коем случае не пытался прогнуть доменную модель под схему базы. Весь API по меппингу у Fluent NHibernate можно посмотреть по этой ссылке.
Вот например как у меня выглядит класс Customer:
public class Customer
{
public virtual string ID { get; set; }
public virtual string CompanyName { get; set; }
public virtual string ContactName { get; set; }
public virtual string ContactTitle { get; set; }
public virtual string Address { get; set; }
public virtual string City { get; set; }
public virtual string Region { get; set; }
public virtual string PostalCode { get; set; }
public virtual string Country { get; set; }
public virtual string Phone { get; set; }
public virtual string Fax { get; set; }
public virtual IListOrders { get; private set; }
}
Соответственно, меппинг этого класса выглядит вот так:
public class CustomerMap : ClassMap
{
public CustomerMap()
{
Table("Customers");
Id(x => x.ID).Column("CustomerID").Length(5);
Map(x => x.CompanyName).Length(40).Not.Nullable();
Map(x => x.ContactName).Length(30);
Map(x => x.ContactTitle).Length(30);
Map(x => x.Address).Length(60);
Map(x => x.City).Length(15);
Map(x => x.Region).Length(15);
Map(x => x.PostalCode).Length(10);
Map(x => x.Country).Length(15);
Map(x => x.Phone).Length(24);
Map(x => x.Fax).Length(24);
HasMany(x => x.Orders).KeyColumn("CustomerID");
}
}
Учитывая, что меппинг описывается вручную, то для того чтобы быть 100% уверенным в его корректности, необходимы тесты. Команда Fluent NHibernate подумала и над этой проблемой и предложила решение в виде Persistence specification testing, который моем случае выглядит следующим образом:
[TestMethod]
public void Save_Customer_in_database()
{
RemoveCustomerIfExists("A");
new PersistenceSpecification(TestHelper.GetSession())
.CheckProperty(c => c.ID, "A")
.CheckProperty(c => c.CompanyName, "TestCompanyName")
.CheckProperty(c => c.ContactName, "TestContactName")
.CheckProperty(c => c.ContactTitle, "TestContactTitle")
.CheckProperty(c => c.Address, "TestAddress")
.CheckProperty(c => c.City, "TestCity")
.CheckProperty(c => c.Region, "TestRegion")
.CheckProperty(c => c.PostalCode, "TestPostalCode")
.CheckProperty(c => c.Country, "TestCountry")
.CheckProperty(c => c.Phone, "TestPhone")
.CheckProperty(c => c.Fax, "TestFax")
.VerifyTheMappings();
}
Этот тест делает следующее:
- приводит БД в предсостояние для теста (удаляет кастомера, если он уже существует);
- создает экземпляр Customer с заданными параметрами;
- вставляет данные по этому кастомеру в базу данных;
- извлекает из базы данных запись в другой экземпляр класса Customer;
- проверяет что полученный Customer соответствует оригинальному.
На этом пожалуй хватит для одного поста. Буду очень благодарен за любой feedback, как положительный так и отрицательный.
[UPDATE]
Поменял тему в блоге на более нейтральную. Подключил google-code-prettify для подсветки синтаксиса. Надеюсь теперь будет удобне читать.