I need to execute a store procedure from another with the common EXEC command.
I need to be sure, that all the sql statements will be under transaction.
BEGIN TRANSACTION
BEGIN TRY
SET #Esercizio = (SELECT ESERCIZIO_OBIETTIVI_CONSUNTIVARE from TB_SCHEDE WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE)
SET #TipoProcesso = (SELECT ISNULL(TipoProcesso, 'middle') from TB_SCHEDE WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE)
DELETE FROM TB_SCHEDE WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE
DELETE FROM TB_SCHEDE_AUTOVAL WHERE MATRICOLA = #iMATRICOLA and COD_VALUTAZIONE = #iCOD_VALUTAZIONE
DELETE FROM TB_OBIETTIVI WHERE MATRICOLA = #iMATRICOLA and ESERCIZIO = #Esercizio
DELETE FROM TB_OBIETTIVI_AUTOVAL WHERE MATRICOLA = #iMATRICOLA and ESERCIZIO = #Esercizio
EXEC AnotherStore #iCOD_VALUTAZIONE, #iMATRICOLA, #TipoProcesso
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
If the AnotherStore procedure throw an exception, does the DB engine ensure rollback from the caller store procedure?
Hope to be clear.
See Exception handling and nested transactions for an example of execption handling in the presence of transactions:
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
The simple answer is yes it will rollback the changes for the caller stored procedure, but think it through if you have transactions inside the other stored procedure, there are possibilities for things not behaving as you expect if that is the case. A ROLLBACK will affect ALL of the transactions, although this is probably what you want. You can use ##TRANCOUNT within your catch and determine if you want to rollback the entire thing, and Savepoints.
Everything you do to the database between the BEGIN TRANSACTION and COMMITor ROLLBACK is part of the transaction, and if any lines get an error, control will be routed to the CATCH block where the transaction will be rolled back. Things like table variables would fall outside of this scope and not get rolled back. And as #David Brabant said, BEGIN TRANSACTION should be in the BEGIN TRY block.
Related
As demonstrated here,
If I have an inner stored procedure, that always rolls back without an error.
CREATE PROCEDURE [inner] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
IF (1 + 1 = 2) BEGIN
ROLLBACK TRANSACTION;
END ELSE BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
ROLLBACK TRANSACTION;
END
END CATCH
END
and an outer SP that calls the inner,
CREATE PROCEDURE [outer] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
EXEC [inner];
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
ROLLBACK TRANSACTION;
END
;THROW;
END CATCH
END
and, then I call the outer SP,
EXEC [outer];
I get this error
Msg 266 Level 16 State 2 Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements.
Previous count = 1, current count = 0.
Now, initially this is not what I was expecting.
What I think is happening,
axioms:
BEGIN TRANSACTION always increments ##TRANCOUNT by 1;
COMMIT TRANSACTION always decrements ##TRANCOUNT by 1;
ROLLBACK TRANSACTION always resets ##TRANCOUNT to 0;
There are no nested transactions!
When ##TRANCOUNT changes to any value, other than 0, nothing else happens.
so, step by step,
[outer] -> BEGIN TRANSACTION sets ##TRANCOUNT to 1.
[outer] -> calls [inner] with ##TRANCOUNT 1.
[inner] -> ROLLBACK TRANSACTION resets ##TRANCOUNT to 0.
[inner] -> returns to [outer] with ##TRANCOUNT 0.
SQL Server detects a mismatch in ##TRANCOUNT before and after the call to [inner] and,
therefore raises the error above.
So, my questions are,
Is my understanding correct and where is the canonical documentation of this?
What is the best way to write an Stored Procedure that may want to rollback its own changes, so that it can be called directly in its own batch or from within another transaction?
Yes, your understanding is correct.
The documentation is here:
In stored procedures, ROLLBACK TRANSACTION statements without a savepoint_name or transaction_name roll back all statements to the outermost BEGIN TRANSACTION. A ROLLBACK TRANSACTION statement in a stored procedure that causes ##TRANCOUNT to have a different value when the stored procedure completes than the ##TRANCOUNT value when the stored procedure was called produces an informational message. This message does not affect subsequent processing.
I'm not sure why it says "does not affect subsequent processing" as that is demonstrably false: instead it throws an error with severity 16, which can be caught with BEGIN CATCH.
If you do want to use nested transactions such as this, and be able to roll them back only partially, it becomes significantly more complicated.
You have to conditionally begin a transaction if there is none. Then you have to SAVE a savepoint. And then each rollback must conditionally roll back either the whole transaction or only the savepoint
CREATE PROCEDURE [inner] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
DECLARE #tranCount int = ##TRANCOUNT;
IF #tranCount = 0
BEGIN TRANSACTION;
SAVE TRANSACTION innerSave;
IF (1 + 1 = 2) BEGIN
IF #tranCount = 0
ROLLBACK;
ELSE
ROLLBACK TRANSACTION innerSave;
END ELSE BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
IF #tranCount = 0
ROLLBACK;
ELSE
ROLLBACK TRANSACTION innerSave;
END
END CATCH
END
db<>fiddle
ALTER PROCEDURE Add_Edit_Courses_new
#CourseCode VARCHAR,
... other params ...
AS
BEGIN TRY
DECLARE #ErrorCode INT =0, #ErrorMessage VARCHAR(25) = 'Action failed'
IF #TaskType > 2
BEGIN
RAISERROR('Wrong action key',16,1)
END
ELSE
BEGIN TRANSACTION
BEGIN
DECLARE #message VARCHAR(MAX)
IF #TaskType = 1
BEGIN
INSERT INTO Courses(...) VALUES(#CourseCode,...)
SET #message = 'Added Successfully'
END
ELSE IF #TaskType = 2
BEGIN
UPDATE Courses SET CourseCode=#CourseCode,...;
SET #message = 'Modified Successfully'
END
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT ERROR_NUMBER() AS ErrorNumber, ...
END CATCH
I wrote this stored procedure to insert and update and I used (1 & 2) to differentiate the task while using a try and catch, but every time I try to execute this stored procedure I keep getting that error, please can you help me with where I am wrong, I am just learning this principle for the first time.
Why is BEGIN TRANSACTION before BEGIN? I feel like BEGIN TRANSACTION/COMMIT TRANSACTION should be inside the ELSE conditional. With some noise removed:
IF #TaskType > 2
BEGIN
RAISERROR('Wrong action key',16,1);
END
ELSE
BEGIN -- moved this here
BEGIN TRANSACTION;
-- BEGIN -- removed this
DECLARE #message varchar(max);
IF #TaskType = 1
BEGIN
INSERT INTO Courses(...
SET #message = 'Added Successfully';
END
IF #TaskType = 2 -- don't really need ELSE there
BEGIN
UPDATE Courses SET ...
SET #message = 'Modified Successfully';
END
-- END -- removed this
COMMIT TRANSACTION;
SELECT #message;
END -- moved this here
In your catch you just blindly say:
ROLLBACK TRANSACTION;
This should be updated to:
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION;
END
Note that if you have a conditional where multiple statements are not wrapped correctly in BEGIN / END, they won't execute like you think. Consider:
IF 1 = 0
PRINT 'foo';
PRINT 'bar';
You get bar output every time, regardless of the result of the conditional.
You had something similar:
IF 1 = 1
-- do stuff
ELSE
BEGIN TRANSACTION;
BEGIN
-- do stuff
END
COMMIT TRANSACTION;
In that case, -- do stuff and the commit happened every time, even if the begin transaction did not, because that BEGIN/END wrapper (and anything that followed it) was not associated with the ELSE.
Perhaps a little dry and wordy if you're new to the topic, but Erland Sommarskog has a very comprehensive series on error handling here, might be worth a bookmark:
https://www.sommarskog.se/error_handling/Part1.html
https://www.sommarskog.se/error_handling/Part2.html
https://www.sommarskog.se/error_handling/Part3.html
Imo there are some major issues with this code. First, if the intention is for the transaction to be atomic then SET XACT_ABORT ON should be specified. Per the Docs RAISERROR does not honor SET XACT_ABORT ON so it could be converted to use THROW. Second, in cases where the ELSE block in the code is hit then COMMIT will always be hit (regardless of what happens to the commitable state of the transaction).
Also, the code performs the ROLLBACK before the code:
SELECT ERROR_NUMBER() AS ErrorNumber, ...
It's the ROLLBACK which clears the error messages and returns the state to normal. To catch the error metadata SELECT it before the ROLLBACK happens.
We have a data import system where we are processing CSV files, each file contains a set of records that have to be validated and processed individually. If they fail for some reason, they're marked with the error and we then go on to the next record. They can fail for either logical or database errors such as dup key or foreign key errors.
The stored procedure that processes the data is either called from a client application, or from an agent job. The difference is that the app will create a transaction whereas the job will not before calling the proc.
SET NOCOUNT ON;
SET XACT_ABORT OFF;
DECLARE #trancount_in INT,
#xstate INT,
#Row INT,
#MaxRows INT
SET #trancount_in = ##trancount
-- CODE: import data into temp table
select #Row = 1,
#MaxRows = ##Rowcount
IF #transcount_in = 0
BEGIN TRANSACTION
while #Row <= #MaxRows
-- CODE: Get data record from table
begin try
save transaction myproc
-- CODE: validate and process record
end try
begin catch
SELECT #xstate = XACT_STATE()
IF #xstate = -1 BEGIN
ROLLBACK
Raiserror('error',16,1)
END
IF #xstate = 1 and ##Trancount = 0 BEGIN
ROLLBACK
Raiserror('error',16,1)
END
IF #xstate = 1 and ##Trancount > 0
ROLLBACK TRANSACTION myproc
-- CODE: update import record with error here
end catch
set #Row = #Row + 1
End
IF #transcount_in = 0
COMMIT
So fairly simple so far, but looking at lots of documentation, it says that you should check the xact_state for -1 in the catch block, but I don't want the transaction to fail at this point. I just want to go on to the next row in the import. So if xact_abort is off, would we ever get the xact_state of -1 in the catch block?
I know that the catch doesn't actually catch all errors, but I have found out that if you create a parent proc with a try catch block then it will catch more errors than the block in the actual proc. So I will be moving the loop logic to a parent proc and then just do the processing in the current one.
Is there anything else I should be doing to mitigate any errors that might occur?
Hope this isn't too confusing.
Cheers
Adrian
I have a simple scenario: logger procedure and main procedure from which logger is called. I am trying to rollback transaction inside logger which is started in main, but getting errors. I am not sure why. Here are the two procs and the error message I receive:
CREATE PROCEDURE spLogger
AS
BEGIN
IF ##TRANCOUNT > 0
BEGIN
PRINT ##TRANCOUNT
ROLLBACK
END
END
GO
CREATE PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
EXEC spLogger
END CATCH
END
GO
EXEC spCaller
1 Msg 266, Level 16, State 2, Procedure spLogger, Line 15 Transaction
count after EXECUTE indicates a mismatching number of BEGIN and COMMIT
statements. Previous count = 1, current count = 0.
1) The error message is clear: number of active TXs at the end of SP should be the same as number of active TXs at the beginning.
So, when at execution of dbo.spLogger begins the number of active TXs (##TRANCOUNT) is 1 if we execute within this SP the ROLLBACK statement this'll cancel ALL active TXs and ##TRANCOUNT becomes 0 -> error/exception
2) If you want just to avoid writing IF ##TRANCOUNT ... ROLLBACK within every CATCH block of every user SP then don't it. I would call dbo.spLogger within CATCH block after ROLLBACK.
3) If I have to call SPs from other SP using TXs then I would use following template (source: Rusanu's blog)
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;
throw;
end catch
end
with few small changes:
a) SET XACT_ABORT ON
b) I would call dbo.spLogger within CATCH block only when there is ##TRANCOUNT = 0:
IF ##TRANCOUNT = 0
BEGIN
EXEC dbo.spLogger ... params ...
END
THROW -- or RAISERROR(#message, 16, #xstate)
Why ? Because if dbo.spLogger SP will insert rows into a dbo.DbException table when one TX is active then in case of ROLLBACK SQL Server will have to ROLLBACL also these rows.
Example:
SP1 -call-> SP2 -call-> SP3
|err/ex -> CATCH & RAISERROR (no full ROLLBACK)
<-----------
|err/ex -> CATCH & RAISERROR (no full ROLLBACK)
<-------------
|err/ex -> CATCH & FULL ROLLBACK & spLogger
4) Update
CREATE PROC TestTx
AS
BEGIN
BEGIN TRAN -- B
ROLLBACK -- C
END
-- D
GO
-- Test
BEGIN TRAN -- A - ##TRANCOUNT = 1
EXEC dbo.TestTx
/*
Number of active TXs (##TRANCOUNT) at the begining of SP is 1
B - ##TRANCOUNT = 2
C - ##TRANCOUNT = 0
D - Execution of SP ends. SQL Server checks & generate an err/ex
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
*/
COMMIT -- E - Because ##TRANCOUNT is 0 this statement generates
another err/ex The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
-- End of Test
5) See autonomous transactions: it requires SQL2008+.
An Autonomous transaction is essentially a nested transaction where
the inner transaction is not affected by the state of the outer
transaction. In other words, you can leave the context of current
transaction (outer transaction) and call another transaction
(autonomous transaction). Once you finish work in the autonomous
transaction, you can come back to continue on within current
transaction. What is done in the autonomous transaction is truly DONE
and won’t be changed no matter what happens to the outer transaction.
keeping aside all xact_abort stuff,i see no reason why you should get the error.So did some research and here are the observations
----This works
alter PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
rollback
END CATCH
END
GO
---Again this works,took the text of sp and kept it in catch block
alter PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
--rollback
IF ##TRANCOUNT > 0
BEGIN
PRINT ##TRANCOUNT
ROLLBACK
END
END CATCH
END
GO
After some research Found answer by Remus Rusanu here:
If your caller starts a transaction and the calee hits, say, a deadlock (which aborted the transaction), how is the callee going to communicate to the caller that the transaction was aborted and it should not continue with 'business as usual'? The only feasible way is to re-raise an exception, forcing the caller to handle the situation. If you silently swallow an aborted transaction and the caller continues assuming is still in the original transaction, only mayhem can ensure (and the error you get is the way the engine tries to protect itself).
In your case,you are getting the error only when using a stored proc and trying to raise the error ,since a stored proc starts a seperate data context.The error you are getting may be SQL way of telling that this wont work.
Do u think there is a better way to write a transaction in t-sql? Is there a better approach that improves maintainability and performance of the application that uses this transaction?
-- Description: Insert email Receiver under specified subject
-- =============================================
ALTER PROCEDURE [Contact].[Receiver_stpInsert]
#First_Name nvarchar(30),
#Last_Name nvarchar(30),
#Email varchar(60),
#Subject_Id int
AS
BEGIN
SET NOCOUNT ON;
DECLARE #error_num int;
BEGIN TRANSACTION
INSERT INTO Contact.Receiver(First_Name, Last_Name, Email) VALUES(#First_Name, #Last_Name, #Email);
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
DECLARE #rec_record_id int;
SET #rec_record_id = (SELECT Record_Id FROM Contact.Receiver WHERE Email = #Email);
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
INSERT INTO Contact.Receiver_Subject(Receiver_Id, Subject_Id) VALUES(#rec_record_id, #Subject_Id);
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
SET #error_num = ##ERROR;
IF (#error_num <> 0)
BEGIN
ROLLBACK;
RETURN;
END
ELSE
BEGIN
Commit;
END
END
If you're using SQL 2005 or later, you can use the TRY...CATCH block, like this:
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO Contact.Receiver(First_Name, Last_Name, Email) VALUES (#First_Name, #Last_Name, #Email);
... other inserts etc
...
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
END CATCH;
This way, you don't keep repeating the same blocks of code checking ##ERROR. If you want to know what error occurred, in the BEGIN CATCH block you can get various bits of info:
ERROR_NUMBER() returns the number of the error.
ERROR_SEVERITY() returns the severity.
ERROR_STATE() returns the error state number.
ERROR_PROCEDURE() returns the name of the stored procedure or trigger
where the error occurred.
ERROR_LINE() returns the line number inside the routine that caused the
error.
ERROR_MESSAGE() returns the complete text of the error message. The text
includes the values supplied for any
substitutable parameters, such as
lengths, object names, or times.
For a long time now I've been advocating the use of TRY/CATCH and nested transactions in stored procedures.
This pattern gives you not only the much simplified error handling of the TRY/CATCH block compared with the ##ERROR check, but it also gives all-or-nothing nested semantics for procedure invocations.
If the procedure is called on the context of a transaction then the procedure rolls back only its own changes and leaves the caller to decide whether to rollback the embedding transaction or to try an alternate error path.
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) ;
return;
end catch
end
The draw backs of this approach are:
It does not work with distributed transactions. Because transaction savepoints are incompatible with distributed transactions, you cannot use this pattern when distributed transactions are required. IMHO distributed transactions are evil and should never be used anyway.
It alters the original error. This problem is inherent in TRY/CATCH blocks and there is nothing you can do about it. An application that is prepared to deal with the original SQL Server error codes (like 1202, 1205, 2627 etc) will have to be changed to deal with the error codes in the above 50000 range raised by Transact-SQL code that uses TRY/CATCH.
Also a word of caution about the use of SET XACT_ABORT ON. This setting will cause a batch to abort a transaction at any error. That raises any TRY/CATCH transaction handling basically useless and I recommend to be avoided.
If you have SQL Server 2000 or before, then yes - checking the ##ERROR value is basically all you can do.
With SQL Server 2005, Microsoft introduced the TRY...CATCH construct which makes it a lot easier:
BEGIN TRY
......
-- your T-SQL code here
......
END TRY
BEGIN CATCH
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() AS ErrorState,
ERROR_PROCEDURE() AS ErrorProcedure,
ERROR_LINE() AS ErrorLine,
ERROR_MESSAGE() AS ErrorMessage
-- do other steps, if you want
END CATCH
Asked not long ago. My answer with a TRY/CATCH template
If you are using sql 2005 or higher you should consider the TRY CATCH approach
You can wrap it all in a try catch, and then you only need to code the rollback in one place. See this for more details.