NHibernate, транзакции и TransactionScope
Я пытаюсь найти лучшее решение для обработки транзакций в веб-приложении, которое использует NHibernate.
Мы используем IHttpModule и в HttpApplication.BeginRequest открываем новый сеанс и привязываем его к HttpContext с помощью ManagedWebSessionContext.Bind(context, session); Мы закрываем и отменяем привязку сеанса по HttpApplication.EndRequest.
В нашем базовом классе репозитория мы всегда обертываем транзакцию вокруг наших методов SaveOrUpdate, Delete, Get, в соответствии с лучшими практиками:
public virtual void Save(T entity)
{
var session = DependencyManager.Resolve<ISession>();
using (var transaction = session.BeginTransaction())
{
session.SaveOrUpdate(entity);
transaction.Commit();
}
}
Но тогда это не сработает, если вам нужно поместить транзакцию где-нибудь, например, в службу приложения, чтобы включить несколько вызовов хранилища для сохранения, удаления и т. Д.
Итак, мы попытались использовать TransactionScope (я не хотел писать свой собственный менеджер транзакций). Чтобы проверить, что это работает, я использую внешний TransactionScope, который не вызывает.Complete() для принудительного отката:
Сохранение репозитория ():
public virtual void Save(T entity)
{
using (TransactionScope scope = new TransactionScope())
{
var session = DependencyManager.Resolve<ISession>();
session.SaveOrUpdate(entity);
scope.Complete();
}
}
Блок, который использует репозиторий:
TestEntity testEntity = new TestEntity { Text = "Test1" };
ITestRepository testRepository = DependencyManager.Resolve<ITestRepository>();
testRepository.Save(testEntity);
using (var scope = new TransactionScope())
{
TestEntity entityToChange = testRepository.GetById(testEntity.Id);
entityToChange.Text = "TestChanged";
testRepository.Save(entityToChange);
}
TestEntity entityChanged = testRepository.GetById(testEntity.Id);
Assert.That(entityChanged.Text, Is.EqualTo("Test1"));
Это не работает Но для меня, если NHibernate поддерживает TransactionScope, это будет! Что происходит, так это то, что ROLLBACK вообще отсутствует в базе данных, но когда testRepository.GetById(testEntity.Id); выполняется оператор UPDATE с SET Text = "TestCahgned" вместо этого (он должен был быть запущен между BEGIN TRAN и ROLLBACK TRAN). NHibernate считывает значение из кэша уровня 1 и запускает ОБНОВЛЕНИЕ в базу данных. Не ожидаемое поведение!? Из того, что я понимаю, когда бы ни происходил откат в рамках NHibernate, вам также необходимо закрыть и отменить привязку текущего сеанса.
Мой вопрос: кто-нибудь знает хороший способ сделать это, используя TransactionScope и ManagedWebSessionContext?
4 ответа
Я выбрал очень похожий подход. В модуле HttpModule я запрашиваю сессионную фабрику для новой сессии + связываю ее, когда приходит новый запрос. Но я также начинаю транзакцию здесь. Затем, когда запрос заканчивается, я просто отвязываю его и пытаюсь совершить транзакцию.
Также мой базовый репозиторий не берет сессию в любом случае - вместо этого он запросит текущую сессию и затем выполнит некоторую работу с сессией. Также я ничего не заключаю внутри этого базового класса с транзакцией. Вместо этого весь http-запрос - это единица работы.
Это может не подходить для проекта, над которым вы работаете, но я предпочитаю такой подход, потому что каждый запрос не будет выполнен или будет выполнен как единое целое. У меня есть полный пост в блоге с исходным кодом, если вы заинтересованы в реальной реализации.
Ниже приведен пример того, как выглядит этот базовый репозиторий:
public abstract class NHibernateRepository<T> where T : class
{
protected readonly ISessionBuilder mSessionBuilder;
public NHibernateRepository()
{
mSessionBuilder = SessionBuilderFactory.CurrentBuilder;
}
public T Retrieve(int id)
{
ISession session = GetSession();
return session.Get<T>(id);
}
public void Save(T entity)
{
ISession session = GetSession();
session.SaveOrUpdate(entity);
}
public void Delete(T entity)
{
ISession session = GetSession();
session.Delete(entity);
}
public IQueryable<T> RetrieveAll()
{
ISession session = GetSession();
var query = from Item in session.Linq<T>() select Item;
return query;
}
protected virtual ISession GetSession()
{
return mSessionBuilder.CurrentSession;
}
}
Жизненный цикл транзакции должен быть:
using (TransactionScope tx = new TransactionScope())
{
using (ISession session1 = ...)
using (ITransaction tx1 = session.BeginTransaction())
{
...do work with session
tx1.Commit();
}
using (ISession session2 = ...)
using (ITransaction tx2 = session.BeginTransaction())
{
...do work with session
tx2.Commit();
}
tx.Complete();
}
Спасибо за ответ!
Да, это простой и прямой способ ее решения. Но моя проблема в том, что я хочу убедиться, что существует транзакция, связанная с операцией хранилища, даже если служба приложения, хранилище и т. Д. Не вызывается веб-запросом (другие типы клиентов), поэтому я хотел, чтобы транзакция окружала самый низкий уровень (например, session.Save), а затем используйте TransactionScope для создания более длинной транзакции, если это необходимо. Но ваше решение простое, и мне нравится, возможно, я воспользуюсь им, а затем позабочусь, чтобы другие клиенты также использовали транзакции.
Вы можете проверить, активна ли транзакция, используя: Session.Transaction.IsActive
, Если один не активен, вы можете создать его. Вы также можете создать Transact
метод, который делает большую часть этого для вас автоматически. Вот выдержка из книги поваренных книг NHibernate 3.0:
// based on NHibernate 3.0 Cookbook, Data Access Layer, pg. 192
public class GenericDataAccessObject<TId> : IGenericDataAccessObject<TId>
{
// if you don't want to new up your DAO per Unit-of-work you can
// resolve the session at the time it's accessed.
private readonly ISession session;
protected GenericDataAccessObject(ISession session)
{
this.session = session;
}
protected ISession Session { get { return session; } }
public virtual T Get<T>(TId id)
{
return Transact(() => Session.Get<T>(id));
}
protected virtual void Save<T>(T entity)
{
Transact(() => Session.Save(entity));
}
/// <summary>
/// Perform func within a transaction block, creating a new active transaction
/// when necessary. No error handling is performed as this function doesn't have
/// sufficient information to provide a useful error message.
/// </summary>
/// <typeparam name="TResult">The return type</typeparam>
/// <param name="func">The function wrapping the db operations</param>
/// <returns>The results returned by <c>func</c></returns>
protected TResult Transact<TResult>(Func<TResult> func)
{
// the null Transaction shouldn't happen in a well-behaving Session
// implementation
if (Session.Transaction == null || !Session.Transaction.IsActive)
{
TResult result;
// transaction rollback happens during dispose when necessary
using (var tx = Session.BeginTransaction())
{
result = func.Invoke();
tx.Commit();
}
return result;
// We purposefully don't catch any exceptions as if we were to catch
// the error at this point we wouldn't have enough information to describe
// to the user why it happened -- we could only describe what happened.
}
return func.Invoke();
}
protected void Transact(Action action)
{
Transact<bool>(() =>
{
action.Invoke();
return false;
}
);
}
}