Nested transactions and NHibernate

by

The short answer for the curious: NHibernate does not support nested transactions.

Then again, nested transactions in SQL Server are not as great as I remember them either, so you should be asking yourself if you need them after all.

SQL Server and nested transactions

In my mind, nested transactions in SQL worked something like this:

CREATE PROCEDURE #createFoo (@error int output)
AS
BEGIN TRAN createFoo
-- Create the foo...
SELECT @error = @@ERROR
IF @error = 0
 COMMIT TRAN createFoo
ELSE
 ROLLBACK
GO

CREATE PROCEDURE #updateFoo (@error int output)
AS
BEGIN TRAN updateFoo
-- Update the foo...
SELECT @error = @@ERROR
IF @error = 0
 COMMIT TRAN updateFoo
ELSE
 ROLLBACK
GO

BEGIN TRAN
DECLARE @err1 int, @err2 int
EXEC #createFoo @err1 output
EXEC #updateFoo @err2 output
IF @err1 = 0 AND @err2 = 0
 COMMIT
ELSE
 ROLLBACK

Here, we can call #createFoo or #updateFoo alone or, as above, wrap them into a larger outer transaction. But in reality, they’re too painful to use like that. Why? Because in the background, or at least for all intents, there’s only ever one transaction.

For example, if you rollback an inner transaction in one of the stored procs above, you’ll get the error:

Msg 266, Level 16, State 2, Procedure #updateFoo, Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.

What’s more, the outer transaction can neither COMMIT nor ROLLBACK, because after one rollback, the whole lot are gone:

Msg 3902, Level 16, State 1, Line 8
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.

It bears repeating: there is only ever one transaction. COMMITs in inner transactions will basically be ignored; a ROLLBACK at any point will rollback all transactions to that point (or, as I’ve said, the one transaction), throwing any code outside of that procedure into total disarray.

So, I am no longer a fan of nested transactions. I don’t know why I ever liked them; perhaps I was using them wrong, or they never rolled back (the one case where it all goes swimmingly; but then, if you’re not prepared for a rollback, why are you doing transactions?)

NHibernate and nested transactions

NHibernate also technically “allows” nested transactions, but they are just as annoying as they are in SQL Server. Consider this code:

using (var outerTX = UnitOfWork.Current.BeginTransaction())
{
  // Create foo.
  using (var nestedTX = UnitOfWork.Current.BeginTransaction())
  {
    // Update foo.
    nestedTX.Commit();
  }
  outerTX.Commit();
}

In this case too, there is only ever one transaction. However, functionally this is even more limited than SQL Server. SQL Server maintains an internal transaction count (in @@TRANCOUNT) so that nested transactions are at least simulated to some degree, but NHibernate does not. This means that the one transaction is destroyed on both Commit() and Rollback(), meaning that the outer transaction Commit() will always fail.

While this limitation cannot really be gotten around, there are at least three solutions that may be acceptable.

Solution 1: TransactionScope()

If you want your NHibernate actions to be performed more directly within a SQL Server transaction, you can use TransactionScope. For example:

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();
}

I’m quite unconvinced about this solution however. For one, I’m not sure if TransactionScopes can themselves be nested, or what effect this has. And does a Rollback() just cause the same issues as it does in normal SQL Server statements? I can’t recall. This solution allows us to nest NHibernate transactions within a SQL Server transaction, but to what end?

In addition, this is basically SQL Server specific, which limits the wonderful range of DBMS that NHibernate can connect to. You also have to enable MSDTC to use TransactionScope, which is a hassle in itself, and even on a local server it seems to too easily want to promote itself to a distributed transaction, which has performance costs, and so on and so on… I did not want to get involved.

Solution 2: With.Transaction()

If you’re using Rhino Commons this is automatically available, though it would be quite easy to make your own version. A usage example is shown below:

With.Transaction(delegate
{
  // Create foo.
  With.Transaction(delegate
  {
    // Update foo. Throw an exception to rollback this transaction and the outer one.
  });
});

The original source is here, but basically it works with the one transaction limitation by:

  • doing nothing if you’re already in a transaction, or
  • creating a new transaction if you’re not. Also,
    • if a new transaction is created, any exception will cause the transaction to be rolled back.
    • if an exception is not thrown, the transaction is committed after the delegate has completed.

So although we have no direct control over the commit/rollback over the transaction, it generally does exactly what we want it to do. It allows us to write functions which effectively wrap their NHibernate calls in transactions, but we don’t need to explicitly worry about whether we’re part of a larger transaction at that point or not.

Generally, I still like to use explicit transactions in the outer transaction, as that’s where we’ll want to control whether to commit or rollback the whole lot.

Solution 3: Create a new session

A new NHibernate session isn’t really nested transactions at all, but it does allow for multiple transactions. That’s because the one transaction limit is actually one transaction per session. Therefore, creating a new session will allow you to always create a new transaction without fear that the Rollback() will harm any other transactions you’ve created.

In this case, of course, the new transaction is in no way a part of any “outer” transaction you’re running, so it is committed or rolled back entirely independently. Depending on how your code is structured, this solution may be sufficient if all you’re looking for is a way to couple together nested functions which each need to create transactions.

Again, using Rhino Commons this is most easily achieved with a new UnitOfWork:

using (UnitOfWork.Start())
{
  // Create foo.
  using (UnitOfWork.Start(UnitOfWorkNestingOptions.CreateNewOrNestUnitOfWork))
  {
    // Update foo. Note that even if you rollback here, the outer transaction will still commit
    // and the foo will be created.
  }
}

Hope that was helpful!

- Gavin

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.