To test how a transaction work, I wrote the following t-sql. As the first attribute is the primary key, none of the insertions should be committed.
But, the first insertion is committed? Why?
begin transaction
insert into instructor
values ('99999', 'Yellow', 'Biology', 700000)
insert into instructor
values ('99999', 'Blue', 'Statistics', 85000)
commit;
select * from instructor where ID = '99999'
--delete from instructor where ID = '99999'
(1 row affected) Msg 2627, Level 14, State 1, Line 100 Violation of
PRIMARY KEY constraint 'PK__instruct__3214EC278C8DA99F'. Cannot insert
duplicate key in object 'dbo.instructor'. The duplicate key value is
(99999). The statement has been terminated.
Completion time: 2019-11-06T14:02:27.3436411+02:00
Because, when the XACT_ABORT IS OFF (which is the default):
When SET XACT_ABORT is OFF, in some cases only the Transact-SQL
statement that raised the error is rolled back and the transaction
continues processing.
and when it is ON:
.. if a Transact-SQL statement raises a run-time error, the entire
transaction is terminated and rolled back.
That's what we need to turn it ON and if you try the code below, you can check this:
DROP TABLE IF EXISTS [dbo].[StackOverflow];
CREATE TABLE [dbo].[StackOverflow]
(
[StackID] TINYINT PRIMARY KEY
);
SET XACT_ABORT ON;
BEGIN TRANSACTION;
INSERT INTO [dbo].[StackOverflow] ([StackID])
VALUES (105);
INSERT INTO [dbo].[StackOverflow] ([StackID])
VALUES (105);
COMMIT TRANSACTION;
SET XACT_ABORT OFF;
SELECT [StackID]
FROM [dbo].[StackOverflow];
Also, note that:
Compile errors, such as syntax errors, are not affected by SET XACT_ABORT.
The above means, that if you want to really have automatic transaction you need to use the following code block:
SET NOCOUNT, XACT_ABORT ON;
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;
SET NOCOUNT, XACT_ABORT OFF;
If you do not use TRY-CATCH block in a specific situations, like the next one, the first statement will be again committed:
DROP TABLE IF EXISTS [dbo].[StackOverflow];
CREATE TABLE [dbo].[StackOverflow]
(
[StackID] TINYINT PRIMARY KEY
);
SET XACT_ABORT ON;
BEGIN TRANSACTION;
INSERT INTO [dbo].[StackOverflow] ([StackID])
VALUES (105);
EXEC
(
'INSERrrrrrT INTO [dbo].[StackOverflow] ([StackID]) VALUES (106);'
)
COMMIT TRANSACTION;
SET XACT_ABORT OFF;
SELECT [StackID]
FROM [dbo].[StackOverflow];
You need to add SET XACT_ABORT ON; and/or use TRY/CATCH so that the script doesn't continue to the COMMIT after an error.
SET XACT_ABORT ON example:
SET XACT_ABORT ON;
begin transaction
insert into instructor
values ('99999', 'Yellow', 'Biology', 700000)
insert into instructor
values ('99999', 'Blue', 'Statistics', 85000)
commit;
TRY/CATCH example:
BEGIN TRY
begin transaction
insert into instructor
values ('99999', 'Yellow', 'Biology', 700000)
insert into instructor
values ('99999', 'Blue', 'Statistics', 85000)
commit;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
Although SET XACT_ABORT ON is optional with this TRY/CATCH pattern, I still recommend it in all T-SQL code with explict transactions. This will ensure the transaction is rolled back immediately even when batch is cancelled manually or due to a query timeout where no CATCH block code executes. Otherwise, the uncommitted transaction will remain open until the connection is closed or reused with connection pooling.
Related
I have SQL Server table with application and push the data to oracle table (3rd party) using link-server. I want to set an error handler if insertion is not successful the delete query will not be executed. Here's my query run in SQL Server Agent every 24 hours.
DELETE FROM oracle_tbl
--If insert into is not successful then rollback else commit--
INSERT INTO oracle_tbl
SELECT*
FROM
sqlserver_tbl
Here's the outline of a stored procedure that will demonstrate the behavior you're looking for. If the insert is not succcessful, i.e. 0 are rows are inserted, then an exception is thrown which will trigger the CATCH BLOCK which contains the rollback statement. If the INSERT is successful then the DELETE statement executes.
drop proc if exists dbo.stored_procedure_name;
go
create proc dbo.stored_procedure_name
#input nvarchar(max)=null,
#test_id bigint output,
#response nvarchar(max) output
as
set nocount on;
set xact_abort on;
begin transaction
begin try
declare
#o_id bigint,
#o_count bigint;
/* attempt to insert into table */
INSERT INTO oracle_tbl
SELECT *
FROM
sqlserver_tbl;
select #o_count=rowcount_big();
select #o_id=cast(scope_identity() as bigint);
/* if the insert failed, then throw an exception which rollback the transaction */
if #o_count<>1
throw 50000, 'No rows inserted', 1;
/* delete from table */
DELETE FROM oracle_tbl;
select
#test_id=#o_id,
#response=(select N'Ok' reply_message, #o_id o_id for json path, without_array_wrapper);
commit transaction;
end try
begin catch
select
#test_id=cast(0 as bigint),
#response=error_message() for json path, without_array_wrapper);
rollback transaction;
end catch
go
In a procedure, I want to make a test then Raiserror when it's actually the case. But before that, I want to log the error in a table. My code is like this
CREATE PROCEDURE proc
#val VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;
DECLARE #test VARCHAR(50)
SELECT #test = test
FROM test_table
WHERE ...
IF #test IS NULL
BEGIN
INSERT INTO log_table VALUES (#val);
RAISERROR ('Invalid value : %i', 16, 1, #val);
END
END
The code compiles. When executed with a bad value, the error is raised, but the insert is cancelled.
I tried turning xact_abort and nocount on and off but had no luck.
I tried encapsulating the insert request in BEGIN TRANSACTION/COMMIT but still get the same result.
What I noticed, my log_table which has an auto-increment id, gets incremented even when those inserts are being cancelled.
How can I raise and error but still persist the insert request?
Thanks
Consider using THROW instead:
CREATE TABLE dbo.log_table (val varchar(50));
GO
CREATE PROCEDURE dbo.[proc] #val varchar(50)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;
DECLARE #test varchar(50); --As i never set this, it'll go into the IF
IF (#test IS NULL)
BEGIN
INSERT INTO log_table
VALUES (#val);
THROW 51000, N'Invalid value.', 1;
END;
END;
GO
EXEC dbo.[proc] #val = 'Some Value';
GO
SELECT *
FROM dbo.log_table;
GO
DROP PROC dbo.[proc];
DROP TABLE dbo.log_table;
DB<>Fiddle
In order to write to a log table you have to rollback any pending transaction. Otherwise your log table INSERT may be rolled back by the calling code, or may fail because the transaction is doomed.
So something like:
CREATE Procedure myproc
#val varchar(50)
as
begin
set nocount on
set xact_abort on
begin transaction;
begin try
-- do stuff
commit transaction;
end try
begin catch
if ##trancount > 0 rollback;
declare #error_message varchar(max) = error_message()
INSERT INTO log_table values (#val);
throw;
end catch
end
So apparently, my procedure was working as expected in SQLServer side. The problem was that I was calling this procedure from Java/Spring native query method and had to be annotated with #Modifying and #Transactional since it's doing insertions. Thus when an exception is caught, it was automatically rolled back.
I didn't find a quick solution to bypass Spring's transaction. Now I think all I have to do is, catch the exception in App layer and log to the log_table in app layer too
Perhaps I am missing something, but even though the RAISERRORs below have severity of 16 (as per documentation) the transaction is still committed as if XACT_ABORT ON has no effect.
CREATE PROCEDURE [ExploringGroups].[RemoveMember]
#groupId uniqueidentifier,
#adminUsername nvarchar(50),
#targetUsername nvarchar(50)
AS
SET XACT_ABORT ON
BEGIN TRANSACTION
DECLARE
#adminUserId uniqueidentifier = dbo.fn_userId(#adminUsername),
#targetUserId uniqueidentifier = dbo.fn_userId(#targetUsername)
IF #targetUserId IS NULL OR ExploringGroups.IsMember(#groupId, #targetUserId) = 0
RAISERROR('Target user was not located', 16, 1)
IF ExploringGroups.IsInRole(#groupId, #adminUserId, 'adm') = 0
RAISERROR('Specified user is not an administrator of this group', 16, 2)
IF #adminUserId = #targetUserId
RAISERROR('You cannot remove yourself', 16, 3)
-- statements below still execute and commit even though there was an error raised above
DELETE FROM ExploringGroups.MemberRole WHERE GroupId = #groupId AND UserId = #targetUserId
DELETE FROM ExploringGroups.Membership WHERE GroupId = #groupId AND UserId = #targetUserId
COMMIT
RETURN 0
Calling
exec exploringgroups.removemember '356048C5-BAB3-45C9-BE3C-A7227225DFDD', 'Crypton', 'Crypton'
Produces
Msg 50000, Level 16, State 2, Procedure RemoveMember, Line 20
Specified user is not an administrator of this group
Msg 50000, Level 16, State 3, Procedure RemoveMember, Line 24
You cannot remove yourself
I thought XACT_ABORT was supposed to roll back the whole transaction if it's set to ON?
Actually, it behaves exactly as it was supposed to. XACT_ABORT really caused the transaction to roll back, so were there any data modifications up to the point of the error they will be rolled back. However, it didn't affect the flow of execution, and it didn't stop running the stored procedure, so the following two DELETEs were executed as implicite transactions. Explicit RAISERRORs don't abort the batch.
See this simplified version:
create table #t(i int);
insert #t values(1);
go
alter procedure sp
as
set xact_abort on
begin tran
raiserror ('x', 16, 1);
print 'deleting';
delete #t;
commit;
go
exec sp
go
select * from #t
go
The only funny thing was that the error about COMMIT not having a corresponding BEGIN TRAN was swallowed.
With SEH, it would jump into CATCH block.
You should be using a "THROW" statement versus the RAISERROR approach.
Use a BEGIN TRY and BEGIN CATCH and commit the transaction normally or rollback in the CATCH block.
BEGIN TRY
-- Do your inserts or throw error
-- Commit transaction
END TRY
BEGIN CATCH
-- Rollback
END CATCH;
Is there a way to abort a stored procedure in SQL Server if a delete statement cannot be successfully executed due to a foreign key constraint violation? By default, the procedure seems to ignore the error and go on to the next statement.
For other types of error (e.g. deleting from a table that does not exist) however, the procedure aborts.
Example procedure:
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[TestSP]
AS
BEGIN
SET NOCOUNT ON;
print 'BEFORE';
DELETE FROM ExistingWithConstraints;
print 'AFTER DELETE ExistingWithConstraints';
DELETE FROM NonExisting;
print 'AFTER DELETE NonExisting';
END
produces output (note that the last message above is not printed):
BEFORE
<snip constraint violation error message>
AFTER DELETE ExistingWithConstraints
<snip invalid object name error message>
Use a transaction and proper error handling
CREATE PROCEDURE [dbo].[TestSP]
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
DELETE FROM ExistingWithConstraints;
DELETE FROM NonExisting;
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc]
END CATCH
GO
This is based on my answer here: Nested stored procedures containing TRY CATCH ROLLBACK pattern?
The execution carries on to allow for you checking the ##ERROR standard variable for problems. A more modern way to do things though it to use the TRY..CATCH blocks added to SQL in 2005:
http://msdn.microsoft.com/en-us/library/ms179296.aspx
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.