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;
                           }
            );
    }
}
Другие вопросы по тегам