Recommendations regarding nested transactions in SQL Server - sql-server

I have some "base operation" stored procedures, like BookAVehicle and UnBookAVehicle. They are both in a transaction.
But now I need to have a somewhat more complex stored procedure: RescheduleBooking. It also needs to be transactional.
Now, from within ResceduleBooking I want to call BookAVehicle, and in this case I don't want the inner transaction to rollback.
But when I call BookAVehicle directly, I want to keep the rollback.
Any suggestion on how to do this elegantly?
I was thinking of something along the lines of having a "wrapper" stored procedure that as a parameter takes the name of a stored procedure and only contains a transaction and a call to the parameter stored procedure.
So when I call it "directly" I call:
TransactionWrapper(BookAVehicleWithoutTrans)
and when I call it from another transaction I call:
RescheduleBooking -> BookAVehicleWithoutTrans

When you do a BEGIN TRANSACTION an internal counter is incremented ##TRANCOUNT. ROLLBACK TRANSACTION will rollback all BEGIN TRANSACTIONS setting ##TRANCOUNT to 0. Doing a commit transaction will only decrement ##TRANCOUNT, it will do a full commit when ##TRANCOUNT is 1 before setting it to 0.
With that in mind, Assuming you have paired BEGIN and COMMIT TRANSACTIONS in your Book and UnBook procedures I would do the RescheduleBooking procedure something like the following which will maintain the first book even if the unbook fails...
CREATE PROCEDURE RescheduleBooking ...
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
EXEC BookAVehicle ...
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
RETURN
END CATCH;
-- If the unbook fails the booking above will still stay.
BEGIN TRY
BEGIN TRANSACTION
EXEC UnBookAVehicle ...
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
RETURN
END CATCH;
END

Related

Rolling back transactions within stored procedures in Transact-SQL

