Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid using duplicate savepoint names in nested transactions in nested stored procs?

I have a pattern that I almost always follow, where if I need to wrap up an operation in a transaction, I do this:

BEGIN TRANSACTION
SAVE TRANSACTION TX

-- Stuff

IF @error <> 0
    ROLLBACK TRANSACTION TX

COMMIT TRANSACTION

That's served me well enough in the past, but after years of using this pattern (and copy-pasting the above code), I've suddenly discovered a flaw which comes as a complete shock.

Quite often, I'll have a stored procedure calling other stored procedures, all of which use this same pattern. What I've discovered (to my cost) is that because I'm using the same savepoint name everywhere, I can get into a situation where my outer transaction is partially committed - precisely the opposite of the atomicity that I'm trying to achieve.

I've put together an example that exhibits the problem. This is a single batch (no nested stored procs), and so it looks a little odd in that you probably wouldn't use the same savepoint name twice in the same batch, but my real-world scenario would be too confusing to post.

CREATE TABLE Test (test INTEGER NOT NULL)

BEGIN TRAN 
SAVE TRAN TX

    BEGIN TRAN
    SAVE TRAN TX
        INSERT INTO Test(test) VALUES (1)
    COMMIT TRAN TX

    BEGIN TRAN
    SAVE TRAN TX
        INSERT INTO Test(test) VALUES (2)
    COMMIT TRAN TX

    DELETE FROM Test

ROLLBACK TRAN TX
COMMIT TRAN TX

SELECT * FROM Test

DROP TABLE Test

When I execute this, it lists one record, with value "1". In other words, even though I rolled back my outer transaction, a record was added to the table.

What's happening is that the ROLLBACK TRANSACTION TX at the outer level is rolling back as far as the last SAVE TRANSACTION TX at the inner level. Now that I write this all out, I can see the logic behind it: the server is looking back through the log file, treating it as a linear stream of transactions; it doesn't understand the nesting/hierarchy implied by either the nesting of the transactions (or, in my real-world scenario, by the calls to other stored procedures).

So, clearly, I need to start using unique savepoint names instead of blindly using "TX" everywhere. But - and this is where I finally get to the point - is there a way to do this in a copy-pastable way so that I can still use the same code everywhere? Can I auto-generate the savepoint name on the fly somehow? Is there a convention or best-practice for doing this sort of thing?

It's not exactly hard to come up with a unique name every time you start a transaction (could base it off the SP name, or somesuch), but I do worry that eventually there would be a conflict - and you wouldn't know about it because rather than causing an error it just silently destroys your data... :-(

like image 829
Gary McGill Avatar asked Apr 09 '10 10:04

Gary McGill


People also ask

What happens when a rollback happens in inside a nested stored procedure?

The idea is that the stored procedure must end with the same number of open transactions as it is entered. Hence, if there is an open transaction in the outer procedure and a ROLLBACK command is executed in the inner procedure, it causes the value of the @@TRANCOUNT variable to be set to 0.

Can we use nested stored procedure in SQL?

Nesting stored procedures and transactions present a special challenge, and SQL Server has additional rules and behavior changes to work with them. Some techniques that may work with just one stored procedure call, or one transaction level, will not work in a deeper nesting level.

Why is a nested transaction still useful?

A nested transaction is used to provide a transactional guarantee for a subset of operations performed within the scope of a larger transaction. Doing this allows you to commit and abort the subset of operations independently of the larger transaction.

What is nested stored procedure in SQL Server?

SQL query example, how to call a stored procedure inside another stored procedure in SQL Server. Here is an example of how to call a stored procedure inside another stored procedure. This is also known as nested stored procedures in SQL Server.


2 Answers

Agree with KM's solution.

I prefer to use GUIDs though to generate unique savepoint names.

DECLARE @savepoint AS VARCHAR(36)
SET @savepoint = CONVERT(VARCHAR(36), NEWID())

BEGIN TRANSACTION
SAVE TRANSACTION @savepoint
...

ROLLBACK TRANSACTION @savepoint
COMMIT TRANSACTION
like image 198
etliens Avatar answered Sep 19 '22 02:09

etliens


look at the docs: SAVE TRANSACTION (Transact-SQL)

SAVE { TRAN | TRANSACTION } { savepoint_name | @savepoint_variable }
[ ; ]

looks like you can name it based on a variable, so try making your pattern:

DECALRE @savepoint_variable varchar(1000)
SET @savepoint_variable=OBJECT_NAME(@@PROCID)+'|'+CONVERT(char(23),GETDATE(),121)

BEGIN TRANSACTION
SAVE TRANSACTION @savepoint_variable

-- Stuff

IF @error <> 0
BEGIN
    ROLLBACK TRANSACTION @savepoint_variable
END

COMMIT TRANSACTION

when called from different procedures, your @savepoint_variable will have a different local value, and your rollbacks should rollback the proper. I put in the current datetime in the save point name, because you might use recursion at some point and if this is a copy paste pattern, it is better to handle all cases.

like image 44
KM. Avatar answered Sep 19 '22 02:09

KM.