It is unclear to me if I need to used a different save point names for each SP I use SAVE TRANSACTION.
Could I always use e.g. SAVE TRANSACTION ProcedureSavePoint and ROLLBACK TRANSACTION ProcedureSavePoint even if a higher level transaction used the same save point name?
My SP(s) signature is as follow:
ALTER PROCEDURE [dbo].[usp_MyTask]()
AS
BEGIN
DECLARE #iReturn int = 0
DECLARE #tranCount int = ##TRANCOUNT;
IF #tranCount > 0
SAVE TRANSACTION ProcSavePoint;
ELSE
BEGIN TRAN
...
IF <some condition>
BEGIN
#iReturn = 1
GOTO Undo
END
...
IF #tranCount = 0
COMMIT TRAN
RETURN
Undo:
IF #tranCount = 0 -- transaction started in procedure. Roll back complete transaction.
ROLLBACK TRAN;
ELSE
IF XACT_STATE() <> -1 ROLLBACK TRANSACTION ProcSavePoint;
RETURN #iReturn
END
Hope my question is clear.
Technically, yes, you can re-use the same Save Point name, and they will get stacked up just like multiple calls to BEGIN TRAN where each call to COMMIT simply decrements the counter. Meaning, if you issue SAVE TRANSACTION ProcSavePoint; 5 times, and then call ROLLBACK TRANSACTION ProcSavePoint; 2 times, you will still be left at the state things were in after calling SAVE TRAN the third time and prior to calling it the fourth time.
However, this code is problematic on a few levels:
Due to the behavior just mentioned, in a nested scenario, depending on the condition(s) for calling GOTO Undo, if you have a situation where you call nested procs 5 levels deep, and then level 5 completes successfully, and then level 4 completes successfully, but then level 3 decides to go to "undo", it will execute ROLLBACK TRANSACTION ProcSavePoint; which will only roll-back that fifth level. This leaves you in a bad state because the intention was to roll-back to the state things were in when level 3 started.
Using unique Save Point names would correct for this.
You are oddly not using the TRY / CATCH construct. You really should. If you have logic that will decide to cancel an operation based on a particular condition that is not a SQL Server error, you can still force that by calling RAISERROR() to go immediately to the CATCH block. Or if you don't want to handle that as an error, you can still do your GOTO undo method in addition to the TRY / CATCH.
I do not believe XACT_STATE() can report a -1 outside of a TRY / CATCH construct.
Why are you using Save Points in the first place? Do you have situations in which outer layers might continue and eventually COMMIT even if there were errors happening in sub-proc calls?
The template I use most often is shown in my answer to this question on DBA.StackExchange: Are we required to handle Transaction in C# Code as well as in Store procedure. That template simply checks for an active transaction at the beginning (similar to your method), but then does nothing if there is an active transaction. So there is never an additional BEGIN TRAN or even SAVE TRAN called, and only the outer later (even if it is app code), will do the COMMIT or ROLLBACK.
And just to have this pointed out because it looks like a functional difference between your code and what I posted in that linked answer, but really isn't: there is no specific need to trap the actual value of ##TRANCOUNT since the only options are 0 or > 0, and unless ##TRANCOUNT is already > 1 upon entering your template, the max it will ever get is 1 anyway (possibly 2 if Triggers and/or INSERT INTO ... EXEC increment even if there is an active transaction). In either case, my use of a BIT variable for #InNestedTransaction is functionally / logically equivalent to storing ##TRANCOUNT in an INT variable since SAVE TRAN does not increment ##TRANCOUNT.
Related
I'm writing a .NET 7 app that connects to an Azure SQL database using ADO.NET.
Some code paths require a transaction to be opened and multiple commands (calls to stored procedures) to be sent to the database as part of the same transaction.
The stored procedures are mostly simple, atomic CRUD operations, but they may also involve multiple statements if the app entities and their underlying storage are different (e.g., for performance or abstraction).
My current need is to have the stored procedures return some value to the app layer to signal some partially faulted but recoverable state (e.g., an UPDATE on a row that does not exist).
I also may want the stored procedure to roll back the transaction to an earlier save point in these cases, but avoid it to roll back the entire transaction.
The app will manage these states using custom exceptions (unexpected, recoverable errors), so my first approach was to use THROW in my stored procedures as well since the exception would map nicely with ExecuteNonQuery() and with some care also with ExecuteReader().
The problem is that in my stored procedures, I use XACT_ABORT ON and TRY...CATCH by default, so if I throw a custom error from inside the TRY block, the transaction would be rolled back at the end of the batch.
I thought about using RAISERROR instead of THROW (that should not honor XACT_ABORT as per Microsoft documentation). Still, on an Azure SQL database, I cannot use sp_addmessage, so I'm limited to returning only error 50000; plus Microsoft suggests THROW for new developments.
Also, I don't think I want to set XACT_ABORT OFF because if a non-custom exception is thrown, I'd like the entire transaction to be rolled back since it's actually an unexpected and non-recoverable situation.
The only options I see left are a custom OUTPUT parameter (e.g., #State) or the RETURN value, but that would require me to ensure the output/return value is always mapped inside the app code, instead of throwing by default.
Are these considerations correct? Are there any alternatives that I'm not considering?
I also wrote this draft stored procedure template for this situation using the return value. Is there anything I should pay attention to?
create or alter procedure [App].[SampleProcedure]
as
declare
#savepointName char(32),
#shouldCommit bit;
begin
set xact_abort, nocount on;
set transaction isolation level read committed;
set #savepointName = replace(newid(), '-', '');
if ##trancount > 0
begin
set #shouldCommit = 0;
save transaction #savepointName;
end;
else
begin
set #shoudlcommit = 1;
begin transaction #savepointName;
end;
begin try
if /* some condition */ 1 = 0
begin
rollback transaction #savepointName;
return -1; /* Precondition failed */
end;
-- update set ... where ...;
if /* some condition */ 1 = 0
begin
rollback transaction #savepointName;
return -2; /* Condition failed */
end;
-- insert into ... values ...;
if isnull(#shouldCommit, 0) = 1
begin
commit transaction #savepointName;
return 0; /* Everything is fine */
end;
end try
begin catch
if ##trancount > 0
begin
rollback transaction;
end;
throw;
end catch
end;
After some time, I came to these conclusions:
"Recoverable errors" are not exceptions. It would certainly be convenient to treat them as such, but the fact that you can detect and recover from them means they're just rare error conditions you thought about.
More than C# exceptions, T-SQL ones are targeted to exceptional, non-recoverable situations where the transaction needs to be rolled back. The best practice of setting XACT_ABORT ON inside procedures is proof.
As per the previous two points, a return value (or an output parameter) is the correct solution to the problem. I chose the return value because it's enough for my use case and will allow me to keep the EnableOptimizedParameterBinding enabled.
I have written a script to do a little experiment:
use MyDatabase;
declare #variable int;
set #variable = 5;
begin tran test
set #variable = 6;
rollback tran test;
select #variable;
As you can see, #variable is declared at the start of the script, initialized with 5 and then inside a transaction it is set to 6. After the transaction is rolled back, we display #variable, which still has a value of 6, despite the fact that its value change was inside a transaction which was rolled back since then. I would have expected the selection to yield a result of 5. What is the cause of this behavior?
Because transactions are used to maintain the real data, the data in tables (not variables, not table variables). That's about all there is to it.
This is useful for scenarios where you may want to rollback but return some individual value from within the transaction as an indication of why you rolled back (perhaps throwing or just retuning a variable status code).
Consider what it would take for the variable to obey transactional semantics. You would need to write all changes to a transaction log (perhaps not the transaction log) where they could be rolled forward and back arbitrarily. And for what? Variables by their nature are ephemeral, so you wouldn't restore them in the case of a database or server crash. So, it's a high cost, low reward operation. Not the quadrant you want to be in.
I have some TSQL code with the following pattern:
BEGIN TRAN;
DECLARE #Set int;
BEGIN TRY
SET #Set = dbo.UDFThatCanTriggerAnError('ByCastingTheDescriptiveErrorToInt');
END TRY
BEGIN CATCH
SET #Set = -1; --Default value;
END CATCH
--Really in another sproc called from here but shown here for brevity
SAVE TRAN Testing;
ROLLBACK TRAN Testing;
--Back to the original sproc
ROLLBACK TRAN;
(In reality the rollbacks are only triggered if there are further errors inside the sprocs, but hopefully that gives the idea.)
In testing on SQL Server 2008 R2 SP1, the transaction save operation consistently throws the error:
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
If I change the UDF call to one which doesn't raise the error, or replace the line with a different way of setting the variable, the code completes.
It'd be so much easier if TRY..CATCH blocks here could be made to work in the same way the analogous blocks do in .Net and let me elect to reset the error flag, rather than automatically assuming every last error is terminal - this one definitely isn't, and is much less code than any alternative way of controlling flow.
So - is there a way to reset the error status inside the Catch block or do I need to rewrite the logic to avoid the caught error being thrown in the first place?
For so long, I've omitted using SQL Transactions, mostly out of ignorance.
But let's say I have a procedure like this:
CREATE PROCEDURE CreatePerson
AS
BEGIN
declare #NewPerson INT
INSERT INTO PersonTable ( Columns... ) VALUES ( #Parameters... )
SET #NewPerson = SCOPE_IDENTITY()
INSERT INTO AnotherTable ( #PersonID, CreatedOn ) VALUES ( #NewPerson, getdate() )
END
GO
In the above example, the second insert depends on the first, as in it will fail if the first one fails.
Secondly, and for whatever reason, transactions are confusing me as far as proper implementation. I see one example here, another there, and I just opened up adventureworks to find another example with try, catch, rollback, etc.
I'm not logging errors. Should I use a transaction here? Is it worth it?
If so, how should it be properly implemented? Based on the examples I've seen:
CREATE PROCEURE CreatePerson
AS
BEGIN TRANSACTION
....
COMMIT TRANSACTION
GO
Or:
CREATE PROCEDURE CreatePerson
AS
BEGIN
BEGIN TRANSACTION
COMMIT TRANSACTION
END
GO
Or:
CREATE PROCEDURE CreatePerson
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
...
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
END CATCH
END
Lastly, in my real code, I have more like 5 separate inserts all based on the newly generated ID for person. If you were me, what would you do? This question is perhaps redundant or a duplicate, but for whatever reason I can't seem to reconcile in my mind the best way to handle this.
Another area of confusion is the rollback. If a transaction must be committed as a single unit of operation, what happens if you don't use the rollback? Or is the rollback needed only in a Try/Catch similar to vb.net/c# error handling?
You are probably missing the point of this: transactions are suppose to make a set of separate actions into one, so if one fails, you can rollback and your database will stay as if nothing happened.
This is easier to see if, let's say, you are saving the details of a purchase in a store. You save the data of the customer (like Name or Address), but somehow in between, you missed the details (server crash). So now you know that John Doe bought something, but you don't know what. You Data Integrity is at stake.
Your third sample code is correct if you want to handle transactions in the SP. To return an error, you can try:
RETURN ##ERROR
After the ROLLBACK. Also, please review about:
set xact_abort on
as in: SQL Server - transactions roll back on error?
If the first insert succeeds and the second fails you will have a database in a bad state because SQL Server cannot read your mind. It will leave the first insert (change) in the database even though you probably wanted it all tosucceed or all fail.
To ensure this you should wrap all the statements in begin transaction as you illustrated in the last example. Its important to have a catch so any half completed transaction are explicitly rolled back and the resources (used by the transaction) released as soon as possible.
If I have a stored procedure that executes another stored procedure several times with different arguments, is it possible to have each of these calls commit independently of the others?
In other words, if the first two executions of the nested procedure succeed, but the third one fails, is it possible to preserve the results of the first two executions (and not roll them back)?
I have a stored procedure defined something like this in SQL Server 2000:
CREATE PROCEDURE toplevel_proc ..
AS
BEGIN
...
while #row_count <= #max_rows
begin
select #parameter ... where rownum = #row_count
exec nested_proc #parameter
select #row_count = #row_count + 1
end
END
First off, there is no such thing as a nested transaction in SQL Server
However, you can use SAVEPOINTs as per this example (too long to reproduce here sorry) from fellow SO user Remus Rusanu
Edit: AlexKuznetsov mentioned (he deleted his answer though) that this won't work if a transaction is doomed. This can happen with SET XACT_ABORT ON or some trigger errors.
From BOL:
ROLLBACK TRANSACTION without a
savepoint_name or transaction_name
rolls back to the beginning of the
transaction. When nesting
transactions, this same statement
rolls back all inner transactions to
the outermost BEGIN TRANSACTION
statement.
I also found the following from another thread here:
Be aware that SQL Server transactions
aren't really nested in the way you
might think. Once an explict
transaction is started, a subsequent
BEGIN TRAN increments ##TRANCOUNT
while a COMMIT decrements the value.
The entire outmost transaction is
committed when a COMMIT results in a
zero ##TRANCOUNT. But a ROLLBACK
without a savepoint rolls back all
work including the outermost
transaction.
If you need nested transaction
behavior, you'll need to use SAVE
TRANSACTION instead of BEGIN TRAN and
use ROLLBACK TRAN [savepoint_name]
instead of ROLLBACK TRAN.
So it would appear possible.