I am new to using Transact-SQL, and I have a question on how transactions within nested stored procedures would be handled.
Consider the following example, where we create an example table as follows:
CREATE TABLE EXAMPLE_TABLE
(
ID INT,
NAME VARCHAR(255)
);
Then, we create a stored procedure with no parameters. This stored procedure involves inserting values into the table from above.
CREATE PROCEDURE SP1
AS
BEGIN
BEGIN TRANSACTION
INSERT INTO EXAMPLE_TABLE (ID, NAME)
VALUES (1, 'BOB')
COMMIT TRANSACTION;
END;
And then we create a second stored procedure with one parameter that calls our first stored procedure.
CREATE PROCEDURE sp2
#EXAMPLE INT
AS
BEGIN
BEGIN TRANSACTION
EXEC SP1
IF (#EXAMPLE < 10)
ROLLBACK TRANSACTION;
ELSE
COMMIT TRANSACTION;
END;
And then we call our second stored procedure as follows:
EXEC sp2 #EXAMPLE = 5;
At the end of this execution, will the values have been added to the EXAMPLE_TABLE? Or does the rollback in the outer stored procedure mean that everything has been rolled back, and nothing committed?
Transactions are scoped, so anything within a transaction is committed/rolled back together. So a value of 5 on your #example variable would prevent records from being added to the EXAMPLE_TABLE. You can check this fiddle for a demo.
I will add that if this example is in anyway similar to actual code you'll be writing, I would suggest to just check the variable value and make a decision on whether or not to run the insert stored procedure in the first place.
The conclusion of Aaron's answer is correct, but the reasoning is a little misleading.
Transactions aren't really "scoped" in the usual way you would think of scoping. The outermost begin tran does of course begin a transaction. But any nested begin tran doesn't really do anything other than increment the ##trancount. Then, when you commit, this doesn't really commit anything unless ##trancount is 1. Only the outermost commit is a "real" commit. Finally, a rollback will rollback everything, not just the current, "most nested" transaction, returning ##trancount to 0. At that point, if you try to commit or rollback you will get an error:
begin tran
print ##trancount
begin tran
print ##trancount
rollback
print ##trancount
commit
1
2
0
Msg 3902, Level 16, State 1, Line 61
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
For this reason, as a stylistic guide when actually coding transactions, I strongly suggest not treating a begin tran as the start of a block which needs to be indented. Treat begin tran, commit and rollback as regular statements, not the start and end of blocks.
The only exception to this behaviour is when you begin a named transaction, in which case you can rollback to the start of that named transaction.

TSQL - Error when running a procedure within a transaction where the procedure includes a transaction

Within a stored procedure I have a try catch block with a transaction which will commit if control never passes to the catch otherwise will rollback if there's an error and control does pass to the catch block.
I'm running this stored procedure from within a transaction, as per the below example:
CREATE PROCEDURE sp_test
AS
BEGIN
BEGIN TRY
BEGIN TRAN
DECLARE #var INT = 1
IF #var <> 2
BEGIN
RAISERROR('error', 16,1)
END
COMMIT
END TRY
BEGIN CATCH
ROLLBACK
PRINT 'Rolled back'
END CATCH
END
BEGIN TRAN
EXEC sp_test
When I run this, I'm seeing the error
Msg 266, Level 16, State 2, Procedure sp_test, Line 0 [Batch Start Line 7]
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
But I don't understand why.
I'm starting a transaction outside of the stored procedure. I'm then going into the SP and beginning another transaction. Once control passes to the catch block, both transactions should be rolled back so transaction count should be 0.
Obviously there's a gap in my understanding here.
This behaviour is by design. Whenever a transaction is started, the ##TRANCOUNT session variable will increase one, and when a transaction is committed, it decreases one. However, when a transaction rolls back, it rolls back the all nested transactions. So in your case, the rollback in your catch will roll back the 2 nested transactions and ##TRANCOUNT will become 0. This will cause the caller throw the mismatch transaction count exception.
To avoid this issue, you can check ##TRANCOUNT in your SP and only start a new transaction when it is 0 and set a flag (a local variable) to indicate that, e.g. #new_tarn=1. Then you commit or rollback only when #new_tran=1.

Commit transaction without begin transaction

I accidentally ran into a situation that I didn't put Begin Transaction at the beginning of my stored procedure and just wrote Commit Transaction as you can see below
ALTER PROCEDURE dbo.spTest
AS
BEGIN
DECLARE #MyId INT=1
BEGIN TRY
UPDATE Test
SET
-- Id -- this column value is auto-generated
CharName = 'david'
WHERE id=4
--Just to test locking behavior
WHILE(1=1)
BEGIN
SET #MyId=2;
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
END
I expected SQL Server to give me a run time error but it didn't happen. Of course I should mention that based on my test it didn't acquire any lock on the table due to the lack of Begin Transaction but what is the point of COMMIT TRANSACTION and ROLLBACK TRANSACTION in such a condition and why didn't SQL Server raise any error?
Edit:
if i remove while block and put WaitFor Sql raise error when reaches to COMMIT TRANSACTION
ALTER PROCEDURE dbo.spTest
AS
BEGIN
UPDATE Test
SET CharName = 'david'
WHERE id=4
PRINT 'waiting for a minute '
WAITFOR DELAY '00:00:10';
COMMIT TRANSACTION
END
Now i am receiving this error
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION
what is the point of COMMIT TRANSACTION and ROLLBACK TRANSACTION in such a condition?
There is no point in this case
and why didn't SQL Server raise any error?
I don't see any code that would raise an error. It would help if you could explain where and why you think an error should be raised
With regards to whatever you're actually doing here;
If the purpose of this proc is to hold a transaction open, you'd need something more like this:
ALTER PROCEDURE dbo.spTest
AS
BEGIN
BEGIN TRANSACTION
UPDATE Test
SET CharName = 'david'
WHERE id=4
--Endless loop
WHILE(1=1)
BEGIN
PRINT 'waiting for a minute inside a transaction. Try something from another session'
WAITFOR DELAY '00:01';
END
-- Transaction will actually never be committed
-- Because this line will never be reached
-- because it's preceded by an endless loop
COMMIT TRANSACTION
END
The TRY / CATCH is a bit of a distraction. I've removed it.

When it's necessary to check ##trancount > 0 in try catch block?

Sometimes I saw the following code snippet. When is the if ##trancount > 0 necessary with begin try? Both of them? Or it's a safe way(best practice) to check it always in case it's rollback before the check?
begin tran
begin try
... just several lines of sql ...
if ##trancount > 0 commit tran
end try
begin catch
if ##trancount > 0 rollback tran
end catch
I can think of a few scenarios to consider when dealing with ##trancount:
The current transaction was called from another stored procedure which had
its own transaction
The current transaction was called by some .NET code with its own
transaction
The current transaction is the only transaction
I believe Remus Rusanu's Exception handling and nested transactions handles all these possibilities.
when u don't use ##trancount, the error message of nested transaction stored procedure does not return the exact cause of error just reurtn "The rollback transaction request has no corresponding begin transaction",otherwise it gives exact cause of error, so its easy to handle the error with proper syntax.
To answer the question - the time to do a ##trancount check is if the code in the middle could potentially have already performed the commit or rollback of the transaction you started. So if you are calling stored procedures for example - then perform the checks at the end.
Incidentally rather than doing an if ##trancount > 0 I would suggest it is better to check the ##trancount at the start of your block of code, and then see if the count has gone up by the end, in which case do the commit or rollback, depending on try/catch.
Particularly if you are in a trigger, because the ##trancount will always be 1 there, so just doing a ##trancount > 0 could cause an error.
But even if your code is just in a stored procedure, supposed it was called by another procedure that itself has an open transaction, if your code errors and rolls back, then the outer stored procedure will have its transaction rolled back also (see https://www.sqlskills.com/blogs/paul/a-sql-server-dba-myth-a-day-2630-nested-transactions-are-real/).
So
BEGIN TRAN
PRINT ##TRANCOUNT
BEGIN TRAN
PRINT ##TRANCOUNT
ROLLBACK TRAN
PRINT ##TRANCOUNT
Will print this output:
1
2
0
So basically - if the code in the middle is calling other procedures, you need to perform the IF ##TRANCOUNT check.
the reason of check is if you commit trans or rollback it when ##trancount=0 you get an exception with this error message :
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
PRINT ##TRANCOUNT
-- The BEGIN TRAN statement will increment the
-- transaction count by 1.
BEGIN TRAN
PRINT ##TRANCOUNT
BEGIN TRAN
PRINT ##TRANCOUNT
-- The COMMIT statement will decrement the transaction count by 1.
COMMIT
PRINT ##TRANCOUNT
COMMIT
PRINT ##TRANCOUNT
--Results
--0
--1
--2
--1
--0
PRINT ##TRANCOUNT
-- The BEGIN TRAN statement will increment the
-- transaction count by 1.
BEGIN TRAN
PRINT ##TRANCOUNT
BEGIN TRAN
PRINT ##TRANCOUNT
-- The ROLLBACK statement will clear the ##TRANCOUNT variable
-- to 0 because all active transactions will be rolled back.
ROLLBACK
PRINT ##TRANCOUNT
--Results
--1
--0

SAVE TRANSACTION vs BEGIN TRANSACTION (SQL Server) how to nest transactions nicely

I have a stored procedure that needs to set a save point so that it can, under certain circumstances, undo everything it did and return an error code to the caller, or accept/commit it and return success to the caller. But I need it to work whether the caller has already started a transaction or not. The doc is extremely confusing on this subject. Here is what I think will work, but I'm not certain of all the ramifications.
The thing is - this Stored Procedure (SP) is called by others. So I don't know if they've started a transaction or not... Even if I require users to start a transaction to use my SP, I still have questions about the proper use of Save Points ...
My SP will test if a transaction is in progress, and if not, start one with BEGIN TRANSACTION. If a transaction is already in progress, it will instead create a save point with SAVE TRANSACTION MySavePointName, and save the fact this is what I did.
Then if I have to roll back my changes, if I did a BEGIN TRANSACTION earlier, then I will ROLLBACK TRANSACTION. If I did the save point, then I will ROLLBACK TRANSACTION MySavePointName. This scenario seems to work great.
Here is where I get a little confused - if I want to keep the work I've done, if I started a transaction I will execute COMMIT TRANSACTION. But if I created a save point? I tried COMMIT TRANSACTION MySavePointName, but then the caller tries to commit its transaction and gets an error:
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
So I'm wondering then - a save point can be rolled back (that works: ROLLBACK TRANSACTION MySavePointName will NOT roll back the caller's transaction). But perhaps one never needs to "commit" it? It just stays there, in case you need to roll back to it, but goes away once the original transaction is committed (or rolled back)?
If there is a "better" way to "nest" a transaction, please shed some light as well. I haven't figured out how to nest with BEGIN TRANSACTION but only rollback or commit my internal transaction. Seems ROLLBACK will always roll back to the top transaction, while COMMIT simply decrements ##trancount.
I believe I've figured this all out now, so I will answer my own question...
I've even blogged my findings if you want more details at http://geekswithblogs.net/bbiales/archive/2012/03/15/how-to-nest-transactions-nicely---quotbegin-transactionquot-vs-quotsave.aspx
So my SP starts with something like this, to start a new transaction if there is none, but use a Save Point if one is already in progress:
DECLARE #startingTranCount int
SET #startingTranCount = ##TRANCOUNT
IF #startingTranCount > 0
SAVE TRANSACTION mySavePointName
ELSE
BEGIN TRANSACTION
-- …
Then, when ready to commit the changes, you only need to commit if we started the transaction ourselves:
IF #startingTranCount = 0
COMMIT TRANSACTION
And finally, to roll back just your changes so far:
-- Roll back changes...
IF #startingTranCount > 0
ROLLBACK TRANSACTION MySavePointName
ELSE
ROLLBACK TRANSACTION
Extending Brian B's answer.
This ensures the save point name is unique and uses the new TRY/CATCH/THROW features of SQL Server 2012.
DECLARE #mark CHAR(32) = replace(newid(), '-', '');
DECLARE #trans INT = ##TRANCOUNT;
IF #trans = 0
BEGIN TRANSACTION #mark;
ELSE
SAVE TRANSACTION #mark;
BEGIN TRY
-- do work here
IF #trans = 0
COMMIT TRANSACTION #mark;
END TRY
BEGIN CATCH
IF xact_state() = 1 OR (#trans = 0 AND xact_state() <> 0) ROLLBACK TRANSACTION #mark;
THROW;
END CATCH
I have used this type of transaction manager in my Stored Procedures :
CREATE PROCEDURE Ardi_Sample_Test
#InputCandidateID INT
AS
DECLARE #TranCounter INT;
SET #TranCounter = ##TRANCOUNT;
IF #TranCounter > 0
SAVE TRANSACTION ProcedureSave;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
/*
<Your Code>
*/
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION ProcedureSave;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE();
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
GO

Resources