Rolling back transactions within stored procedures in Transact-SQL - sql-server

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.

Related

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.

Recommendations regarding nested transactions in 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

Relationship between Transactions in Nested Stored Procedures?

I put transactions in all my "set" procedures. No problems. Everything works.
In this case, I need one set procedure, to call another, thankfully, only once, or that would potentially complicate things further.
So the happy bath would be.
I'm in ProcA and start a transaction.
It calls ProcB and it starts a transaction.
ProcB is successful and commits.
ProcA is successful and commits.
However, what happens if ProcB fails, rollsback, and rethrows the error. It should cause ProcA to rollback as well correct?
What if ProcB succeeds, commits, then ProcA subsequently fails, and rollsback...will what happened in ProcB be rolled back? or is it commited?
I need these two to work together, either both succeed, or fail and both be rolled back. What's the best way to ensure this happens?
I'm working with Microsoft SQL Server 2008 R2 (SP1)
Note: If ProcB requires a transaction because it can be called without ProcA wrapping it. And technically, ProcA won't always call ProcB (depends on input).
Here's a simple demo to show what happens with nested transations:
CREATE TABLE TranTest (Field1 INTEGER)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 1 open transaction
INSERT TranTest VALUES (1)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2 open transactions
INSERT TranTest VALUES (2)
ROLLBACK TRANSACTION -- this rolls back ALL transaction
SELECT ##TRANCOUNT -- 0 open transactions (you may have expected 1?)
SELECT * FROM TranTest -- No rows
Instead f the ROLLBACK above, if you did a COMMIT TRANSACTION, this actual does nothing other then decrement ##TRANCOUNT. So you then would need to to either COMMIT the outer transaction (which would COMMIT both rows to the table), or do a ROLLBACK which would result in no rows being committed to the table.
Here's the MSDN ref on nested transactions: http://msdn.microsoft.com/en-us/library/ms189336.aspx
Just use XACT_ABORT ON, and you are all set. Run the following script and see for yourself:
CREATE DATABASE ak_test;
GO
USE ak_test;
GO
CREATE TABLE dbo.a(i INT CONSTRAINT a_CannotInsertNegative CHECK(i>=0));
GO
CREATE TABLE dbo.b(i INT CONSTRAINT b_CannotInsertNegative CHECK(i>=0));
GO
CREATE PROCEDURE dbo.innerProc #i INT
AS
SET XACT_ABORT ON ;
BEGIN TRAN
INSERT b(i)VALUES(#i);
COMMIT;
GO
CREATE PROCEDURE dbo.outerProc #i1 INT, #i2 INT, #i3 INT
AS
SET XACT_ABORT ON ;
BEGIN TRAN
INSERT a(i)VALUES(#i1);
EXEC innerProc #i=#i2;
INSERT a(i)VALUES(#i3);
COMMIT;
GO
-- succeeds
EXEC dbo.outerProc 1, 2, 3;
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
GO
-- inner proc fails
EXEC dbo.outerProc 2, -3, 4;
GO
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
GO
-- second insert in outer proc fails
EXEC dbo.outerProc 3, 4, -5;
GO
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
I'm paranoid about transactions (there was this transaction left open on Production once that no one noticed for half an hour...) so I'd warp the potentially inner transaction like so:
CREATE PROCEDURE etcetc
...
DECLARE #IsTransaction bit = 0
IF ##trancount > 0
BEGIN
BEGIN TRANSACTION
SET #IsTransaction = 1
END
...
IF #IsTransaction = 1
BEGIN
COMMIT
-- or ROLLBACk, as necessary
END
All transaction processing (and handling of errors that occur within the transaction) must then be dealt with at whatever level launched the transaction.
(And did anyone else notice how BOL doesn't actually say what happens when you issue a ROLLBACK to a named transaction that isn't the outermost transaction? They do spell out every other permutation...)

The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION

Here's a strange problem I'm running into on a production server. It has happened twice in the last two weeks, and this is a server that gets a lot of traffic.
We have some code in a Web Service that executes a BEGIN TRAN, then runs a few SQL queries (two inserts followed by an update). Then at the end executes a COMMIT. Twice now we have gotten the message in the logs:
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
Between the first two inserts and the update, we call another web service, so there could be a slight delay between the first two inserts and last update before the COMMIT is called. Could this be causing our problem? We're running this on IIS 7 and Server 2008 R2 (all updated applied).
Originally we though it could be the app pools getting recycled, but changed that to recycle in the middle of the night. Now I'm not sure what would be causing SQL server to forget the call to BEGIN TRAN.
This web service does get called quite a bit. Has anyone seen something like this before? I'm at a total loss at the moment...
Any help or suggestion appreciated greatly!
It looks like your transaction failed, got rolled back and there is nothing to commit
example of such a thing
CREATE TABLE BlaTest(id INT PRIMARY KEY NOT NULL)
GO
Now run this
BEGIN TRAN
INSERT BlaTest VALUES('a')
GO
COMMIT TRAN
Here is the error
Msg 245, Level 16, State 1, Line 3
Conversion failed when converting the varchar value 'a' to data type int.
Msg 3902, Level 16, State 1, Line 2
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
This will run without a problem
BEGIN TRAN
INSERT BlaTest VALUES(5)
GO
COMMIT TRAN
A good article on transactions is Error Handling in SQL 2005 and Later by Erland Sommarskog
My issue was I needed a BEGIN and END around my BEGIN TRAN and COMMIT TRAN.
BEGIN
BEGIN TRAN
INSERT BlaTest VALUES(5)
GO
COMMIT TRAN
END
BEGIN TRANS
at the top will help
I had the same issue. This is what I did to solve it.
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION
After I Checked the SQL Query and Add a BEGIN TRAN it will executed successfully. Here My sample code. It will work:
ALTER procedure [dbo].[DeactivateUser]
#UserId bigint,
#LoginEmail Nvarchar(100),
#merchantId int
as
Begin
Begin tran
update Users set
LoginEmail='inactive'+CONVERT(VARCHAR(11), getdate(), 106)+'-'+#LoginEmail,
IsActive=0
where LoginEmail=#LoginEmail and MerchantID=#merchantId
if(##ERROR=0)
begin
commit Tran
select 0
end
else
begin
rollback Tran
select -1
end
end

Resources