Не так давно попался под руки новый проект. До сих пор, в основном, приходилось допиливать старые. В проекте предполагалось использование БД. Погуглив немного решил отказаться от старых методов работы с данными в пользу 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:
Сразу отмечу особенность NHibernate: он не требует никаких дополнительных усилий по реализации специфического интерфейса или наследования от базового класса. Т.е. вы можете описывая объекты бизнес-логики не задумываться о реализации каких бы то ни было механизмов для взаимодействия с БД. Всю грязную работу на себя возьмет NHibernate. Но каким бы он ни был всемогущим, ему все равно надо брать откуда то информацию о классах, их полях и свойствах. Есть два способа предоставить эту информацию. Первый — описать всю необходимую информацию в атрибутах класса, его полей и свойств, второй — задать все необходимые соответствия между классом, его данными и таблицей в БД в mapping-файле. Считаю второй способ более удобным и наглядным(отделим мух от котлет ;)).
о_О Знаю, что это субъективное мнение, но мне так проще. К тому же есть инструменты облегчающие и автоматизирующие это.
О них будет рассказано в продолжении серии.
Mapping-файл может выглядеть примерно так:
Здесь мы описываем класс который мапим и таблицу в БД в которой будут хранится данные.
Здесь задается идентификатор. В качестве генератора идентификатора используется класс Identity, который есть в SQL Serrver-е. Можно использовать некоторые другие генераторы, а также написать самому класс, реализующий интерфейс NHibernate.Id.IIdentifierGenerator.
Ну и здесь описываются все остальные свойства нашего класса.
Для начала этого, думаю, будет достаточно. Подробнее о mapping-файлах будет рассказано позже. По сложившейся традиции файлу дают название Имя_класса.hbm.xml, но это не является обязательным требованием. Далее не забудьте указать в свойствах этого xml файла Build Action = Embedded Resource для того, чтоб файл был включен в финальную сборку.
Следующим шагом будет подключение к БД, где мы и будем хранить даные. Нам необходимо создать файл настроек для NHibernate. В нашем первом примере он будет предельно простым, но все же достаточным для того, чтоб можно было полноценно работать с БД.
Выглядит он следующим образом:
Обязательный элемент. Здесь находятся все настройки для объекта класса ISessionFactory.
connection.provider — Провайдер для подключения к базе. Должен реализовывать интерфейс IConnectionProvider. В нашем случае используется родной провайдер.
connection.driver_class — Класс реализующий IDriver. В нашем случае используется встроенный драйвер для подключения к MS SQL Server.
dialect — Реализация диалекта для конкретного сервера БД. В нашем случае используется NHibernate.Dialect.MsSql2005Dialect, так как мы используем MS SQL Server 2005 Express Edition.
connection.connection_string — Строка подключения к БД.
Здесь мы указываем mapping-файл нашего класса и сборку, в которой его можно найти.
Следующим шагом будет конфигурирование самого NHibernate. За это отвечает класс Configuration.
Можно и не задавать название файла конфигурации «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.
Для упрощения работы с ISessionFactory и ISession мы создадим класс Domain. Здесь я не буду приводить весь текст класса,
а только метод получения сессии:
В зависимости от того работаем мы с БД из web- или win-приложения сессия сохраняется/извлекается в/из
HttpContext.Current.Items и CallContext соответственно. Конечно сам класс Domain далек от идеала и мне там не все нравится, но пока ничего лучшего не придумал. Буду рад советам. :)
Итак мы закончили поготовку простой и минимально необходимой инфрасткутуры для нашего не менее простого примера демонтрации работы NHibernate:
Надеюсь, код с комментариями предельно ясен. Я намеренно не стал приводить здесь примеры посложнее. Эту первую статью я постарался сделать как можно проще, чтобы создать базу для последующих, в которых будет (я очень надеюсь ;)) более глубокое содержание материала.
Исходники проекта можно скачать здесь.
UPD: Совсем забыл :-[, хочу выразить отдельную благодарность XaocCPS за помощь в редактировании статьи.
Главная задача 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:
- public class Book
- {
- private long id;
- private string isbn;
- private string title;
-
- public virtual long ID
- {
- get { return id; }
- protected set { id = value; }
- }
-
- public virtual string ISBN
- {
- get { return isbn; }
- set { isbn = value; }
- }
-
- public virtual string Title
- {
- get { return title; }
- set { title = value; }
- }
-
- public Book()
- {
- }
-
- public Book(string bookIsbn, string bookTitle)
- {
- isbn = bookIsbn;
- title = bookTitle;
- }
- }
* 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-файл может выглядеть примерно так:
- <?xml version="1.0" encoding="utf-8" ?>
- <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHibernate_1" assembly="NHibernate_1">
- <class name="Book" table="Books">
- <id name="ID" unsaved-value="0">
- <column name="ID" not-null="true" />
- <generator class="identity"/>
- </id>
- <property name="ISBN" />
- <property name="Title" />
- </class>
- </hibernate-mapping>
* This source code was highlighted with Source Code Highlighter.
- <class name="Book" table="Books">
Здесь мы описываем класс который мапим и таблицу в БД в которой будут хранится данные.
- <id name="ID" unsaved-value="0">
- <column name="ID" not-null="true" />
- <generator class="identity"/>
- </id>
Здесь задается идентификатор. В качестве генератора идентификатора используется класс Identity, который есть в SQL Serrver-е. Можно использовать некоторые другие генераторы, а также написать самому класс, реализующий интерфейс NHibernate.Id.IIdentifierGenerator.
- <property name=«ISBN» />
- <property name=«Title» />
Ну и здесь описываются все остальные свойства нашего класса.
Для начала этого, думаю, будет достаточно. Подробнее о mapping-файлах будет рассказано позже. По сложившейся традиции файлу дают название Имя_класса.hbm.xml, но это не является обязательным требованием. Далее не забудьте указать в свойствах этого xml файла Build Action = Embedded Resource для того, чтоб файл был включен в финальную сборку.
Следующим шагом будет подключение к БД, где мы и будем хранить даные. Нам необходимо создать файл настроек для NHibernate. В нашем первом примере он будет предельно простым, но все же достаточным для того, чтоб можно было полноценно работать с БД.
Выглядит он следующим образом:
- <?xml version='1.0' encoding='utf-8'?>
- <hibernate-configuration>
-
- <session-factory xmlns="urn:nhibernate-configuration-2.2">
-
- <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
- <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
- <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
- <property name="connection.connection_string">Server=DevDB;Initial Catalog=NHibTut;Trusted_Connection=yes;</property>
-
- <mapping resource="NHibernate_1.Book.hbm.xml" assembly="NHibernate_1" />
-
- </session-factory>
-
- </hibernate-configuration>
* This source code was highlighted with Source Code Highlighter.
- <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) для вашей базы. Таким образом вам больше не надо будет беспокоиться о строке подключения откуда бы вы ни работали. |
- <mapping resource="NHibernate_1.Book.hbm.xml" assembly="NHibernate_1" />
Здесь мы указываем mapping-файл нашего класса и сборку, в которой его можно найти.
Следующим шагом будет конфигурирование самого NHibernate. За это отвечает класс Configuration.
- 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. Здесь я не буду приводить весь текст класса,
а только метод получения сессии:
- private static ISession GetSession(bool getNewIfNotExists)
- {
- ISession currentSession;
-
- if (HttpContext.Current != null)
- {
- HttpContext context = HttpContext.Current;
- currentSession = context.Items[sessionKey] as ISession;
-
- if (currentSession == null && getNewIfNotExists)
- {
- currentSession = sessionFactory.OpenSession();
- context.Items[sessionKey] = currentSession;
- }
- }
- else
- {
- currentSession = CallContext.GetData(sessionKey) as ISession;
-
- if (currentSession == null && getNewIfNotExists)
- {
- currentSession = sessionFactory.OpenSession();
- CallContext.SetData(sessionKey, currentSession);
- }
- }
-
- return currentSession;
- }
* This source code was highlighted with Source Code Highlighter.
В зависимости от того работаем мы с БД из web- или win-приложения сессия сохраняется/извлекается в/из
HttpContext.Current.Items и CallContext соответственно. Конечно сам класс Domain далек от идеала и мне там не все нравится, но пока ничего лучшего не придумал. Буду рад советам. :)
Итак мы закончили поготовку простой и минимально необходимой инфрасткутуры для нашего не менее простого примера демонтрации работы NHibernate:
- class Program
- {
- static void Main()
- {
- // Инициваллизация домена
- Domain.Init();
-
- // Идентификатор объекта Book
- long bookId;
-
- // достаем сессию
- ISession session = Domain.CurrentSession;
-
- // Начинаем явную транзакцию
- ITransaction tr = session.BeginTransaction();
-
- // Создаем экземпляр класса Book
- Book book = new Book("NW8523IDISDN", "How to learn not to do stupid things v.1");
-
- // Сохрананяем объект в сессии
- session.Save(book);
-
- // Запоминаем идентификатор объекта
- bookId = book.ID;
-
- // Завершаем транзакцию. Сейчас данные будут физически записаны в БД
- // После этого можно отрыть таблицу в БД и убедиться, что там действительно появилаь новая запись
- tr.Commit();
- session.Flush();
-
- // Очищаем кэш сессии, чтобы быть уверенными, что объект будет получен из базы, а не из сессии
- session.Clear();
-
- // Пытаемся достать объект из базы по идентификатору
- Book book1 = session.Get<Book>(bookId);
-
- // Сравниваем и видим, что объект который мы сохранили и тот который мы достали из сессии один и тот же
- if (book == book1) { Console.WriteLine("Yep!!!"); }
-
- // Завершаем работу с базой.
- Domain.Close();
- Console.ReadKey();
- }
- }
* This source code was highlighted with Source Code Highlighter.
Надеюсь, код с комментариями предельно ясен. Я намеренно не стал приводить здесь примеры посложнее. Эту первую статью я постарался сделать как можно проще, чтобы создать базу для последующих, в которых будет (я очень надеюсь ;)) более глубокое содержание материала.
Исходники проекта можно скачать здесь.
UPD: Совсем забыл :-[, хочу выразить отдельную благодарность XaocCPS за помощь в редактировании статьи.