Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TransactionScope and Transactions

In my C# code I am using TransactionScope because I was told not to rely that my sql programmers will always use transactions and we are responsible and yada yada.

Having said that

It looks like TransactionScope object Rolls back before the SqlTransaction? Is that possible and if so what is the correct methodology for wrapping a TransactionScope in a transaction.

Here is the sql test

CREATE PROC ThrowError
AS

BEGIN TRANSACTION --SqlTransaction
SELECT 1/0

IF @@ERROR<> 0
BEGIN
  ROLLBACK TRANSACTION --SqlTransaction
  RETURN -1 
END
ELSE
BEGIN
  COMMIT TRANSACTION --SqlTransaction
  RETURN 0
END

go

DECLARE @RESULT INT

EXEC @RESULT = ThrowError

SELECT @RESULT

And if I run this I get just the divide by 0 and return -1

Call from the C# code I get an extra error message

Divide by zero error encountered.
Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION tatement is missing. Previous count = 1, current count = 0.

If I give the sql transaction a name then

Cannot roll back SqlTransaction. No transaction or savepoint of that name was found. Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. Previous count = 1, current count = 2.

some times it seems the count goes up, until the app completely exits

The c# is just

        using (TransactionScope scope = new TransactionScope())
        {
             ... Execute Sql 

             scope.Commit()
         }

EDIT:

The sql code has to work for 2000 and 2005

like image 546
Mike Avatar asked Apr 23 '10 20:04

Mike


People also ask

What is TransactionScope?

The TransactionScope class provides a simple way to mark a block of code as participating in a transaction, without requiring you to interact with the transaction itself. A transaction scope can select and manage the ambient transaction automatically.

How does Entity Framework handle transactions?

In all versions of Entity Framework, whenever you execute SaveChanges() to insert, update or delete on the database the framework will wrap that operation in a transaction. This transaction lasts only long enough to execute the operation and then completes.

What is an ambient transaction?

Ambient TransactionA transaction which automatically identifies a code block that needs to support a transaction without explicitly mentioning any transaction related things. An ambient transaction is not tied just to a database, any transaction aware provider can be used.

What are transactions in SQL?

A transaction is a logical unit of work that contains one or more SQL statements. A transaction is an atomic unit. The effects of all the SQL statements in a transaction can be either all committed (applied to the database) or all rolled back (undone from the database).


3 Answers

There was a massive upgrade to the error handling within SQL Server 2005. These articles are fairly extensive: Error Handling in SQL 2005 and Later by Erland Sommarskog and Error Handling in SQL 2000 – a Background by Erland Sommarskog

The best way is something like this:

Create your stored procedure like:

CREATE PROCEDURE YourProcedure
AS
BEGIN TRY
    BEGIN TRANSACTION --SqlTransaction
    DECLARE @ReturnValue int
    SET @ReturnValue=NULL

    IF (DAY(GETDATE())=1 --logical error
    BEGIN
        SET @ReturnValue=5
        RAISERROR('Error, first day of the month!',16,1) --send control to the BEGIN CATCH block
    END

    SELECT 1/0  --actual hard error

    COMMIT TRANSACTION --SqlTransaction
    RETURN 0

END TRY
BEGIN CATCH
    IF XACT_STATE()!=0
    BEGIN
        ROLLBACK TRANSACTION --only rollback if a transaction is in progress
    END

    --will echo back the complete original error message to the caller
    --comment out if not needed
    DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int

    SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
    RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)

    RETURN ISNULL(@ReturnValue,1)

END CATCH

GO

however that is only for SQL Server 2005 and up. Without using the TRY-CATCH blocks in SQL Server 2005, you have a very difficult time removing all of the messages that SQL Server sends back. The extra messages you refer to are caused by the nature of how rollbacks are handled using @@trancount:

from http://www.sommarskog.se/error-handling-I.html#trancount

@@trancount is a global variable which reflects the level of nested transactions. Each BEGIN TRANSACTION increases @@trancount by 1, and each COMMIT TRANSACTION decreases @@trancount by 1. Nothing is actually committed until @@trancount reaches 0. ROLLBACK TRANSACTION rolls back everything to the outermost BEGIN TRANSACTION (unless you have used the fairly exotic SAVE TRANSACTION), and forces @@trancount to 0, regards of the previous value.

When you exit a stored procedure, if @@trancount does not have the same value as it had when the procedure commenced execution, SQL Server raises error 266. This error is not raised, though, if the procedure is called from a trigger, directly or indirectly. Neither is it raised if you are running with SET IMPLICIT TRANSACTIONS ON

If you don't want to get the warning about the transaction count not matching, you need to only have one transaction open at any one time. You do this by creating all of your procedure like this:

CREATE PROC YourProcedure
AS
DECLARE @SelfTransaction char(1)
SET @SelfTransaction='N'

IF @@trancount=0
BEGIN
    SET @SelfTransaction='Y'
    BEGIN TRANSACTION --SqlTransaction
END

SELECT 1/0

IF @@ERROR<> 0
BEGIN
    IF @SelfTransaction='Y'
    BEGIN
        ROLLBACK TRANSACTION --SqlTransaction
    END
    RETURN -1 
END
ELSE
BEGIN
    IF @SelfTransaction='Y'
    BEGIN
        COMMIT TRANSACTION --SqlTransaction
    END
    RETURN 0
END

GO

By doing this, you only issue the transaction commands if you are not already in a transaction. If you code all of your procedures this way, only the procedure or the C# code that issues the BEGIN TRANSACTION will actually issue the COMMIT/ROLLBACK and the transaction counts will always match (you won't get an error).

in C# from TransactionScope Class Documentation:

static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the 
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.   
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }
    catch (ApplicationException ex)
    {
        writer.WriteLine("ApplicationException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}

Just a thought, but you might be able to use the TransactionAbortedException catch to get the actual error and ignore the transaction count mismatch warning.

like image 186
KM. Avatar answered Sep 23 '22 15:09

KM.


Don't use transactions in both your C# code and the sprocs. One is enough. Which almost always should be your C# code, only it knows what set of updates to the dbase should be rejected or committed in whole.

like image 30
Hans Passant Avatar answered Sep 23 '22 15:09

Hans Passant


If you have to support SQL Server 2000, use TransactionScope to make your life easier. However, see at the bottom for why it has limitations.

SQL error handling before TRY/CATCH is erractic. Erland's article posted by KM explains the statement/scope/batch aborting errors that make it so. Basically, the code may just stop executing and you are left with locks on rows etc.

This is what happens above so your rollback does not run so you get error 226 about transaction counts.

If you support only SQL Server 2005+, then use TRY/CATCH which catches all errors and also use SET XACT_ABORT ON. TRY/CATCH makes SQL Server far more resilient and traps all runtime errors. SET XACT_ABORT ON also suppresses error 226 because it issues rollback automatically and ensures all locks are released.

BTW:

SELECT 1/0 is an excellent example of why you should use SQL error handling.

Use a DataAdapter to fill

  • a Datatable from a stored proc with SELECT 1/0 -> no error trapped
  • a DataSet from a stored proc with SELECT 1/0 -> error trapped

SQL TRY/CATCH will deal with this...

like image 28
gbn Avatar answered Sep 20 '22 15:09

gbn