views:

1984

answers:

3

Can I do nested transactions in NHibernate, and how do I implement them? I'm using SQL Server 2008, so support is definitely in the DBMS.

I find that if I try something like this:

using (var outerTX = UnitOfWork.Current.BeginTransaction())
{
    using (var nestedTX = UnitOfWork.Current.BeginTransaction())
    {
        ... do stuff
        nestedTX.Commit();
    }

    outerTX.Commit();
}

then by the time it comes to outerTX.Commit() the transaction has become inactive, and results in a ObjectDisposedException on the session AdoTransaction.

Are we therefore supposed to create nested NHibernate sessions instead? Or is there some other class we should use to wrap around the transactions (I've heard of TransactionScope, but I'm not sure what that is)?

I'm now using Ayende's UnitOfWork implementation (thanks Sneal).

Forgive any naivety in this question, I'm still new to NHibernate.

Thanks!

EDIT: I've discovered that you can use TransactionScope, such as:

using (var transactionScope = new TransactionScope())
{
    using (var tx = UnitOfWork.Current.BeginTransaction())
    {
        ... do stuff
        tx.Commit();
    }

    using (var tx = UnitOfWork.Current.BeginTransaction())
    {
        ... do stuff
        tx.Commit();
    }

    transactionScope.Commit();
}

However I'm not all that excited about this, as it locks us in to using SQL Server, and also I've found that if the database is remote then you have to worry about having MSDTC enabled... one more component to go wrong. Nested transactions are so useful and easy to do in SQL that I kind of assumed NHibernate would have some way of emulating the same...

+1  A: 

That implementation doesn't support nesting, if you want nesting use Ayende's UnitOfWork implementation. Another problem with the implementation your are using (at least for web apps) is that it holds onto the ISession instance in a static variable.

I just rewrote our UnitOfWork yesterday for these reasons, it was originally based off of Gabriel's.

We don't use UnitOfWork.Current.BeginTransaction(), we use UnitofWork.TransactionalFlush(), which creates a separate transaction at the very end to flush all the changes at once.

using (var uow = UnitOfWork.Start())
{
     var entity = repository.Get(1);
     entity.Name = "Sneal";
     uow.TransactionalFlush();
}
Sneal
Yeah, I feel like I should have used Rhino Commons from the beginning... I think I went with Gabriel's so I could get an idea of how the Unit Of Work pattern worked by building it from scratch (and it did help my understanding a lot actually), but maybe now it's time to play with the big boys... thanks.
Gavin Schultz-Ohkubo
@Sneal: We've implemented Ayende's UnitOfWork using Rhino.Commons, and it's good, but I'm still unclear about how to get nested transactions working. The code I described in my original question acts in exactly the same way as before (i.e. the transaction objects seem to be shared). Can you give me any pointers? Thanks.
Gavin Schultz-Ohkubo
A: 

I've been struggling with this for a while now. Am going to have another crack at it.

I want to implement transactions in individual service containers - because that makes them self-contained - but then be able to nest a bunch of those service methods within a larger transaction and rollback the whole lot if necessary.

Because I'm using Rhino Commons I'm now going to try refactoring using the With.Transaction method. Basically it allows us to write code as if transactions were nested, though in reality there is only one.

For example:

private Project CreateProject(string name)
{
    var project = new Project(name);
    With.Transaction(delegate
    {
        UnitOfWork.CurrentSession.Save(project);
    });
    return project;
}

private Sample CreateSample(Project project, string code)
{
    var sample = new Sample(project, code);
    With.Transaction(delegate
    {
        UnitOfWork.CurrentSession.Save(sample);
    });
    return sample;
}

private void Test_NoNestedTransaction()
{
  var project = CreateProject("Project 1");
}

private void TestNestedTransaction()
{
  using (var tx = UnitOfWork.Current.BeginTransaction())
  {
      try
      {
          var project = CreateProject("Project 6");
          var sample = CreateSample(project, "SAMPLE006", true);
      }
      catch
      {
          tx.Rollback();
          throw;
      }
      tx.Commit();
  }
}

In Test_NoNestedTransaction(), we are creating a project alone, without the context of a larger transaction. In this case, in CreateSample a new transaction will be created and committed, or rolled back if an exception occurs.

In Test_NestedTransaction(), we are creating both a sample and a project. If anything goes wrong, we want both to be rolled back. In reality, the code in CreateSample and CreateProject will run just as if there were no transactions at all; it is entirely the outer transaction that decides whether to rollback or commit, and does so based on whether an exception is thrown. Really that's why I'm using a manually created transaction for the outer transaction; so we I have control over whether to commit or rollback, rather than just defaulting to on-exception-rollback-else-commit.

You could achieve the same thing without Rhino.Commons by putting a whole lot of this sort of thing through your code:

if (!UnitOfWork.Current.IsInActiveTransaction)
{
  tx = UnitOfWork.Current.BeginTransaction();
}

_auditRepository.SaveNew(auditEvent);
if (tx != null)
{
  tx.Commit();
}

... and so on. But With.Transaction, despite the clunkiness of needing to create anonymous delegates, does that quite conveniently.

An advantage of this approach over using TransactionScopes (apart from the reliance on MSDTC) is that there ought to be just a single flush to the database in the final outer-transaction commit, regardless of how many methods have been called in-between. In other words, we don't need to write uncommitted data to the database as we go, we're always just writing it to the local NHibernate cache.

In short, this solution doesn't offer ultimate control over your transactions, because it doesn't ever use more than one transaction. I guess I can accept that, since nested transactions are by no means universally supported in every DBMS anyway. But now perhaps I can at least write code without worrying about whether we're already in a transaction or not.

Gavin Schultz-Ohkubo
+1  A: 

NHibernate sessions don't support nested transactions.

The following test is always true in version 2.1.2:

var session = sessionFactory.Open();
var tx1 = session.BeginTransaction();
var tx2  = session.BeginTransaction();
Assert.AreEqual(tx1, tx2);

You need to wrap it in a TransactionScope to support nested transactions.

Sathish Naga