How to handle Transaction in Nested procedure in SQL server? - sql

I have 2 proc i.e. Proc1 and Proc2.
I am executing proc1 inside proc2. There are multiple DML operation in both procedure. output of proc1 is used in proc2 for DML operation.
if Error occurred in proc2 then
how to handle transaction in both proc for rollback all DML operation?
Should I write transaction in both proc?

We use a generic error handler procedure based on http://www.sommarskog.se/error_handling/Part1.html that we - when applicable - include in our (nested) transactions to ensure the chain is managed properly:
CREATE PROCEDURE [dbo].[sp_ErrorHandler](#caller VARCHAR(255))
AS BEGIN
SET NOCOUNT ON;
DECLARE #errmsg NVARCHAR(2048), #severity TINYINT, #state TINYINT, #errno INT, #lineno INT;
SELECT #errmsg=REPLACE(ERROR_MESSAGE(), 'DatabaseException: ', 'DatabaseException: '+QUOTENAME(#caller)+' --> ')
, #severity=ERROR_SEVERITY()
, #state=ERROR_STATE()
, #errno=ERROR_NUMBER()
, #lineno=ERROR_LINE();
IF #errmsg NOT LIKE 'DatabaseException%' BEGIN
SELECT #errmsg=N'DatabaseException: '+QUOTENAME(#caller)+N', Line '+LTRIM(STR(#lineno))+N', Error '+LTRIM(STR(#errno))+N': '+#errmsg;
END;
RAISERROR('%s', #severity, #state, #errmsg);
END;
(Compiled in the master database and marked as system procedure)
We use this error handler as follows. In the demo I have an outer proc and an inner proc both using a transaction.
CREATE PROCEDURE dbo.uspOuterProc
AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
EXEC dbo.uspInnerProc;
PRINT 1;
COMMIT;
END TRY
BEGIN CATCH
IF ##trancount > 0
ROLLBACK TRANSACTION;
EXEC master.dbo.sp_ErrorHandler #caller = 'dbo.uspOuterProc';
END CATCH;
END;
GO
CREATE PROCEDURE dbo.uspInnerProc
AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
PRINT 2;
SELECT 1 / 0;
PRINT 3;
COMMIT;
END TRY
BEGIN CATCH
IF ##trancount > 0
ROLLBACK TRANSACTION;
EXEC master.dbo.sp_ErrorHandler #caller = 'dbo.uspInnerProc';
END CATCH;
END;
GO
After you compile this and run:
EXEC dbo.uspOuterProc
You should get this result:
2
Msg 50000, Level 16, State 1, Procedure sp_ErrorHandler, Line 13 [Batch Start Line 48]
DatabaseException: [dbo.uspOuterProc] --> [dbo.uspInnerProc], Line 12, Error 8134: Divide by zero error encountered.

You can handle the transaction in the outer procedure ( in your case proc2). If any error will be occurred in proc1 it will be taken care of by proc2 transaction handler.
Am assuming that proc1 will not be called directly, it will be called inside the proc2.

There are 3 basic transaction handling statements (and a few advanced ones I'm not gonna mention):
BEGIN TRANSACTION: Will raise the ##TRANCOUNT session variable by 1. If it goes from 0 to 1 then this marks the start of a transaction. Any value higher than 1 will keep the same transaction ongoing.
COMMIT: Will lower the ##TRANCOUNT session variable by 1. If it goes from 1 to 0 then the transaction is marked as finished and will impact all changes done since it was first created.
ROLLBACK: Will decrease the ##TRANCOUNT session variable to 0 (whichever it's value was), as long as it was at least 1 or higher. This will close the transaction and revert all changes done since it was first created.
Nested transactions are a bunch of BEGIN TRANSACTION statements put together. The only point where the transaction gets fully commited and the changes are made permanent is when there is a COMMIT that lowers the transaction count from 1 to 0. That means you need one COMMIT for each BEGIN TRANSACTION you executed, like a pyramid.
Check the following example:
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 1
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2
COMMIT TRANSACTION
SELECT ##TRANCOUNT -- 1 (no change is permanent yet, not even the last one)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2
ROLLBACK
SELECT ##TRANCOUNT -- 0 (all changes were discarded)
When you have an SP that executes another SP and both have their transactions, the only thing you need to care about is to CATCH errors and do the proper ROLLBACK IF there's an open/active transaction ongoing (if not the ROLLBACK statement will fail saying that there is nothing to rollback).
A very basic CATCH would be like the following:
BEGIN TRY
BEGIN TRANSACTION
/* Do some operations */
/* Execute another SP that might have the following:
BEGIN TRANSACTION
-- Some other operations
COMMIT
*/
COMMIT
END TRY
BEGIN CATCH
DECLARE #v_ErrorMessage VARCHAR(MAX) = ERROR_MESSAGE()
IF ##TRANCOUNT > 0 -- Only rollback if there is an active transaction
ROLLBACK
RAISERROR (#v_ErrorMessage, 16, 1)
END CATCH
You can read this post if you want to delve deeply into the best way for handling transactions on SQL Server.

Related

Write stored procedure so if one statement fails it should not effect the other?

I have this procedure which basically insert data.
Begin Transaction
Insert into [dbo].Values
(
EQ
)
values
(
#EQ
)
End
--Set #STATUSRet= 'Created'
--Set #ErrorRet= ''
Commit Transaction
End Try
Begin Catch
Set #STATUSRet= 'Failed'
Set #ErrorRet= (Select ERROR_MESSAGE())
Rollback Transaction
End Catch
Now I want to add a piece of code that calls another database server and insert data into the table in that server i.e. remotely. That's ok I will do that but if that fails then that should not effect my current process of inserting the data as I have described above i.e. if the remote data insertion fails it should not effect the prior insert in any way and should return successfully to the calling application behaving like nothing happened.
The default method of controlling transactions is auto-commit:
Any single statement that changes data and executes by itself is
automatically an atomic transaction. Whether the change affects one
row or thousands of rows, it must complete successfully for each row
to be committed. You cannot manually rollback an auto-commit
transaction.
So, if the two inserts are not wrapped in explicit transaction this will be the behavior. If you have more code blocks, then you can use two separate explicit transactions blocks like this:
DECLARE #ExecuteSecondTransaction BIT = 0;
-- local database
BEGIN TRY
BEGIN TRANSACTION;
-- CODE BLOCK GOES HERE
SET #ExecuteSecondTransaction = 1;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
-- GET ERRORS DETAILS OR THROW ERROR
END CATCH;
-- remote database
IF #ExecuteSecondTransaction = 1
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
-- CODE BLOCK GOES HERE
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
-- GET ERRORS DETAILS OR THROW ERROR
END CATCH;
END;

Why are these nested SQL Server transactions throwing a mismatch error if there is a rollback?

By running 'Test Errors' I get unexpected results. I thought by checking for ##Trancount it would avoid mismatches. Can anyone help me with a better way to rollback errors? I want to rollback all transactions which are nested. Stored procedures can be both nested and on their own.
alter procedure TestErrors
as
begin
begin try
begin transaction
exec TestErrorsInner;
IF ##TRANCOUNT > 0
commit transaction;
end try
begin catch
IF ##TRANCOUNT > 0
rollback transaction;
select ERROR_MESSAGE();
end catch
end
alter procedure TestErrorsInner
as
begin
begin try
begin transaction
RAISERROR('Test Error',16,1);
IF ##TRANCOUNT > 0
commit transaction;
end try
begin catch
IF ##TRANCOUNT > 0
rollback transaction;
select ERROR_MESSAGE();
end catch
end
Results:
Test Error
Transaction count after EXECUTE indicates a mismatching number of
BEGIN and COMMIT statements. Previous count = 1, current count = 0.
This is because you are catching a transaction in the TestErrors which is not in Active state.
You have already rolled back your transaction in Catch block of TestErrorsInner.
Then again you are trying to do COMMIT/ROLLBACK it in TestErrors. So it is throwing an error.
It is your responsibility to Raise an Error explicitly again in Catch block of TestErrorsInner. So that Error will be the input for Parent SP.
So your TestErrorsInner should be like
ALTER PROCEDURE TESTERRORSINNER
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('TEST ERROR',16,1);
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
--SELECT ERROR_MESSAGE();
RAISERROR('TEST ERROR in Catch',16,1); --Here Raised
END CATCH
END
Now execute the TestErrors Stored procedure, you won't get that error.
And You can check the Transaction Status with XACT_STATE()
Calling XACT_STATE() will give result of 0 or 1 or -1 (From MSDN)
If 1, the transaction is committable.
If -1, the transaction is uncommittable and should be rolled back.
if XACT_STATE = 0 means there is no transaction and a commit or rollback operation would generate an error.

Rollback an entire stored procedure

I have a stored procedure with multiple update statements.I dont want to use try catch.How can I rollback the stored procedure and get back the original table?
can something like this work -
begin transaction t1
spName
rollback transaction t1
Yes you can wrap everything into a sproc into a transaction
begin tran
exec testproc
commit tran
--rollback tran --for condition
It works fine even for commit as well rollback
If for inside the sproc you need to open another transaction then you need to capture
DECLARE #vTranCount INT = ##TRANCOUNT
--Commit
IF (#vTranCount = 0 AND ##TRANCOUNT <> 0) COMMIT TRANSACTION --Commit if the Tran is created by this sproc
--rollback during catch
IF(#vTranCount = 0 AND ##TRANCOUNT > 0) ROLLBACK TRANSACTION --Rollback if the Tran is created by this sproc

Sql Server nested transaction rollback ##TRANCOUNT

I've got a stored procedure which begins a new transaction for data manipulation. The procedure itself is executed within another transaction.
I have no influence what happens before my procedure. And it could change.
My idea is to check the ##TRANCOUNT before I begin the nested transaction. Then check the ##TRANCOUNT again in catch block and compare it. In no case I want the outer transaction to be rollbacked. So i wonder if i am safe with this code?
thx for your help!
SET #TRANSCOUNTBEFORE = ##TRANCOUNT;
BEGIN TRANSACTION tx;
BEGIN TRY
/* some data manipulation here */
COMMIT TRANSACTION tx;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > #TRANSCOUNTBEFORE ROLLBACK TRANSACTION tx;
/* some error handling here */
END CATCH;

Msg 3902, Level 16, State 1. The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION

DECLARE #cnt_inv int,
#cnt_jrn int,
#pl_per varchar(2),
#pl_yr varchar(4),
#pl_jrn varchar (6),
#pl_inv varchar (6)
IF ##ERROR <> 0
BEGIN
BEGIN TRANSACTION JD_Imp
IF #cnt_inv > 0
BEGIN
BEGIN TRANSACTION JD_Inv
COMMIT TRANSACTION JD_Inv;
PRINT N'The Invoice Commits DONE.';
END
IF #cnt_jrn > 0
BEGIN
BEGIN TRANSACTION JD_Jrn
COMMIT TRANSACTION JD_Jrn;
PRINT N'The Journals Commits DONE.';
END
COMMIT TRANSACTION JD_Imp;
END
The core of your issue is this:
IF #cnt_jrn > 0
BEGIN TRANSACTION JD_Jrn
All this will do is only start a new transaction if #cnt_jrn > 0. It's still going to execute all of the code below regardless of the condition. So if #cnt_jrn <= 0, it's going to call commit transaction JD_Jrn without ever having started it.
You need to enclose the body of any multi-statement if body with begin and end. For example:
IF #cnt_jrn > 0
BEGIN
BEGIN TRANSACTION JD_Jrn
... code ...
END
But you are enclosing single insert and update statements in transactions, which is not necessary. SQL operations are guaranteed to be atomic, so you only need a transaction if you're spanning multiple operations.
Problem solved.....
(a) Having the Begin...END Blocks did solve the error message Msg 3902.. I noticed that without the BEGIN..END Blocks, previous runs of the procedure will still be hanging uncommitted
(b) IF ##ERROR <> 0 BEGIN will always be true so no wonder the script block inside was not being executed.
(c) The Debugger was not moving past the IF statement due to (b).
Thanks a lot Adam.