Click here to Skip to main content
15,886,919 members
Articles / Programming Languages / T-SQL
Tip/Trick

Template for Managing Microsoft T-SQL Nested Transactions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
24 Jun 2015CPOL 12K   1   1
A proposed template for writing nested stored procedures that perform updates (inserts, updates, deletes)

Introduction

The code below has been tested using Microsoft T-SQL 2008R2.  It avoid the problem of transactions:

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

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

Background

My client started with the design of a database that was not intended for online transaction procession (OLTP) only to find that it has evolved into one. As such, we needed a way to alter existing stored procedures that alter data. Any procedure could be executed on its own, or it could be called n-levels deep from other procedures that also alter date.

Using the Code

The code uses the @@TRANCOUNT to determine if a transaction has been started. The existing logic was placed in the middle of the template.

SQL
//
// 

--  USE YourDatabaseName;

IF NOT EXISTS (SELECT 'Common'
    FROM information_schema.schemata 
    WHERE schema_name = 'Common' )
BEGIN
    EXEC sp_executesql N'CREATE SCHEMA Common;';
END

IF OBJECT_ID('[Common].[SetConfiguration]') IS NOT NULL -- Check if SP Exists
 DROP PROCEDURE [Common].[SetConfiguration]
GO
CREATE PROCEDURE [Common].[SetConfiguration] AS BEGIN SET LOCK_TIMEOUT 500; END  -- 1,000 Milliseconds = 1 second
go

--  This is the template for all stored procedures that modify data (UPDATE/DELETE/INSERT):

CREATE PROCEDURE <INSERT Procedure Name> 
AS
/*
  3/2015  bje  Added the transaction template.
*/
EXEC [Common].[SetConfiguration];
SET NOCOUNT ON;
DECLARE @TranStarted BIT = 0;
BEGIN TRY

    IF (@@TRANCOUNT = 0)        -- if no 'BEGIN TRAN" is in effect, start one
    BEGIN
        BEGIN TRANSACTION;
        SET @TranStarted = 1;    --  This is the SP that began the TRANsaction, 
    END;

    /*  add code that will modify db here */


    -- At END of code, if no error & if this code started the transaction, do commit 
    IF (@TranStarted = 1)        -- this is the "outermost" PROCEDURE
    BEGIN
        COMMIT TRANSACTION;
    END;
END TRY

BEGIN CATCH

DECLARE @ErrorNumber    INT, 
        @ErrorMessage   nVARCHAR(4000), 
        @ErrorSeverity  INT, 
        @ErrorState     INT,
        @ErrorProc      nVARCHAR(4000) = ISNULL(ERROR_PROCEDURE(),OBJECT_NAME(@@PROCID));

    SELECT    @ErrorNumber    = ISNULL(ERROR_NUMBER(), 50000),
            @ErrorMessage    = ISNULL(ERROR_MESSAGE(), 'Error has been generated in ' + @ErrorProc), 
            @ErrorSeverity    = ISNULL(ERROR_SEVERITY(), 16), 
            @ErrorState        = ISNULL(ERROR_STATE(), 1);

    IF ((@@TRANCOUNT > 0) AND (@TranStarted > 0))
    BEGIN
        ROLLBACK TRANSACTION;
    END

    EXEC [Common].[LogErrorMessage]
        @ErrorNumber    = @ErrorNumber,
        @ErrorMessage    = @ErrorMessage,
        @ErrorSeverity    = @ErrorSeverity,
        @ErrorState        = @ErrorState,
        @ErrorProc        = @ErrorProc;

    IF ((@ErrorNumber < 13000) OR (@ErrorNumber = 50000))
        RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState) WITH LOG;
    ELSE
        RAISERROR(@ErrorNumber, @ErrorMessage, @ErrorSeverity, @ErrorState) WITH SETERROR, LOG;

END CATCH
go

If there is a problem with deadlocks, you can use:

SQL
IF OBJECT_ID('[Common].[MaxRetries]') IS NULL -- Check if SP Exists
 EXEC('CREATE FUNCTION [Common].[MaxRetries]( )  RETURNS int AS BEGIN RETURN(3); END') 
