2

I have an issue working with rollback and transactions.

The set up is like this: I have 1 main stored procedure and 1 inner loop stored procedure. Both have their own begin and rollback transaction. Loop begin transaction can't be changed as its part of the existing application but the outer main one I have added now which is causing the problem.

I have reproduced the problem on a smaller application.

I have 2 rows of data:

enter image description here

Loop Logic:

What the loop does is goes through each record and checks if the cash is >= 5 then it will update the status on the table to 2 and commits it.

If the cash is not >= 5 then I will raise an error which jumps to begin catch and rollback the transaction. It will update the status to 3.

Problem:

If I run records which only have cash >= 5 meaning no rollbacks occur then all accounts are updated successfully to 2.

However, when I run for example the data shown above (row with cash < 5). The first record is indeed set to 2 but when the second row is processed, the first record resets back to the previous status 1 which does not make sense. 2nd record does correctly update status to 3 and rollbacks any changes but this should be a separate transaction from the first row. I have no idea why they are connected.

Also if I remove the first begin transaction from the main stored procedure then it will run fine however, I need a begin transaction for the main process so if anything fails in the main stored procedure, I can rollback everything. Any idea why the records are rolling back on loop stored procedure even though it supposed to commit it before moving to next row?

Main stored procedure:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE sp_runMain
    @pBulkID INT
AS
BEGIN
    DECLARE @errorMessage AS VARCHAR(255) = ''
    DECLARE @error AS INT = 0

    BEGIN TRY
            BEGIN TRANSACTION

            exec spRun_Process @pBulkID
            if(@@TRANCOUNT > 0)
            COMMIT TRANSACTION
        END TRY
        BEGIN CATCH
            set @error = @@ERROR 
            set @errorMessage= ERROR_MESSAGE()
            ROLLBACK TRANSACTION

        END CATCH

    END
    GO

Inner loop stored procedure:

    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO

    ALTER PROCEDURE spRun_Process
        @pBulkID INT
    AS
    BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    Declare @totalCount As int = 0
    Declare @rowCount As int = 1

    Declare @processID as int = 0
    Declare @name as varchar(255) = ''
    Declare @cash as int = 0


    Declare @processTable as table(
    rowid int identity(1,1),
    processID int ,
    name varchar(255) ,
    cash int ,
    status int
    )

    INSERT INTO @processTable
    SELECT Process_ID, Name , Cash , Status FROM dbo.process WHERE BulkID = @pBulkID

    SELECT @totalCount = count(*) FROM dbo.Process WHERE BulkID = @pBulkID

    WHILE @rowCount <= @totalCount
    BEGIN
        BEGIN TRY
            BEGIN TRANSACTION

            SELECT @processID = pt.processID,
                    @name = pt.name ,
                    @cash = pt.cash
            FROM @processTable AS pt
            WHERE rowid = @rowCount

            if @cash >= 5
            BEGIN
                PRINT 'WORKS!'
            END
            ELSE
            BEGIN
                RAISERROR ('cash less than 5', 16, 1)
            END

            UPDATE dbo.Process set status = 2 where Process_ID = @processID and BulkID = @pBulkID

            COMMIT TRANSACTION
        END TRY
        BEGIN CATCH
            ROLLBACK TRANSACTION
            UPDATE dbo.Process set status = 3 where Process_ID = @processID and BulkID = @pBulkID
        END CATCH

        SET @rowCount = @rowCount + 1
     END 
    END
    GO

Script to create data table:

    SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Process](
    [Process_ID] [int] IDENTITY(1,1) NOT NULL,
    [Name] [varchar](255) NULL,
    [Cash] [int] NULL,
    [Status] [int] NULL,
    [BulkID] [int] NULL,
 CONSTRAINT [PK_Process] PRIMARY KEY CLUSTERED 
(
    [Process_ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Maz
  • 59
  • 9
  • 1
    The bigger question for me is why are you using loops here? This looks like this could be done in a single update statement. And be careful with the sp_ prefix. You should chose a different prefix, or even none at all. http://sqlperformance.com/2012/10/t-sql-queries/sp_prefix – Sean Lange Mar 14 '18 at 21:28
  • 2
    Also, you have what appears to be nested transactions. This seems logical but it does NOT do what it seems like it should. Nested transactions are a myth, just like the unicorn. When you execute a commit or rollback inside a "nested" transaction the outer transaction is also affected. This article explains this in detail. https://www.sqlskills.com/blogs/paul/a-sql-server-dba-myth-a-day-2630-nested-transactions-are-real/ – Sean Lange Mar 14 '18 at 21:30
  • I agree an update statement would be easier in the example I made above however this isn't the full code. This is just simplified version of the problem I am getting. – Maz Mar 14 '18 at 21:31
  • 1
    `@@TRANCOUNT` will always return 0 and not an `ERROR`, `@@ROWCOUNT` can't be used after a stored proc. – RoMEoMusTDiE Mar 14 '18 at 21:32
  • @Maz - there are a few logic errors here (maStaShuFu is correct and the nested transactions issue Sean points out)...you appear to be trying to port linear logic into the set-based SQL language and it's not going to function well (general rule is if you are looping in SQL, you aren't using SQL correctly). I think this error set you are getting can be summed up as you are not using SQL as intended and you will want to rethink your design – Twelfth Mar 14 '18 at 21:42
  • Is this purely a learning exercise or is it production code? – MJH Mar 14 '18 at 21:48
  • @twelfth I do agree the design is not great. The loop store procedure is the current existing project of the application. I couldn't change this as it's part of something big and I don't think I will be allowed to. What I was introducing was other processes I can run in the main store procedure. That's why I have introduced the outer transaction so if any of the other processes fail, I can rollback everything. – Maz Mar 14 '18 at 21:53
  • @maz - i find myself in that position frequently enough to know what you are going through. Nested transactions are just so messy and looping through them is painful at the best of times. I will see what i can find...a more manual process might work (store the roll back info in a table), but ill try to find something more optimal for you. – Twelfth Mar 14 '18 at 22:31
  • @Twelfth Nested loops are awful. Just learning that now. It has been working most of the time. Only time it fails if it rollbacks in the loop. I need to find a way to track all the changes(like a table you mentioned) I make in the loop so if it hits the catch then I need to just reverse the data instead of a rollback. Only problem is, its actually a big loop and then there's some inner loops inside this big loop which contain catches too =( . It's definitely not the way I would have done it. I would not use loops at all as they decrease performance. – Maz Mar 14 '18 at 22:59
  • The issue you have is your transactions. They would need to be removed from the inner procedure. And of course those nested loops are going to cripple your server. – Sean Lange Mar 15 '18 at 13:17

1 Answers1

1

I think you will want a table to emulate the 'rollback' process you are looking for. Create a table that stores 'proposed changes' that has all the columns that you will need to make the updates you are looking for. Insert to this table throughout the loop. At the very end of the giant procedure (and outside of the first commit), go through and make the changes to the data (add rollback/commits here if desired, though I'd write this last proc as set based queries to do all the changes at once instead of a loop...use a script to detect all records that would cause the need for a rollback and have a column in the proposed changes that records that it will cause a rollback).

As an added benefit, if you put a 'date entered' and 'date processed' column to the 'proposed changes' table, it makes a decent audit/tracking log (records with a 'date processed' entry are archived, records with that as null are awaiting processing).

Twelfth
  • 7,070
  • 3
  • 26
  • 34