В этой статье я продемонстрирую реализацию внедрения зависимости, репозитория и единицы работы, используя Castle Windsor в качестве DI-контейнера и NHibernate как инструмент объектно-реляционного отображения (ORM).
→ Скачать исходный код – 962 Кб
Внедрение зависимости – это паттерн разработки ПО, который позволяет удалять жестко запрограммированные зависимости, а также менять их во время выполнения и компиляции [1].
Репозиторий – это посредник между доменом и уровнями отображения данных, который использует специальный интерфейс, чтобы получить доступ к объектам домена [2].
Единица работы – паттерн, который используется для определения и управления транзакционными функциями вашего приложения [4].
На тему внедрения сущностей и единицы работы есть много статей, уроков и прочих ресурсов, потому я не буду давать им определение. Эта статья посвящена решению трудностей, о которых мы поговорим далее.
Трудности
Разработка управляемого данными приложения должна выполняться в соответствии с некоторыми принципами. Именно о таких принципах я и хочу поговорить.
Как открывать и закрывать соединения
Конечно, соединениями лучше всего управлять на уровне БД (в репозиториях). К примеру, можно открыть соединение, выполнить команду в базе данных и закрыть соединение в каждом вызове метода репозитория. Но такой вариант будет неэффективным, если нам нужно использовать одно соединение для разных методов в пределах того же репозитория или для разных методов в разных репозиториях (подумайте о транзакции, использующей методы разных репозиториев).
При создании сайта (с ASP.NET MVC или веб-формами) можно открыть соединение с помощью Application_BeginRequest и закрыть его с помощью Application_EndRequest. Но у этого подхода есть недостатки:
- База данных открывается и закрывается для каждого запроса, даже если не все из них используют БД. Получается, что соединение берется из пула даже в том случае, когда оно не используется.
- База данных открывается в начале запроса и закрывается в конце. Но иногда запрос бывает слишком длинным, а работа БД занимает незначительную часть его времени, что опять же ведет к неэффективному использованию пула соединений.
Описанные выше трудности могут показаться вам незначительными, но для меня они представляют большую проблему. Но что делать, если вы разрабатываете не сайт, а сервис Windows, запускающий много потоков, которые в течение какого-то времени используют базу данных? Где в таком случае открывать и закрывать соединения?
Как управлять транзакциями
Если ваше приложение (как большинство приложений) использует транзакции при работе с БД, где в таком случае вам следует начинать, фиксировать или откатывать транзакцию? Делать это в методах репозитория не представляется возможным – транзакция может включать много разных вызовов методов репозитория. Потому все эти операции может выполнять слой предметной области. Но у этого подхода тоже есть недостатки:
- Слой предметной области включает специфический для базы данных код, что нарушает принцип единственной обязанности и использование многоуровневого представления.
- Этот подход дублирует логику транзакций в каждом методе слоя предметной области.
Как я упоминал выше, управлять транзакциями можно с помощью Application_BeginRequest и Application_EndRequest. Здесь возникают те же трудности: вы запускаете или фиксируете ненужные транзакции. К тому же придется по необходимости откатывать некоторые из них после исправления ошибок.
Если вы разрабатываете не сайт, а приложение, будет непросто найти удачное место для запуска, фиксации и отката транзакций.
Поэтому лучше всего начинать транзакцию, когда вам это действительно нужно, фиксировать ее в том случае, если все ваши операции успешны, и откатывать ее только при условии, что какая-либо из ваших операций не удалась. Именно этим принципом я и буду руководствоваться дальше.
Реализация
Мое приложение представляет собой телефонную книгу, созданную с использованием ASP.NET MVC (в качестве веб-фреймворка), Sql Server (как СУБД), NHibernate (как ORM) и Castle Windsor (в качестве контейнера внедрения зависимостей).
Сущности
В моей реализации сущность преобразуется в запись таблицы в БД. Сущность в предметно-ориентированном проектировании – это сохраняемый объект с уникальным идентификатором. В данном случае все сущности являются производными от класса Entity, приведенного ниже:
public interface IEntity<TPrimaryKey>
{
TPrimaryKey Id { get; set; }
}
public class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
{
public virtual TPrimaryKey Id { get; set; }
}
У сущности есть уникальный идентификатор – первичный ключ, который может иметь разные типы (int, long, guid и т. д.). Соответственно, мы имеем дело с родовым классом, а сущности People, Phone, City и т. д. являются производными от него. Вот как выглядит определение класса People:
public class Person : Entity<int>
{
public virtual int CityId { get; set; }
public virtual string Name { get; set; }
public virtual DateTime BirthDay { get; set; }
public virtual string Notes { get; set; }
public virtual DateTime RecordDate { get; set; }
public Person()
{
Notes = "";
RecordDate = DateTime.Now;
}
}
Как видно, первичный ключ для Person определен как int.
Преобразование сущностей
Инструментам объектно-реляционного отображения, таким как фреймворк Entity и NHibernate, требуется определение преобразования сущностей в таблицы БД. Известно много способов это реализовать. Я, например, использовал NHibernate Fluent API. Нужно определить преобразующий класс для всех сущностей, как показано ниже на примере сущности People:
public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Table("People");
Id(x => x.Id).Column("PersonId");
Map(x => x.CityId);
Map(x => x.Name);
Map(x => x.BirthDay);
Map(x => x.Notes);
Map(x => x.RecordDate);
Репозитории (Уровень БД)
Репозитории используются для создания уровня БД, чтобы отделить логику доступа к данным от верхних уровней. Класс репозитория обычно создается для каждой сущности или агрегирования – группы сущностей. Я создал репозиторий для каждой сущности. Сначала я определил интерфейс, который должен быть реализован всеми классами репозитория:
/// <summary>
/// This interface must be implemented by all repositories to ensure UnitOfWork to work.
/// </summary>
public interface IRepository
{
}
/// <summary>
/// This interface is implemented by all repositories to ensure implementation of fixed methods.
/// </summary>
/// <typeparam name="TEntity">Main Entity type this repository works on</typeparam>
/// <typeparam name="TPrimaryKey">Primary key type of the entity</typeparam>
public interface IRepository<TEntity, TPrimaryKey> : IRepository where TEntity : Entity<TPrimaryKey>
{
/// <summary>
/// Used to get a IQueryable that is used to retrive entities from entire table.
/// </summary>
/// <returns>IQueryable to be used to select entities from database</returns>
IQueryable<TEntity> GetAll();
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">Primary key of the entity to get</param>
/// <returns>Entity</returns>
TEntity Get(TPrimaryKey key);
/// <summary>
/// Inserts a new entity.
/// </summary>
/// <param name="entity">Entity</param>
void Insert(TEntity entity);
/// <summary>
/// Updates an existing entity.
/// </summary>
/// <param name="entity">Entity</param>
void Update(TEntity entity);
/// <summary>
/// Deletes an entity.
/// </summary>
/// <param name="id">Id of the entity</param>
void Delete(TPrimaryKey id);
}
Таким образом, все классы репозитория должны реализовывать приведенные выше методы. Но NHibernate имеет практически аналогичную реализацию этих методов. Выходит, можно определить базовый класс для всех репозиториев, не применив ко всем из них одинаковую логику. Ниже показано определение NhRepositoryBase:
/// <summary>
/// Base class for all repositories those uses NHibernate.
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
/// <typeparam name="TPrimaryKey">Primary key type of the entity</typeparam>
public abstract class NhRepositoryBase<TEntity, TPrimaryKey> : IRepository<TEntity, TPrimaryKey> where TEntity : Entity<TPrimaryKey>
{
/// <summary>
/// Gets the NHibernate session object to perform database operations.
/// </summary>
protected ISession Session { get { return NhUnitOfWork.Current.Session; } }
/// <summary>
/// Used to get a IQueryable that is used to retrive object from entire table.
/// </summary>
/// <returns>IQueryable to be used to select entities from database</returns>
public IQueryable<TEntity> GetAll()
{
return Session.Query<TEntity>();
}
/// <summary>
/// Gets an entity.
/// </summary>
/// <param name="key">Primary key of the entity to get</param>
/// <returns>Entity</returns>
public TEntity Get(TPrimaryKey key)
{
return Session.Get<TEntity>(key);
}
/// <summary>
/// Inserts a new entity.
/// </summary>
/// <param name="entity">Entity</param>
public void Insert(TEntity entity)
{
Session.Save(entity);
}
/// <summary>
/// Updates an existing entity.
/// </summary>
/// <param name="entity">Entity</param>
public void Update(TEntity entity)
{
Session.Update(entity);
}
/// <summary>
/// Deletes an entity.
/// </summary>
/// <param name="id">Id of the entity</param>
public void Delete(TPrimaryKey id)
{
Session.Delete(Session.Load<TEntity>(id));
}
}
Свойство сессии используется для получения объекта сессии (объект соединения с базой данных в NHibernate) от NhUnitOfWork.Current.Session, который получает правильный объект Session для текущей транзакции. Поэтому не приходится решать, как открывать и закрывать соединение или транзакцию. Дальше я постараюсь подробнее остановиться на описании этого механизма.
Все операции CRUD по умолчанию реализуются для всех репозиториев. Теперь можно создать PersonRepository с возможностью выбирать, обновлять и удалять записи. Для этого нужно объявить два типа, как показано ниже.
public interface IPersonRepository : IRepository<Person, int>
{
}
public class NhPersonRepository : NhRepositoryBase<Person, int>, IPersonRepository
{
}
То же самое можно также сделать для сущностей Phone и City. Если необходимо добавить специальный метод репозитория, это можно сделать в репозиторий соответствующей сущности. Например, добавить новый метод в PhoneRepository, чтобы была возможность удалить телефоны определенного человека:
public interface IPhoneRepository : IRepository<Phone, int>
{
/// <summary>
/// Deletes all phone numbers for given person id.
/// </summary>
/// <param name="personId">Id of the person</param>
void DeletePhonesOfPerson(int personId);
}
public class NhPhoneRepository : NhRepositoryBase<Phone, int>, IPhoneRepository
{
public void DeletePhonesOfPerson(int personId)
{
var phones = GetAll().Where(phone => phone.PersonId == personId).ToList();
foreach (var phone in phones)
{
Session.Delete(phone);
}
}
}
Единица работы
Единица работы используется для определения и управления транзакционными функциями приложения. Прежде всего нужно определить интерфейс IUnitOfWork:
/// <summary>
/// Represents a transactional job.
/// </summary>
public interface IUnitOfWork
{
/// <summary>
/// Opens database connection and begins transaction.
/// </summary>
void BeginTransaction();
/// <summary>
/// Commits transaction and closes database connection.
/// </summary>
void Commit();
/// <summary>
/// Rollbacks transaction and closes database connection.
/// </summary>
void Rollback();
}
Реализация IUnitOfWork для NHibernate показана ниже:
/// <summary>
/// Implements Unit of work for NHibernate.
/// </summary>
public class NhUnitOfWork : IUnitOfWork
{
/// <summary>
/// Gets current instance of the NhUnitOfWork.
/// It gets the right instance that is related to current thread.
/// </summary>
public static NhUnitOfWork Current
{
get { return _current; }
set { _current = value; }
}
[ThreadStatic]
private static NhUnitOfWork _current;
/// <summary>
/// Gets Nhibernate session object to perform queries.
/// </summary>
public ISession Session { get; private set; }
/// <summary>
/// Reference to the session factory.
/// </summary>
private readonly ISessionFactory _sessionFactory;
/// <summary>
/// Reference to the currently running transcation.
/// </summary>
private ITransaction _transaction;
/// <summary>
/// Creates a new instance of NhUnitOfWork.
/// </summary>
/// <param name="sessionFactory"></param>
public NhUnitOfWork(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
/// <summary>
/// Opens database connection and begins transaction.
/// </summary>
public void BeginTransaction()
{
Session = _sessionFactory.OpenSession();
_transaction = Session.BeginTransaction();
}
/// <summary>
/// Commits transaction and closes database connection.
/// </summary>
public void Commit()
{
try
{
_transaction.Commit();
}
finally
{
Session.Close();
}
}
/// <summary>
/// Rollbacks transaction and closes database connection.
/// </summary>
public void Rollback()
{
try
{
_transaction.Rollback();
}
finally
{
Session.Close();
}
}
}
Статическое свойство Current является ключевым для всего класса. Оно получает и настраивает поле _current, помеченное как ThreadStatic. Таким образом, я могу использовать тот же объект единицы работы в том же потоке. Это значит, что несколько объектов могут совместно использовать одно соединение или транзакцию. Наконец, определяю атрибут, используемый для пометки метода, который должен быть транзакционным:
/// <summary>
/// This attribute is used to indicate that declaring method is transactional (atomic).
/// A method that has this attribute is intercepted, a transaction starts before call the method.
/// At the end of method call, transaction is commited if there is no exception, othervise it's rolled back.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class UnitOfWorkAttribute : Attribute
{
}
Если определенный метод должен быть транзакционным, его нужно пометить атрибутом UnitOfWork. Затем я перехвачу эти методы с помощью внедрения зависимостей, как будет показано ниже.
Уровень служб
В предметно-ориентированном проектировании службы домена применяются для реализации логики предметной области и могут использовать репозитории для выполнения задач БД. Например, вот так выглядит определение PersonService:
public class PersonService : IPersonService
{
private readonly IPersonRepository _personRepository;
private readonly IPhoneRepository _phoneRepository;
public PersonService(IPersonRepository personRepository, IPhoneRepository phoneRepository)
{
_personRepository = personRepository;
_phoneRepository = phoneRepository;
}
public void CreatePerson(Person person)
{
_personRepository.Insert(person);
}
[UnitOfWork]
public void DeletePerson(int personId)
{
_personRepository.Delete(personId);
_phoneRepository.DeletePhonesOfPerson(personId);
}
//... some other methods are not shown here since it's not needed. See source codes.
}
Обратите внимание на использование атрибута UnitOfWork, определенного выше. Метод DeletePerson помечен как UnitOfWork. Он вызывает два разных метода репозитория, и эти вызовы методов должны быть транзакционными. С другой стороны, метод CreatePerson не помечен как UnitOfWork, потому что вызывает всего один метод репозитория, Insert для репозитория person, который может управлять собственной транзакцией: открывать и закрывать ее. Дальше мы увидим, как это реализуется.
Внедрение зависимостей (DI)
DI-контейнеры, такие как Castle Windsor, используются для управления зависимостями и жизненными циклами объекта приложения. Это позволяет создавать в приложении слабо связанные компоненты и модули. В приложении на ASP.NET DI-контейнер, как правило, инициализируется в файле global.asax. Обычно это происходит при запуске.
public class MvcApplication : System.Web.HttpApplication
{
private WindsorContainer _windsorContainer;
protected void Application_Start()
{
InitializeWindsor();
// Other startup logic...
}
protected void Application_End()
{
if (_windsorContainer != null)
{
_windsorContainer.Dispose();
}
}
private void InitializeWindsor()
{
_windsorContainer = new WindsorContainer();
_windsorContainer.Install(FromAssembly.This());
ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(_windsorContainer.Kernel));
}
}
Объект WindsowContainer, который является ключевым при внедрении зависимостей, создается при запуске приложения и удаляется в конце. Для внедрения зависимостей нужно также изменить стандартные настройки фабрики контроллеров MVC в методе InitializeWindsor. Всякий раз, когда шаблону MVC на ASP.NET требуется Controller (в каждом веб-запросе), он создает его с помощью внедрения зависимостей. Больше информации о Castle Windsor можно найти здесь. Вот как выглядит фабрика контроллеров:
public class WindsorControllerFactory : DefaultControllerFactory
{
private readonly IKernel _kernel;
public WindsorControllerFactory(IKernel kernel)
{
_kernel = kernel;
}
public override void ReleaseController(IController controller)
{
_kernel.ReleaseComponent(controller);
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
throw new HttpException(404, string.Format("The controller for path '{0}' could not be found.", requestContext.HttpContext.Request.Path));
}
return (IController)_kernel.Resolve(controllerType);
}
}
Она довольно проста и понятна даже на первый взгляд. Вам следует внедрить наши собственные зависимости объектов с помощью класса PhoneBookDependencyInstaller. Castle Windsor автоматически находит этот класс благодаря реализации IWindsorInstaller. Вспомните строку _windsorContainer.Install(FromAssembly.This()); в файле global.asax.
public class PhoneBookDependencyInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
//Register all controllers
container.Register(
//Nhibernate session factory
Component.For<ISessionFactory>().UsingFactoryMethod(CreateNhSessionFactory).LifeStyle.Singleton,
//Unitofwork interceptor
Component.For<NhUnitOfWorkInterceptor>().LifeStyle.Transient,
//All repoistories
Classes.FromAssembly(Assembly.GetAssembly(typeof(NhPersonRepository))).InSameNamespaceAs<NhPersonRepository>().WithService.DefaultInterfaces().LifestyleTransient(),
//All services
Classes.FromAssembly(Assembly.GetAssembly(typeof(PersonService))).InSameNamespaceAs<PersonService>().WithService.DefaultInterfaces().LifestyleTransient(),
//All MVC controllers
Classes.FromThisAssembly().BasedOn<IController>().LifestyleTransient()
);
}
/// <summary>
/// Creates NHibernate Session Factory.
/// </summary>
/// <returns>NHibernate Session Factory</returns>
private static ISessionFactory CreateNhSessionFactory()
{
var connStr = ConfigurationManager.ConnectionStrings["PhoneBook"].ConnectionString;
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(connStr))
.Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetAssembly(typeof(PersonMap))))
.BuildSessionFactory();
}
void Kernel_ComponentRegistered(string key, Castle.MicroKernel.IHandler handler)
{
//Intercept all methods of all repositories.
if (UnitOfWorkHelper.IsRepositoryClass(handler.ComponentModel.Implementation))
{
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(NhUnitOfWorkInterceptor)));
}
//Intercept all methods of classes those have at least one method that has UnitOfWork attribute.
foreach (var method in handler.ComponentModel.Implementation.GetMethods())
{
if (UnitOfWorkHelper.HasUnitOfWorkAttribute(method))
{
handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(NhUnitOfWorkInterceptor)));
return;
}
}
}
}
Как видно, я регистрирую все компоненты, используя метод Register в Castle Windsor. Обратите внимание: все классы репозитория регистрируются одной строкой. То же нужно проделать для сервисов и контроллеров. Я использую фабричный метод для создания фабрики ISessionFactory, которая создает объекты ISession (соединение БД) для их использования с NHibernate. В начале метода Install я регистрирую событие ComponentRegistered для внедрения логики перехвата. Взгляните на Kernel_ComponentRegistered. Если метод является методом репозитория, я всегда буду использовать для него перехват. Помимо этого, если метод помечен атрибутом UnitOfWork, он также перехватывается классом NhUnitOfWorkInterceptor.
Перехват
Перехват – это специальный прием, позволяющий выполнять некоторый код в начале и в конце вызова метода. Перехват, как правило, используется для логирования, профилирования, кэширования и т.д. Он позволяет динамически внедрять код в необходимые методы, не меняя сами методы.
В нашем случае перехват пригодится для реализации единицы работы. Если определенный метод является методом репозитория или помечен как атрибут UnitOfWork (описано выше), я открываю соединение с базой данных и (Session в NHibernate) и запускаю транзакцию в начале метода. Если перехваченный метод не выдал ни одного исключения, транзакция фиксируется в конце метода. Если же метод выдает исключение, вся транзакция откатывается. Взглянем на мою реализацию класса NhUnitOfWorkInterceptor:
/// <summary>
/// This interceptor is used to manage transactions.
/// </summary>
public class NhUnitOfWorkInterceptor : IInterceptor
{
private readonly ISessionFactory _sessionFactory;
/// <summary>
/// Creates a new NhUnitOfWorkInterceptor object.
/// </summary>
/// <param name="sessionFactory">Nhibernate session factory.</param>
public NhUnitOfWorkInterceptor(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
/// <summary>
/// Intercepts a method.
/// </summary>
/// <param name="invocation">Method invocation arguments</param>
public void Intercept(IInvocation invocation)
{
//If there is a running transaction, just run the method
if (NhUnitOfWork.Current != null || !RequiresDbConnection(invocation.MethodInvocationTarget))
{
invocation.Proceed();
return;
}
try
{
NhUnitOfWork.Current = new NhUnitOfWork(_sessionFactory);
NhUnitOfWork.Current.BeginTransaction();
try
{
invocation.Proceed();
NhUnitOfWork.Current.Commit();
}
catch
{
try
{
NhUnitOfWork.Current.Rollback();
}
catch
{
}
throw;
}
}
finally
{
NhUnitOfWork.Current = null;
}
}
private static bool RequiresDbConnection(MethodInfo methodInfo)
{
if (UnitOfWorkHelper.HasUnitOfWorkAttribute(methodInfo))
{
return true;
}
if (UnitOfWorkHelper.IsRepositoryMethod(methodInfo))
{
return true;
}
return false;
}
}
Intercept – это основной метод. Сначала он проверяет, существует ли ранее запущенная транзакция для данного потока. Если да, он не запускает новую транзакцию, а использует текущую (см. NhUnitOfWork.Current). Таким образом, вложенные вызовы методов с аттрибутом UnitOfWork могут совместно использовать одну и ту же транзакцию. Транзакция создается или фиксируется только при первом использовании метода единицы работы. К тому же, если метод не является транзакционным, происходит просто вызов метода и возврат. Команда invocation.Proceed() выполняет вызов в перехваченный метод.
Если текущих транзакций нет, нужно начать новую транзакцию и зафиксировать ее при отсутствии ошибок. В противном случае следует откатить ее. После этого следует задать NhUnitOfWork.Current = null, чтобы потом при необходимости начинать другие транзакции для данного потока. Можете также взглянуть на класс UnitOfWorkHelper.
Таким образом, код для открытия и закрытия соединений, а также для запуска, фиксации и отката транзакций определяется только в одной точке приложения.
Автор: Plarium