GrabDuck

Основы NHibernate. Часть 1

:

Не так давно попался под руки новый проект. До сих пор, в основном, приходилось допиливать старые. В проекте предполагалось использование БД. Погуглив немного решил отказаться от старых методов работы с данными в пользу ORM. Да, есть много кодогенераторов(например, CodeSmith), которые в считанные секунды создадут уровень доступа к данным, но такие решения не отличаются гибкостью, а при дальнейшем развитии грозят превратиться в кошмар. Хотя и у ORM тоже есть свои недостатки. Но обо всем по порядку. Сейчас же я хочу поделиться с вами моим опытом в освоении одного из представителей мира ORM — NHibernate. Почему из всех возможных ORM я выбрал для изучения NHibernate? Во-первых, потому что надо было выбрать что-то одно. Во-вторых, история NHibernate уходит глубоко корнями в ORM-фреймвокр Hibernate для Java и является достаточно зрелым решением. Больше пока, вроде, и нет аргументов, но, думаю, они появятся позже при более близком знакомстве с NHibernate.

Главная задача ORM(Object-Relational Mapping) заключается в том, чтобы являться тем связующим звеном, которое прозрачно для нас будет делать всю работу по сохранению данных наших бизнес-объектов и заполнению их даными из базы при необходимости. К тому же ORM избавляет от необходимости большого количества однотипного примитивного кода выполняющего всего лишь CRUD-операции(Create, Read, Update, Delete). Это конечно самое простое объяснение смысла ORM, но думаю идея ясна. Больше можно прочитать здесь.

Прежде чем начать работать нам необходима сама библиотека. Пока будет идти речь о версии 1.2.1, если не будет указано обратное. Совсем недавно вышла вторая версия фреймворка в которой обещали много вкусностей, но опять же не будем торопить события ;). Архив со всем необходимым можно скачать здесь. Распаковав архив, вы увидите множество файлов, но на первых порах не все они нам будут нужны. Самым главным и необходимым для нас файлом будет NHibernate.dll. Это пока единственная библиотека, референс на которую нужно добавлять в проект. Также необходимы будут Iesi.Collections.dll, Castle.DynamicProxy.dll и log4net.dll, но их нет нужды добавлять, NHibernate.dll сам их подгрузит
при необходимости.

Для понимания основ NHibernate мы создадим простенький класс, сконфигурируем наше приложение и попробуем создать экземпляр класса, сохранить его в базе и достать его обратно.

Первым делом создадим простой класс Book:

  1. public class Book
  2. {
  3.   private long id;
  4.   private string isbn;
  5.   private string title;
  6.  
  7.   public virtual long ID
  8.   {
  9.     get { return id; }
  10.     protected set { id = value; }
  11.   }
  12.  
  13.   public virtual string ISBN
  14.   {
  15.     get { return isbn; }
  16.     set { isbn = value; }
  17.   }
  18.  
  19.   public virtual string Title
  20.   {
  21.     get { return title; }
  22.     set { title = value; }
  23.   }
  24.  
  25.   public Book()
  26.   {
  27.   }
  28.  
  29.   public Book(string bookIsbn, string bookTitle)
  30.   {
  31.     isbn = bookIsbn;
  32.     title = bookTitle;
  33.   }
  34. }
* This source code was highlighted with Source Code Highlighter.

Сразу отмечу особенность NHibernate: он не требует никаких дополнительных усилий по реализации специфического интерфейса или наследования от базового класса. Т.е. вы можете описывая объекты бизнес-логики не задумываться о реализации каких бы то ни было механизмов для взаимодействия с БД. Всю грязную работу на себя возьмет NHibernate. Но каким бы он ни был всемогущим, ему все равно надо брать откуда то информацию о классах, их полях и свойствах. Есть два способа предоставить эту информацию. Первый — описать всю необходимую информацию в атрибутах класса, его полей и свойств, второй — задать все необходимые соответствия между классом, его данными и таблицей в БД в mapping-файле. Считаю второй способ более удобным и наглядным(отделим мух от котлет ;)).

о_О Знаю, что это субъективное мнение, но мне так проще. К тому же есть инструменты облегчающие и автоматизирующие это.
О них будет рассказано в продолжении серии.

При написании или правки мэппинга вручную совсем не хочется обращаться каждый раз к справочнику, чтобы вспомнить нужный атрибут или элемент. В этом нам поможет VS и xsd-схемы для конфигурационного и мэппинг файлов. Они находятся в скачанном ранее архиве. Это nhibernate-configuration-2.0.xsd, nhibernate-mapping-2.0.xsd и nhibernate-generic.xsd. Копируем их в папку [Path To Microsoft Visual Studio Folder]\Xml\Schemas и перезапускаем студию. Всё, можно пользоваться Intellisense при создании mapping-файлов.

