Главная страница   /   15.4. Конфигурационная инфраструктура приложения в NHibernate (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

15.4. Конфигурационная инфраструктура приложения в NHibernate

Чтобы подтолкнуть NHibernate к беспрепятственному сохранению, необходимо написать небольшой фрагмент кода. NHibernate – это библиотека, а не фреймворк, и это отличие очень важно. Фреймворки предоставляют шаблоны кода, а вы затем заполняете пробелы, чтобы создать что-то полезное. Библиотеки пригодны для использования без предоставления шаблонов. NHibernate не требует, чтобы ваши объекты наследовались от конкретного базового класса, а также не требует реализации конкретного интерфейса. NHibernate может сохранять разные виды объектов, пока конфигурация корректна.

В этом разделе мы пройдемся по конфигурации NHibernate и увидим, как мы можем сохранять и извлекать объект Visitor. В данной главе мы используем NHibernate 3.0.0.2001 вместе с Fluent NHibernate 1.1 для получения справки о конфигурации. Fluent NHibernate предоставляет без XML-ные, безопасно-компилируемые, автоматизированные, основанные на условных обозначениях преобразования NHibernate. Вы можете найти его на сайте http://fluentnhibernate.org/.

Перед тем, как мы погрузимся в конфигурацию, давайте изучим реализацию интерфейса IVisitorRepository, заданного в доменной модели. Мы начнем с этого класса для того, чтобы продемонстрировать, как записывается небольшой фрагмент кода при вызове NHibernate с целью выполнения операции сохранения. Следующий листинг демонстрирует класс VisitorRepository, расположенный в проекте Infrastructure.

Листинг 15-4: Реализация репозитория, связанного с NHibernate APIs
using System.Linq;
using Core;
using NHibernate;
using NHibernate.Linq;
namespace Infrastructure
{
	public class VisitorRepository : IVisitorRepository
	{
		public void Save(Visitor visitor)
		{
			using (ISession session = DataConfig.GetSession())
			{
				session.BeginTransaction();
				session.SaveOrUpdate(visitor);
				session.Transaction.Commit();
			}
		}
		public Visitor[] GetRecentVisitors(int numberOfVisitors)
		{
			using (ISession session = DataConfig.GetSession())
			{
				Visitor[] recentVisitors =
						session.Query<Visitor>()
								.OrderByDescending(v => v.VisitDate)
								.Take(numberOfVisitors)
								.ToArray();
				return recentVisitors;
			}
		}
	}
}

Строка 13-15: Сохраняет экземпляры Visitor

Строка 22-25: Использует HQL для выбора Visitors

Строка 26-27: Возвращает массив Visitors

Этот класс использует NHibernate API для того, чтобы сохранять экземпляры Visitors, а также извлекать совокупность недавних посетителей сайта. Метод GetRecentVisitors использует язык запросов Hibernate (Hibernate Query Language или HQL) для того, чтобы выполнить запрос относительно базы данных.

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

Конфигурация NHibernate

Началом процесса настройки является файл hibernate.cfg.xml. Этот файл имеет то же название, что и конфигурационный файл, используемый библиотекой Hibernate в Java. Поскольку NHibernate был запущен как порт Hibernate, одним из множества сходств является то, что знания одного, главным образом, напрямую переводятся другому.

Содержание файла hibernate.cfg.xml также может быть помещено в файл Web.config или файл app.config. Для простых приложений вложение этой информации в конфигурационный файл .NET может быть вполне пригодным, но данный пример делает акцент на концепцию разделения с тем, чтобы применительно к приложению среднего размера код и конфигурация не запускались вместе. Мы видели, что размер файлов Web.config увеличивается, и это банально – хранить конфигурацию NHibernate в указанном файле.

Следующий листинг демонстрирует содержимое файла hibernate.cfg.xml.

Листинг 15-5: Файл hibernate.cfg.xml
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
	<session-factory>
		<property name="connection.driver_class">
			NHibernate.Driver.SqlClientDriver
		</property>
		<property name="connection.connection_string">
			server=.\SQLExpress;database=NHibernateSample;
			Integrated Security=true;
		</property>
		<property name="show_sql">false</property>
		<property name="dialect">
			NHibernate.Dialect.MsSql2005Dialect
		</property>
		<property name="adonet.batch_size">100</property>
		<property name="proxyfactory.factory_class">
			NHibernate.ByteCode.Castle.ProxyFactoryFactory,
			NHibernate.ByteCode.Castle
		</property>
	</session-factory>
</hibernate-configuration>

Строка 3: Определяет используемый драйвер

Строка 6: Определяет строку соединения

Строка 11: Определяет используемый диалект

Строка 15: Определяет прокси-фабрику

Это простая конфигурация, и существует множество других вариантов, которые обсуждаются в документации к NHibernate (http://nhforge.org/doc/nh/en/index.html). Наиболее очевидный фрагмент информации – это строка соединения. Также класс драйвера и диалект задают подробную информацию об используемом движке базы данных. В данном примере используется SQL Server 2005, но эти значения можно было бы изменить, если бы вам захотелось использовать версию Oracle, SQLite, или множество других поддерживаемых движков базы данных.

Свойство show_sql будет выводить каждый SQL запрос в консоль, как только утверждение отправляется в базу данных, что довольно полезно для отладки. Свойство adonet.batch_size управляет тем, сколько обновлений, удалений и вставок было отправлено в базу данных в одном пакете. Более эффективно отправлять составные утверждения в одном сетевом вызове, нежели делать отдельные сетевые вызовы для каждого утверждения. NHibernate будет делать это автоматически.

Последний элемент конфигурации – это прокси-фабрика, которая применяется для преобразований, использующих отложенную загрузку (lazy loading), которая является используемой по умолчанию. Если бы мы использовали файлы XML преобразований, мы бы также сконфигурировали комплект, в котором Nhibernate мог бы найти вложенные преобразования, но в данном случае это не является необходимым, поскольку мы используем кодовые преобразования при помощи Fluent NHibernate. Вместо этого мы можем определить направления нашего преобразования в C#.

NHibernate преобразование – простое, но значительное

Для NHibernate требуется, по крайней мере, одно преобразование. Рисунок 15-5 демонстрирует проект Infrastructure, и на этом рисунке вы увидите, что существует файл кода под названием VisitorMap.cs.

Рисунок 15-5: Проект Infrastructure содержит NHibernate преобразование Visitor.

Мы собираемся рассмотреть файл VisitorMap.cs, который содержит информацию о преобразовании класса Visitor. Но сначала отметьте два файла, связанных с проектом:

  • Hibernate.cfg.xml
  • Log4Net.config

Эти файлы не принадлежат к проекту напрямую, они связаны с ним в другом месте. Мы делаем это, поскольку для составных проектов нужна одинаковая копия этих файлов. Первый пример, для которого нужны связанные файлы – это IntegrationTests, он будет содержать тесты для всех доступов к данным. Чтобы выполнить тестирование процесса доступа к данным, тестам необходимо использовать ту же конфигурацию, которую использует и приложение.

Мы уже рассматривали файл hibernate.cfg.xml. Файл Log4Net.config содержит информацию о конфигурации log4net, которая широко применяется для любого типа приложений. Если вы не знакомы с Apache log4net, то вы можете найти больше информации об этом на сайте http://logging.apache.org/log4net/index.html.

Давайте теперь вернемся к преобразованию класса Visitor. Файл VisitorMap.cs продемонстрирован ниже.

Листинг 15-6: Файл VisitorMap.cs содержит преобразование класса Visitor
using Core;
using FluentNHibernate.Mapping;
namespace Infrastructure
{
	public class VisitorMap : ClassMap<Visitor>
	{
		public VisitorMap()
		{
			Not.LazyLoad();
			Table("Visitor");
			Id(x => x.Id).GeneratedBy.GuidComb();
			Map(x => x.PathAndQuerystring).Length(4000).Not.Nullable();
			Map(x => x.LoginName).Length(255).Not.Nullable();
			Map(x => x.Browser).Length(4000).Not.Nullable();
			Map(x => x.VisitDate).Not.Nullable();
			Map(x => x.IpAddress).Not.Nullable();
		}
	}
}

Строка 10: Объявляет таблицу преобразований

Строка 11: Определяет свойство первичного ключа

Первая строка довольно стандартна и задает используемую таблицу. Метод Id – особенный, и он должен быть первым свойством, преобразованным для объекта. Он станет первичным ключом таблицы, а узел генератора имеет множество вариантов определения того, как генерируется этот первичный ключ, включая функциональность SQL Server "identity" и Oracle "sequence". Мы хотим, чтобы для объекта Visitor устанавливалось значение свойства Id до того, как он будет сохранен, поэтому мы сконфигурировали NHibernate так, чтобы он генерировал Guid для нас перед тем, как вставлять утверждение (метод INSERT) в базу данных. Генератор GuidComb() является особенным; он генерирует GUIDs в последовательном порядке с тем, чтобы кластерному индексу столбца первичного ключа мало что нужно было делать при вставке новой записи в таблицу. Такое задание последовательности приносит в жертву некоторую уникальность GUID алгоритма, но в данном контексте важно только то, что GUID является уникальным для этой конкретной таблицы.

Примечание

Вы можете получить больше информации о COMB GUID от создателя, Джимми Нильссона, в его статье "Стоимость GUID в качестве первичных ключей" на сайте http://mng.bz/4q49.

Остальные свойства, главным образом, не требуют разъяснений. Они имеют названия и ограничители, а строки могут иметь заданную длину. Если вы комфортно себя чувствуете в том случае, когда название столбца совпадает с названием свойства класса, то атрибуты столбца не нужны. Когда все свойства преобразованы, вы готовы перейти дальше.

Если у вас более сложная структура класса, то вам нужно будет просмотреть все варианты ваших преобразований в справочной документации к NHibernate (http://nhforge.org/doc/nh/en/index.html) и в документации к Fluent NHibernate (http://fluentnhibernate.org/).

Инициализация конфигурации

В NHibernate есть две основных абстракции: ISessionFactory и ISession. Фабрика сессии создает сессию, и подразумевается, что сессия будет использоваться в приложении для единственной задачи – это может быть единичная транзакция или составные успешные транзакции в быстрой последовательности. Вам следует использовать, а потом быстро ликвидировать NHibernate сессии. С другой стороны, подразумевается, что фабрика сессий будет храниться на протяжении всего жизненного цкила приложения, поэтому она может использоваться для создания всех сессий.

Интерфейс ISession – это абстракция, но реализация, которую предоставляет NHibernate требует некоторых разъяснений. Следующий листинг демонстрирует, как создать фабрику сессий, которая будет использоваться на протяжении всего жизненного цкила приложения.

Листинг 15-7: Объект Configuration, который создает фабрику сессий
public class DataConfig
	{
		private static ISessionFactory _sessionFactory;
		private static bool _startupComplete = false;
		private static readonly object _locker = new object();

		public static ISession GetSession()
		{
			ISession session = _sessionFactory.OpenSession();
			session.BeginTransaction();
			return session;
		}
		public static void EnsureStartup()
		{
			if (!_startupComplete)
			{
				lock (_locker)
				{
					if (!_startupComplete)
					{
						DataConfig.PerformStartup();
						_startupComplete = true;
					}
				}
			}
		}
		private static void PerformStartup()
		{
			InitializeLog4Net();
			InitializeSessionFactory();
			InitializeRepositories();
		}
		private static void InitializeSessionFactory()
		{
			Configuration configuration = BuildConfiguration();
			_sessionFactory = configuration.BuildSessionFactory();
		}
		public static Configuration BuildConfiguration()
		{
			return Fluently.Configure(new Configuration().Configure())
						.Mappings(cfg =>
							cfg.FluentMappings
								.AddFromAssembly(typeof(VisitorMap).Assembly))
						.BuildConfiguration();
		}
		private static void InitializeLog4Net()
		{
			string configPath = Path.Combine(
					AppDomain.CurrentDomain.BaseDirectory,
					"Log4Net.config");
			var fileInfo = new FileInfo(configPath);
			XmlConfigurator.ConfigureAndWatch(fileInfo);
		}
		private static void InitializeRepositories()
		{
			Func<IVisitorRepository> builder = () => new VisitorRepository();
			VisitorRepositoryFactory.RepositoryBuilder = builder;
		}
	}

Строка 35: Конфигурирует NHibernate с помощью XML конфигурации

Строка 36: Создает и кэширует фабрику сессий

Строка 40-44: Применяет Fluent NHibernate преобразования

Создание фабрики сессий – трудозатратно. Она выполняет довольно много инициализации и валидации для того, чтобы убедиться, что она может быстро выполнять доступ к данным с помощью объекта сессии. Объект конфигурации читает файл hibernate.cfg.xml (который представляет собой внепроцессный вызов) и затем создает фабрику сессий, используя эту конфигурацию. При создании фабрики сессий объект конфигурации будет применять все свойства, найденные в конфигурационном файле. Если в комплект были включены вложенные XML преобразования, то объект конфигурации извлечет все эти файлы преобразований из DLLs (который является еще одним внепроцессным вызовом). Каждый файл преобразований будет проанализирован с помощью XML DOM. Независимо от того, используете ли вы преобразования кода или XML преобразования, NHibernate будет использовать рефлексию для всех типов для того, чтобы убедиться в том, что каждое свойство, объявленное в преобразовании, существует в указанных типах. Если отложенная загрузка разрешена (по умолчанию), то NHibernate также проверит, что все открытые свойства и методы отмечены с помощью virtual. Если вы предпочитаете не помечать их virtual, как это делаем мы, то вам нужно будет запретить отложенную загрузку.

Для большинства приложений создание фабрики сессий занимает, по крайней мере, целую секунду (или более), поэтому эта операция – это то, что вам не хотелось бы выполнять часто. Если бы вы создавали фабрику сессий для каждого веб-запроса, то ваше приложение резко замедлилось бы. Мы поместим экземпляр фабрики сессий в статическую переменную таким образом, чтобы могли использовать ее на протяжении всего жизненного цикла приложения.

NHibernate сессия, с другой стороны, является малозатратной. Мы будем создавать и разрушать множество таких объектов. В приложении, имеющем внутреннее состояние, мы будем использовать сессию для единичной транзакции или пользовательской операции. Код создания сессии похож на следующий:

ISession session = SessionFactory.OpenSession();

Перед тем, как мы сможем перейти к коду, который использует ISession, мы должны быть обладателями базы данных. Мы объявили нашу строку соединения, и благодаря преобразованию NHibernate знает структуру таблицы. Мы можем продолжать создавать схему нашей базы данных вручную или мы можем использовать NHibernate. Для того чтобы использовать NHibernate для создания нашей схемы, мы можем создать пустую базу данных под названием NHibernateSample (как это объявлено строкой соединения) внутри SQL Server Express и выполнить код, продемонстрированный ниже:

Листинг 15-8: NHibernate генерирует базу данных из преобразований
using Infrastructure;
using NHibernate.Tool.hbm2ddl;
using NUnit.Framework;
namespace IntegrationTests
{
	[TestFixture]
	public class DatabaseTester
	{
		[Test, Explicit]
		public void CreateDatabaseSchema()
		{
			var export = new SchemaExport(DataConfig.BuildConfiguration());
			export.Execute(true, true, false);
		}
	}
}

Мы используем конструкцию NUnit теста как легкую возможность запуска этого кода, что делает тривиальным выполнение фрагментов кода. После выполнения этого теста в Visual Studio с помощью встроенного TestDriven.Net (http://testdriven.net/), вы увидите выходной результат в окне Output. В рамках нашей системы в окне Output продемонстрирован следующий текст.

Листинг 15-9: Выходной результат экспорта схемы
------ Test started: Assembly: IntegrationTests.dll ------
	if exists (select * from dbo.sysobjects where id = object_id(N'Visitor')
		and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table Visitor
	create table Visitor (
		Id UNIQUEIDENTIFIER not null,
		PathAndQuerystring NVARCHAR(4000) not null,
		LoginName NVARCHAR(255) not null,
		Browser NVARCHAR(4000) not null,
		VisitDate DATETIME not null,
		IpAddress NVARCHAR(255) not null,
		primary key (Id)
	)
1 passed, 0 failed, 0 skipped, took 1.29 seconds (NUnit 2.5.5).

NUnit тест расположен в проекте IntegrationTests, который также связан с файлом hibernate.cfg.xml для того, чтобы использовать ту же самую конфигурацию. Рисунок 15-6 демонстрирует структуру проекта IntegrationTests. Мы храним его минимальным ради простоты.

Рисунок 15-6: Проект IntegrationTests содержит тесты для всех преобразований и репозиториев.

Обратите внимание на класс VisitorRepositoryTester. В него входит автоматизированное тестирование, необходимое для того, чтобы убедиться, что реализация репозитория функционирует так, как и ожидалось. Мы не можем писать модульные тесты для процесса доступа к данным, потому что процесс доступ к данным, по своей сути, – это дело интеграционного теста. Мы не только осуществляем интеграцию со сторонней библиотекой, NHibernate, но мы также ожидаем, что в нашей сети, сервере или рабочей станции будет запускаться еще один процесс. SQL Server должен подниматься и запускаться, а также он должен содержать корректную схему. Если что-то пойдет не так, то тест не выполнится. Благодаря такому размещению эти интеграционные тесты являются более сложными, чем тесты, для которых не нужны сохраненные данные. Все-таки при написании тестов доступа к данным делайте их такими небольшими, насколько это возможно, и тестируйте только процесс доступ к данным.

Следующий листинг демонстрирует код для VisitorRepositoryTester.

Листинг 15-10: Интеграционные тесты
using System;
using System.Collections.Generic;
using System.Linq;
using Core;
using Infrastructure;
using NHibernate;
using NUnit.Framework;
namespace IntegrationTests
{
	[TestFixture]
	public class VisitorRepositoryTester
	{
		[SetUp]
		public void Setup()
		{
			new DatabaseTester().CreateDatabaseSchema();
			DataConfig.EnsureStartup();
		}
		[Test]
		public void When_saving_should_write_to_database()
		{
			var visitor = new Visitor
			{
				Browser = "1",
				IpAddress = "2",
				LoginName = "3",
				PathAndQuerystring = "4",
				VisitDate =
				new DateTime(2000, 1, 1)
			};
			var repository = new VisitorRepository();
			repository.Save(visitor);
			Visitor loadedVisitor;
			using (ISession session = DataConfig.GetSession())
			{
				loadedVisitor = session.Load<Visitor>(visitor.Id);
			}
			Assert.That(loadedVisitor, Is.Not.Null);
			Assert.That(loadedVisitor.Browser, Is.EqualTo("1"));
			Assert.That(loadedVisitor.IpAddress, Is.EqualTo("2"));
			Assert.That(loadedVisitor.LoginName, Is.EqualTo("3"));
			Assert.That(loadedVisitor.PathAndQuerystring, Is.EqualTo("4"));
			Assert.That(loadedVisitor.VisitDate, Is.EqualTo(new DateTime(2000, 1, 1)));
		}
	}
}

Строка 17: Конфигурирует NHibernate

Строка 22: Создает новый Visitor

Строка 32: Сохраняет Visitor

Строка 34: Создает новую сессию

Строка 36: Перегружает Visitor

Строка 38-43: Принимает корректные данные

Эти тесты являются существенными для того, чтобы убедиться в том, что каждый запрос, который генерируется NHibernate, тестируется и повторно тестируется при каждом создании. Поскольку изменения конфигурации будут модифицировать генерируемые запросы, тесты очень важны для стабильности приложения.

Когда мы запускаем тесты в листинге 15-10, мы видим, что они выполняются, как это показано на рисунке 15-7.

Рисунок 15-7: Когда выполняется тест репозитория, мы знаем, что преобразование корректно. Результаты теста отображаются в ReSharper test runner.

Всякое использование NHibernate API должно оставаться в проекте Infrastructure. Помните, что ни один из остальных проектов решения не имеет сслыки на Infrastructure, поэтому остальная часть кода не связана с этой конкретной библиотекой для доступа к данным. Такое разъединение является важным, поскольку методы доступа к данным часто изменяются. Вы не хотите связывать ваше приложение с инфраструктурными элементами в тех случаях, когда они, вероятно, часто изменяются.

Теперь вы знакомы с основными принципами сохранения при помощи NHibernate. Мы рассмотрели как проект Core, так и проект Infrastructure, поэтому давайте посмотрим, как они связываются друг с другом в пользовательском интерфейсе.