GO
ALTER FUNCTION [Common].[MaxRetries]( )  RETURNS int AS BEGIN RETURN(3); END
go

IF OBJECT_ID('[Common].[WaitForDelay]') IS NULL -- Check if SP Exists
 EXEC('CREATE PROCEDURE [Common].[WaitForDelay] AS BEGIN WAITFOR DELAY _
	''00:00:00.250''; END') -- 250 milliseconds = 1/4 second
GO
ALTER PROCEDURE [Common].[WaitForDelay] AS BEGIN WAITFOR DELAY '00:00:00.250'; END
GO


IF OBJECT_ID('[Common].[SetConfiguration]') IS NOT NULL -- Check if SP Exists
 DROP PROCEDURE [Common].[SetConfiguration]
GO
CREATE PROCEDURE [Common].[SetConfiguration] AS BEGIN SET LOCK_TIMEOUT 500; _
END  -- 1,000 Milliseconds = 1 second
go

IF OBJECT_ID('[Security].[UserUpdateOnAuthentication]') IS NULL -- Check if SP Exists
 EXEC('CREATE PROCEDURE [Security].[UserUpdateOnAuthentication] _
	AS SET NOCOUNT ON;') -- Create dummy/empty SP
GO

------------------------------

CREATE PROCEDURE <Insert procedure name>
    DECLARE @TranStarted BIT = 0;
    DECLARE @Error INT = 0;
    DECLARE @Retries INT = 0;
    DECLARE @WaitForDelay int = 250;
    DECLARE @MaxRetries int = [Common].[MaxRetries]( );

EXEC [Common].[SetConfiguration];
WHILE (@Retries < @MaxRetries)
BEGIN
BEGIN TRY
IF (@@TRANCOUNT = 0)            -- IF no 'BEGIN TRAN" is in effect, start one
    BEGIN
        BEGIN TRAN;
        SAVE TRAN TRAN_UserUpdateOnLogin;
        SET @TranStarted = 1;        -- This is the SP that began the TRANsaction, 
    END

< insert sp code here>

    IF (@TranStarted = 1)        -- this is the "outermost" PROCEDURE
        COMMIT TRAN TRAN_UserUpdateOnLogin;
     BREAK;
END TRY
BEGIN CATCH
    SET @Error = ERROR_NUMBER( );
    IF (@TranStarted = 1)
    BEGIN
        ROLLBACK TRAN TRAN_UserUpdateOnLogin;
    END

    IF ((@Error = 1205) or (@Error = 1222))
    BEGIN
        SET @Retries = @Retries + 1;
        EXEC XP_LOGEVENT 9999, '<Insert Procedure Name>: DEADLOCK', WARNING;
        IF (@Retries < @MaxRetries)
        BEGIN
            EXEC [Common].[WaitForDelay];    -- 500 milliseconds = 0.5 seconds
            CONTINUE;    --    branches to the outhermost WHILE loop to try again
        END
    END

    DECLARE @Error_No       INT, 
            @Error_Message  nVARCHAR(4000), 
            @Error_Severity INT, 
            @Error_State    INT ;

    SELECT    @Error_No        = ISNULL(ERROR_NUMBER(), 50000),
            @Error_Message    = ISNULL(ERROR_MESSAGE(), 'Error has been generated.'), 
            @Error_Severity = ISNULL(ERROR_SEVERITY(), 16), 
            @Error_State    = ISNULL(ERROR_STATE(), 1) ;

    IF ((@@TRANCOUNT > 0) AND (@TranStarted > 0)) BEGIN ROLLBACK; END
    RAISERROR(@Error_Message, @Error_Severity, @Error_State) WITH LOG;                       
    BREAK;
    --THROW ;
END CATCH
END    -- END WHILE LOOP
go

Hope this helps.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralWorth Sharing Pin
Tahir Alvi24-Jun-15 20:57
Tahir Alvi24-Jun-15 20:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.