Mapping-файл может выглядеть примерно так:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHibernate_1" assembly="NHibernate_1">
  3.  <class name="Book" table="Books">
  4.   <id name="ID" unsaved-value="0">
  5.    <column name="ID" not-null="true" />
  6.     <generator class="identity"/>
  7.   </id>
  8.   <property name="ISBN" />
  9.   <property name="Title" />
  10.  </class>
  11. </hibernate-mapping>
* This source code was highlighted with Source Code Highlighter.

  1. <class name="Book" table="Books">

Здесь мы описываем класс который мапим и таблицу в БД в которой будут хранится данные.
  1. <id name="ID" unsaved-value="0">
  2.  <column name="ID" not-null="true" />
  3.   <generator class="identity"/>
  4. </id>

Здесь задается идентификатор. В качестве генератора идентификатора используется класс Identity, который есть в SQL Serrver-е. Можно использовать некоторые другие генераторы, а также написать самому класс, реализующий интерфейс NHibernate.Id.IIdentifierGenerator.
  1. <property name=«ISBN» />
  2. <property name=«Title» />

Ну и здесь описываются все остальные свойства нашего класса.

Для начала этого, думаю, будет достаточно. Подробнее о mapping-файлах будет рассказано позже. По сложившейся традиции файлу дают название Имя_класса.hbm.xml, но это не является обязательным требованием. Далее не забудьте указать в свойствах этого xml файла Build Action = Embedded Resource для того, чтоб файл был включен в финальную сборку.

Следующим шагом будет подключение к БД, где мы и будем хранить даные. Нам необходимо создать файл настроек для NHibernate. В нашем первом примере он будет предельно простым, но все же достаточным для того, чтоб можно было полноценно работать с БД.

Выглядит он следующим образом:

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <hibernate-configuration>
  3.    
  4.    <session-factory xmlns="urn:nhibernate-configuration-2.2">
  5.       
  6.       <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
  7.       <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
  8.       <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
  9.       <property name="connection.connection_string">Server=DevDB;Initial Catalog=NHibTut;Trusted_Connection=yes;</property>
  10.  
  11.       <mapping resource="NHibernate_1.Book.hbm.xml" assembly="NHibernate_1" />
  12.  
  13.    </session-factory>
  14.    
  15. </hibernate-configuration>
* This source code was highlighted with Source Code Highlighter.
  1. <session-factory xmlns="urn:nhibernate-configuration-2.2">

Обязательный элемент. Здесь находятся все настройки для объекта класса ISessionFactory.

connection.provider — Провайдер для подключения к базе. Должен реализовывать интерфейс IConnectionProvider. В нашем случае используется родной провайдер.

connection.driver_class — Класс реализующий IDriver. В нашем случае используется встроенный драйвер для подключения к MS SQL Server.

dialect — Реализация диалекта для конкретного сервера БД. В нашем случае используется NHibernate.Dialect.MsSql2005Dialect, так как мы используем MS SQL Server 2005 Express Edition.

connection.connection_string — Строка подключения к БД.

Здесь хочу сделать небольшое уточнение. Так как зачастую приходится работать на разных компьютерах, то для того чтоб постоянно не менять строку подключения к БД, можно создать псевдоним(alias) для вашей базы. Таким образом вам больше не надо будет беспокоиться о строке подключения откуда бы вы ни работали.
  1. <mapping resource="NHibernate_1.Book.hbm.xml" assembly="NHibernate_1" />

Здесь мы указываем mapping-файл нашего класса и сборку, в которой его можно найти.

Следующим шагом будет конфигурирование самого NHibernate. За это отвечает класс Configuration.

  1. ISessionFactory sessionFactory = new Configuration().Configure("Nhibernate.cfg.xml").BuildSessionFactory();

Можно и не задавать название файла конфигурации «Nhibernate.cfg.xml», тогда он должен называться «hibernate.cfg.xml», потому что именно файл с таким названием ищет в корневом каталоге NHibernate. В приведенном выше примере мы сконфигурировали NHibernate и на основе даной конфигурации создали фабрику сессий ISessionFactory.

ISessionFactory — интерфейс служащий фабрикой по созданию ISession непосредственно для работы с БД. Создание ISessionFactory ресурсоемкий процесс и предполагается использованние его единственного экземпляра из любых частей приложения, так как является потокобезопасным. Если из вашего приложения есть необходимость подключения к нескольким БД, то для каждой из них нужно создавать отдельный ISessionFactory. ISessionFactory также кэширует SQL запросы к базе, может хранить кэшированные данные, которые были запрошены из базы.

