Looking at the SQL Server Books Online, Microsoft seems to have an (incorrect) method of handling nested transactions in a stored procedure:
Nesting Transactions
Explicit transactions can be nested. This is primarily intended to support transactions in stored procedures that can be called either from a process already in a transaction or from processes that have no active transaction.
The example goes on to show a stored procedure that starts its own transaction ("The procedure enforces its transaction regardless of the transaction mode of any process that executes it."):
CREATE PROCEDURE TransProc #PriKey INT, #CharCol CHAR(3) AS
BEGIN TRANSACTION InProc
...
COMMIT TRANSACTION InProc;
This procedure can then either be called without a transaction running:
EXECUTE TransProc 3,'bbb';
Or with an explicit transaction:
BEGIN TRANSACTION OutOfProc;
EXEC TransProc 1, 'aaa';
COMMIT TRANSACTION OutOfProc
What they don't address is what happens when the stored produre:
fails with an error, but leaves the transaction running
fails with an error, but doesn't leave the transaction running
encounters an error, but continues executing with the transaction open
encounters an error, but continues executing with the transaction rolled back
There is no:
SET XACT_ABORT ON
##TRANCOUNT
anywhere in the canonical example.
If i didn't know any better, i would have thought that the line:
The following example shows the intended use of nested transactions.
should actually read
The following example shows the how not to use nested transactions.
Unless someone can make heads or tails of this BOL example?
You need to use the try catch block with the transaction. So in case you get the error in your catch block then you can rollback your transaction.
Please see the below sql server code for that.
BEGIN TRANSACTION;
BEGIN TRY
-- Some code
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
CREATE PROCEDURE [usp_my_procedure_name]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #trancount int;
SET #trancount = ##trancount;
BEGIN TRY
IF #trancount = 0
BEGIN TRANSACTION
ELSE
SAVE TRANSACTION usp_my_procedure_name;
-- Do the actual work here
lbexit:
IF #trancount = 0
COMMIT;
END TRY
BEGIN CATCH
DECLARE #error int,
#message varchar(4000),
#xstate int;
SELECT
#error = ERROR_NUMBER(),
#message = ERROR_MESSAGE(),
#xstate = XACT_STATE();
IF #xstate = -1
ROLLBACK;
IF #xstate = 1 AND #trancount = 0
ROLLBACK
IF #xstate = 1 AND #trancount > 0
ROLLBACK TRANSACTION usp_my_procedure_name;
RAISERROR ('usp_my_procedure_name: %d: %s', 16, 1, #error, #message);
END CATCH
END
Related
I am providing 4 examples below. My question is - which is the appropriate template to use when dealing with try/catch and transactions.
Example 1
TSQL Try / Catch within Transaction or vice versa?
The accepted answer for the above has the following structure:
BEGIN TRY
BEGIN TRANSACTION SCHEDULEDELETE
// do something
COMMIT TRANSACTION SCHEDULEDELETE
PRINT 'Operation Successful.'
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION SCHEDULEDELETE
PRINT 'Error detected, all changes reversed'
END
//may be print/log/throw error
END CATCH
Example 2
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql?view=sql-server-ver15#b-using-trycatch-in-a-transaction
The structure given is:
BEGIN TRANSACTION;
BEGIN TRY
-- Generate a constraint violation error.
DELETE FROM Production.Product
WHERE ProductID = 980;
END TRY
BEGIN CATCH
//may be print/log/throw error
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
GO
Example 3
https://learn.microsoft.com/en-us/sql/t-sql/language-elements/try-catch-transact-sql?view=sql-server-ver15#c-using-trycatch-with-xact_state
Microsoft also recommends using XACT_ABORT.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
//do something
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Test whether the transaction is uncommittable.
IF (XACT_STATE()) = -1
BEGIN
PRINT
N'The transaction is in an uncommittable state.' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
-- Test whether the transaction is committable.
-- You may want to commit a transaction in a catch block if
-- you want to commit changes to statements that ran prior
-- to the error.
IF (XACT_STATE()) = 1
BEGIN
PRINT
N'The transaction is committable.' +
'Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH;
Example 4
This is what I use personally
BEGIN TRY
BEGIN TRANSACTION
//do something
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION
END ;
//may be print/log/throw error
END CATCH
To summarize:
Example 1 - Transaction is inside the try block
Example 2 - Try is inside the transaction block, COMMIT is after the catch block
Example 3 - Transaction is inside the try block with xact_abort ON functionality
Example 4 - Transaction is inside the try block, COMMIT is inside and as the last line of the try block
Is my approach (example 4) the correct way to handle try/catch and transaction in SQL. If not, then why not and which example should I use to refactor my code from example 4?
according to the documentation
TRY...CATCH constructs do not trap the following conditions:
Warnings or informational messages that have a severity of 10 or
lower.
Errors that have a severity of 20 or higher that stop the SQL Server
Database Engine task processing for the session. If an error occurs
that has severity of 20 or higher and the database connection is not
disrupted, TRY...CATCH will handle the error.
Attentions, such as client-interrupt requests or broken client
connections.
When the session is ended by a system administrator by using the KILL
statement.
The following types of errors are not handled by a CATCH block when
they occur at the same level of execution as the TRY...CATCH
construct:
Compile errors, such as syntax errors, that prevent a batch from
running.
Errors that occur during statement-level recompilation, such as object
name resolution errors that occur after compilation because of
deferred name resolution.
Object name resolution errors
These errors are returned to the level that ran the batch, stored
procedure, or trigger.
so taking this fact into account, let's consider this examples.
Exaple 1: If we try to delete a row from a non-existent table, the session will remain with an open transaction, which will preserve resource locks and prevent VLFs from being reused in the transaction log for a simple recovery model.
BEGIN TRY
BEGIN TRANSACTION SCHEDULEDELETE
Delete From dbo.NoExists Where ID=1
COMMIT TRANSACTION SCHEDULEDELETE
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION SCHEDULEDELETE
END
END CATCH
Invalid object name "dbo.NoExists".
Select ##TRANCOUNT
-- 1
Example 2: The same problem as in example 1.
BEGIN TRANSACTION;
BEGIN TRY
DELETE FROM dbo.NoExists Where ID=1
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION;
Invalid object name "dbo.NoExists".
Select ##TRANCOUNT
-- 1
Example 3: In this example, XACT_ABORT ON will rollback the transaction if CATCH block doesn't handle this error and, therefore, doesn't rollback the transaction.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
DELETE FROM dbo.NoExists Where ID=1
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- Test whether the transaction is uncommittable.
IF (XACT_STATE()) = -1
BEGIN
PRINT
N'The transaction is in an uncommittable state.' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
IF (XACT_STATE()) = 1
BEGIN
PRINT
N'The transaction is committable.' +
'Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH;
Invalid object name "dbo.NoExists".
Select ##TRANCOUNT
-- 0
The below sql gives a syntax error at RAISEERROR, which goes away if I remove [Tran1] from the query. What is the correct syntax to rollback an aliased transaction and then call RAISERROR?
BEGIN TRY
BEGIN TRANSACTION [Tran1]
...sql goes here...
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION [Tran1]
RAISEERROR ('Error occured')
END CATCH;
Also, if I remove the [Tran1]I get:
Cannot roll back RAISEERROR. No transaction or savepoint of that name
was found.
You do not mention the essential COMMIT TRANSACTION [Tran1] within the 'TRY' block. I presume you have that.
The RAISERROR will raise an error by itself, since it has to be coded like
RAISERROR('Error Occurred',0,0).
A RAISERROR statement with an error will call the BEGIN CATCH section, but there is no transaction anymore to ROLLBACK.
It's better if you don't lose information about the original exception. In SQL Server 2012 and above there is a THROW clause exactly for this purpose. So you can simply do:
BEGIN TRY
BEGIN TRANSACTION [Tran1];
--- Some SQL here --
COMMIT TRANSACTION [Tra1];
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION [Tran1];
THROW;
END CATCH;
In SQL Server 2008R2 and below, it's a bit more cumbersome to preserve the error information:
BEGIN TRY
BEGIN TRANSACTION [Tran1];
--- Some SQL here --
COMMIT TRANSACTION [Tra1];
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION [Tran1];
-- Now throw the exception
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH;
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
I have a TSQL script that does a lot of database structure adjustments but it's not really safe to just let it go through when something fails.
to make things clear:
using MS SQL 2005
it's NOT a stored procedure, just a script file (.sql)
what I have is something in the following order
BEGIN TRANSACTION
ALTER Stuff
GO
CREATE New Stuff
GO
DROP Old Stuff
GO
IF ##ERROR != 0
BEGIN
PRINT 'Errors Found ... Rolling back'
ROLLBACK TRANSACTION
RETURN
END
ELSE
PRINT 'No Errors ... Committing changes'
COMMIT TRANSACTION
just to illustrate what I'm working with ... can't go into specifics
now, the problem ...
When I introduce an error (to test if things get rolled back), I get a statement that the ROLLBACK TRANSACTION could not find a corresponding BEGIN TRANSACTION.
This leads me to believe that something when REALLY wrong and the transaction was already killed.
what I also noticed is that the script didn't fully quit on error and thus DID try to execute every statement after the error occured. (I noticed this when new tables showed up when I wasn't expecting them because it should have rollbacked)
When the error occurs, the transaction is rolled back automatically, and the current batch is aborted.
Execution continues into the next batch, however. So all the stuff in the batches after the error gets executed. And then when you check for errors later, you try to rollback an already rolled back transaction.
Also, to stop the entire script, not just the current batch, you should use:
raiserror('Error description here', 20, -1) with log
See my answer here for details on that one.
So you need to check for #error after each batch, I think something like this should work:
BEGIN TRANSACTION
GO
ALTER Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
CREATE New Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
DROP Old Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
PRINT 'No Errors ... Committing changes'
COMMIT TRANSACTION
Try using RETURN. this will exit the script or procedure immediately and will not execute any of the following statements. You can use this in conjunction with BEGIN, ROLLBACK and COMMIT TRANSACTION statements to undo any data damage:
BEGIN
BEGIN TRANSACTION
<first batch>
IF ##error <> 0
begin
RAISERROR ('first batch failed',16,-1)
ROLLBACK TRANSACTION
RETURN
end
<second batch>
IF ##error <> 0
begin
RAISERROR ('second batch failed',16,-1)
ROLLBACK TRANSACTION
RETURN
end
PRINT 'WIN!'
COMMIT TRANSACTION
END
I didn't use the raiseerror solution, because it failed as I didn't have admin permissions. I extended the noexec on/off solution with the transaction handling as follows:
set noexec off
begin transaction
go
<First batch, do something here>
go
if ##error != 0 set noexec on;
<Second batch, do something here>
go
if ##error != 0 set noexec on;
<... etc>
declare #finished bit;
set #finished = 1;
SET noexec off;
IF #finished = 1
BEGIN
PRINT 'Committing changes'
COMMIT TRANSACTION
END
ELSE
BEGIN
PRINT 'Errors occured. Rolling back changes'
ROLLBACK TRANSACTION
END
Apparently the compiler "understands" the #finished variable in the IF, even if there was an error and the execution was disabled. However, the value is set to 1 only if the execution was not disabled. Hence I can nicely commit or rollback the transaction accordingly.
You could try something like this... If you are using Try block... The error level 16, (or most of application error), immediately transfers the control to the CATCH block without executing any further statements in the try block...
Begin Transaction
Begin Try
-- Do your Stuff
If (##RowCount <> 1) -- Error condition
Begin
Raiserror('Error Message',16,1)
End
Commit
End Try
Begin Catch
IF ##Trancount > 0
begin
Rollback Transaction
End
Declare #ErrMsg varchar(4000), #Errseverity int
SELECT #ErrMsg = ERROR_MESSAGE(),
#ErrSeverity = ERROR_SEVERITY()
RAISERROR(#ErrMsg, #ErrSeverity, 1)
End Catch
Hope this helps...
SET XACT_ABORT ON
BEGIN TRAN
-- Batch 1
GO
if ##TRANCOUNT = 0
SET NOEXEC ON;
GO
-- Batch 2
GO
if ##TRANCOUNT = 0
SET NOEXEC ON;
GO
-- Batch 3
GO
if ##TRANCOUNT > 0
COMMIT
GO
If I simply wrap my query with:
BEGIN TRANSACTION
COMMIT TRANSACTION
If anything fails inside of that, will it automatically rollback?
From looking at other code, they seem to check for an error, if there is an error then they do a GOTO statement which then calls ROLLBACK TRANSACTION
But that seems like allot of work, to have to check for IF( ##ERROR <> 0) after every insert/update.
I typically do something like this inside my stored procedures. It keeps things nice and safe and passes along any errors that I encounter.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- Code goes here
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
DECLARE
#ERROR_SEVERITY INT,
#ERROR_STATE INT,
#ERROR_NUMBER INT,
#ERROR_LINE INT,
#ERROR_MESSAGE NVARCHAR(4000);
SELECT
#ERROR_SEVERITY = ERROR_SEVERITY(),
#ERROR_STATE = ERROR_STATE(),
#ERROR_NUMBER = ERROR_NUMBER(),
#ERROR_LINE = ERROR_LINE(),
#ERROR_MESSAGE = ERROR_MESSAGE();
RAISERROR('Msg %d, Line %d, :%s',
#ERROR_SEVERITY,
#ERROR_STATE,
#ERROR_NUMBER,
#ERROR_LINE,
#ERROR_MESSAGE);
END CATCH
yes it is important to explicitly rollback the transaction in the case that it does not work.
I usually tell my son you only have to brush the teeth you want to keep.
In this case, you only need to rollback the commands you don't want to execute.
This will automatically rollback the transaction in case off error
SET XACT_ABORT ON
BEGIN TRANSACTION
-- CODE HERE
COMMIT TRANSACTION
For transaction control you use begin, commit and rollback. You begin a transaction by supplying BEGIN TRANSACTION. Then you put the various SQL statements you need. Then you end the transaction by issuing either a commit or rollback. COMMIT TRANSACTION will commit all the changes that you did to the database after the BEGIN statement and make them permanent, so to speak. ROLLBACK TRANSACTION will rollback all changes that you did to the database after the BEGIN statement. However, it will not change variable values.
Example:
BEGIN TRANSACTION
UPDATE table SET column = 'ABC' WHERE column = '123'
COMMIT TRANSACTION
--//column now has a value of 'ABC'
BEGIN TRANSACTION
UPDATE table SET column = 'ABC' WHERE column = '123'
ROLLBACK TRANSACTION
--//column still has it's previous value ('123') No changes were made.