Для выполнения любого действия связанного с БД, будь то запрос, сохранение, редактирование объекта, нам не обойтись без ISession. Это легковесный интерфейс и его создание требует совсем немного ресурсов. Разработчики NHibernate советуют создавать его каждый раз при обращении к базе. Для примера в ASP.NET приложении можно создавать экземпляр ISession на каждый HttpRequest. ISession не является потокобезопасным, поэтому необходимо его использовать в одном потоке. ISession является реализацией разработчиками NHibernate паттерна Unit of Work.

Unit of Work содержит список объектов, отслеживает их изменения, а затем записывает изменения в базу.

UnitofWork отслеживает все изменения, так как является единственным способом доступа к базе.
Объекты, не помещенные в UnitOfWork, не будут записаны в базу, т.е. при использовании данного шаблона нельзя обращаться к базе напрямую.

Алгоритм работы с Unit of Work в общем случае такой:

1. Объект считывается из базы и регистрируется в списке Unit of Work.
Сразу после считывания из базы объект помечается как «чистый», т.е. Dirty = false;
2. При изменении или при создании объект помечается как Dirty = true
3. При удалении как Deleted
4. Для записи изменений в базу вызывается Commit

Для упрощения работы с ISessionFactory и ISession мы создадим класс Domain. Здесь я не буду приводить весь текст класса,
а только метод получения сессии:

  1. private static ISession GetSession(bool getNewIfNotExists)
  2. {            
  3.    ISession currentSession;
  4.  
  5.    if (HttpContext.Current != null)
  6.    {
  7.       HttpContext context = HttpContext.Current;
  8.       currentSession = context.Items[sessionKey] as ISession;
  9.  
  10.       if (currentSession == null && getNewIfNotExists)
  11.       {
  12.          currentSession = sessionFactory.OpenSession();
  13.          context.Items[sessionKey] = currentSession;
  14.       }
  15.    }
  16.    else
  17.    {
  18.       currentSession = CallContext.GetData(sessionKey) as ISession;
  19.  
  20.       if (currentSession == null && getNewIfNotExists)
  21.       {
  22.          currentSession = sessionFactory.OpenSession();
  23.          CallContext.SetData(sessionKey, currentSession);
  24.       }
  25.    }
  26.  
  27.    return currentSession;
  28. }
* This source code was highlighted with Source Code Highlighter.

В зависимости от того работаем мы с БД из web- или win-приложения сессия сохраняется/извлекается в/из
HttpContext.Current.Items и CallContext соответственно. Конечно сам класс Domain далек от идеала и мне там не все нравится, но пока ничего лучшего не придумал. Буду рад советам. :)

Итак мы закончили поготовку простой и минимально необходимой инфрасткутуры для нашего не менее простого примера демонтрации работы NHibernate:

  1. class Program
  2. {
  3.    static void Main()
  4.    {
  5.       // Инициваллизация домена
  6.       Domain.Init();
  7.       
  8.       // Идентификатор объекта Book
  9.       long bookId;
  10.       
  11.       // достаем сессию
  12.       ISession session = Domain.CurrentSession;
  13.       
  14.       // Начинаем явную транзакцию
  15.       ITransaction tr = session.BeginTransaction();
  16.  
  17.       // Создаем экземпляр класса Book
  18.       Book book = new Book("NW8523IDISDN", "How to learn not to do stupid things v.1");
  19.                
  20.       // Сохрананяем объект в сессии
  21.       session.Save(book);
  22.       
  23.       // Запоминаем идентификатор объекта
  24.       bookId = book.ID;
  25.       
  26.       // Завершаем транзакцию. Сейчас данные будут физически записаны в БД
  27.       // После этого можно отрыть таблицу в БД и убедиться, что там действительно появилаь новая запись
  28.       tr.Commit();
  29.       session.Flush();
  30.  
  31.       // Очищаем кэш сессии, чтобы быть уверенными, что объект будет получен из базы, а не из сессии
  32.       session.Clear();
  33.       
  34.       // Пытаемся достать объект из базы по идентификатору
  35.       Book book1 = session.Get<Book>(bookId);
  36.  
  37.       // Сравниваем и видим, что объект который мы сохранили и тот который мы достали из сессии один и тот же
  38.       if (book == book1) { Console.WriteLine("Yep!!!"); }
  39.  
  40.       // Завершаем работу с базой.
  41.       Domain.Close();
  42.       Console.ReadKey();
  43.    }
  44. }
* This source code was highlighted with Source Code Highlighter.

Надеюсь, код с комментариями предельно ясен. Я намеренно не стал приводить здесь примеры посложнее. Эту первую статью я постарался сделать как можно проще, чтобы создать базу для последующих, в которых будет (я очень надеюсь ;)) более глубокое содержание материала.

Исходники проекта можно скачать здесь.

UPD: Совсем забыл :-[, хочу выразить отдельную благодарность XaocCPS за помощь в редактировании статьи.