How to put CREATE TRIGGER into TRY-CATCH block with TRANSACTION? - sql-server

I'm not advanced programmer in SQL and maybe my question is silly, but I haven't found an answer in google. We have some SQL construction for implementing packages of changes:
...
BEGIN TRY
BEGIN TRANSACTION;
<User Code Is Here>
...
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
...
END CATCH;
...
How can I put the chain of CREATE TRIGGER blocks instead of <User Code Is Here> without errors:
-- Table1
CREATE TRIGGER trTable1_Dates ON dbo.Table1
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
...
SET NOCOUNT OFF;
END
GO
...
-- TableN
CREATE TRIGGER trTableN_Dates ON dbo.TableN
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
...
SET NOCOUNT OFF;
END
GO
The purpose is to create all triggers or nothing and print message in CATCH block of code if fails.
Edited
The errors are:
On first trigger's BEGIN: SQL80001: Incorrect syntax near 'BEGIN'. Expecting EXTERNAL.
After first trigger, on GO: SQL80001: Incorrect syntax near 'GO'.
END TRY: SQL80001: Incorrect syntax near 'TRY'. Expecting CONVERSATION.
END CATCH: SQL80001: Incorrect syntax near 'CATCH'. Expecting CONVERSATION.

You need to run each create trigger statement in a separate scope/batch (because it has to be the first statement in the batch). So you'll have to escape any quotes in the trigger definitions too:
BEGIN TRY
BEGIN TRANSACTION;
exec sp_executesql N'CREATE TRIGGER trTable1_Dates ON dbo.Table1
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
//An empty string in here has to be '''' to escape the quotes
SET NOCOUNT OFF;
END'
exec sp_executesql N'CREATE TRIGGER trTableN_Dates ON dbo.TableN
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
...
SET NOCOUNT OFF;
END'
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
...
END CATCH;
Transactions are orthogonal to batches and nested scopes, so the transaction covers all activity that occurs inside each EXEC too.

Related

SQL Server / T-SQL : Raiserror cancels prior inserts

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

SQL Server XACT_ABORT with exclusion

I have a larger stored procedure which utilizes several TRY/CATCH blocks in order to catch and log individual errors. I have also wrapped a transaction around the entire contents of the procedure, so as to be able to roll back the entire thing in the event of an error raised somewhere along the way (in order to prevent a lot of messy cleanup); XACT_ABORT has been enabled since it would otherwise not roll back the entire transaction.
Key component:
There is a table in my database which gets a record inserted each time this procedure is run with the results of operations and details on what went wrong.
Funny thing is happening - actually, when I finally figured out what was wrong, it was pretty obvious... the the insert statement into my log table is getting rolled back as well, hence, if I am not running this out of SSMS, I will not be able to see that this was even run, as the rollback removes all trances of activity.
Question:
Would it be possible to have the entire transaction roll back with the exception of this single insert statement? I would still want to preserve the error message which I compile during the running of the stored procedure.
Thanks so much!
~Eli
Update 6/28
Here's a code sample of what I'm looking at. Key difference between this and the samples posed by #Alex and #gameiswar is that in my case, the try/catch blocks are all nested inside the single transaction. The purpose of this is to have multiple catches (for the multiple tables), though we would the entire mess to be rolled back even if the last update failed.
SET XACT_ABORT ON;
BEGIN TRANSACTION
DECLARE #message AS VARCHAR(MAX) = '';
-- TABLE 1
BEGIN TRY
UPDATE TABLE xx
SET yy = zz
END TRY
BEGIN CATCH
SET #message = 'TABLE 1 '+ ERROR_MESSAGE();
INSERT INTO LOGTABLE
SELECT
GETDATE(),
#message
RETURN;
END CATCH
-- TABLE 2
BEGIN TRY
UPDATE TABLE sss
SET tt = xyz
END TRY
BEGIN CATCH
SET #message = 'TABLE 2 '+ ERROR_MESSAGE();
INSERT INTO LOGTABLE
SELECT
GETDATE(),
#message
RETURN;
END CATCH
COMMIT TRANSACTION
You can try something like below ,which ensures you log the operation.This takes advantage of the fact that table variables dont get rollbacked..
Psuedo code only to give you idea:
create table test1
(
id int primary key
)
create table logg
(
errmsg varchar(max)
)
declare #errmsg varchar(max)
set xact_abort on
begin try
begin tran
insert into test1
select 1
insert into test1
select 1
commit
end try
begin catch
set #errmsg=ERROR_MESSAGE()
select #errmsg as "in block"
if ##trancount>0
rollback tran
end catch
set xact_abort off
select #errmsg as "after block";
insert into logg
select #errmsg
select * from logg
OK... I was able to solve this using a combination of the great suggestions put forth by Alex and GameisWar, with the addition of the T-SQL GOTO control flow statement.
The basic ideas was to store the error message in a variable, which survives a rollback, then have the Catch send you to a FAILURE label which will do the following:
Rollback the transaction
Insert a record into the log table, using the data from the aforementioned variable
Exit the stored procedure
I also use a second GOTO statement to make sure that a successful run will skip over the FAILURE section and commit the transaction.
Below is a code snippet of what the test SQL looked like. It worked like a charm, and I have already implemented this and tested it (successfully) in our production environment.
I really appreciate all the help and input!
SET XACT_ABORT ON
DECLARE #MESSAGE VARCHAR(MAX) = '';
BEGIN TRANSACTION
BEGIN TRY
INSERT INTO TEST_TABLE VALUES ('TEST'); -- WORKS FINE
END TRY
BEGIN CATCH
SET #MESSAGE = 'ERROR - SECTION 1: ' + ERROR_MESSAGE();
GOTO FAILURE;
END CATCH
BEGIN TRY
INSERT INTO TEST_TABLE VALUES ('TEST2'); --WORKS FINE
INSERT INTO TEST_TABLE VALUES ('ANOTHER TEST'); -- ERRORS OUT, DATA WOULD BE TRUNCATED
END TRY
BEGIN CATCH
SET #MESSAGE = 'ERROR - SECTION 2: ' + ERROR_MESSAGE();
GOTO FAILURE;
END CATCH
GOTO SUCCESS;
FAILURE:
ROLLBACK
INSERT INTO LOGG SELECT #MESSAGE
RETURN;
SUCCESS:
COMMIT TRANSACTION
I don't know details but IMHO general logic can be like this.
--set XACT_ABORT ON --not include it
declare #result varchar(max) --collect details in case you need it
begin transaction
begin try
--your logic here
--if something wrong RAISERROR(...#result)
--everything OK
commit
end try
begin catch
--collect error_message() and other into #result
rollback
end catch
insert log(result) values (#result)

SET XACT_ABORT OFF if transaction terminated by SET XACT_ABORT ON

I read that many tend to SET XACT_ABORT ON at the beginning of the procedure.
CREATE PROC myProc
AS
Begin
BEGIN TRAN
SET XACT_ABORT ON
[..code1 that might throw an error..]
[..code2..]
SET XACT_ABORT OFF [?]
COMMIT TRAN
END
SET XACT_ABORT OFF [?]
Because run-time errors will terminate the procedure, SET XACT_ABORT will be left as ON. I have some questions :
Where do you set it on ? Before the CREATE PROC definition, after BEGIN or after BEGIN TRAN ? SET is at connection level so I suppose all three would not make a difference ?
When do you turn it OFF if it is left ON when errors occur ?
Setting XACT_ABORT before the CREATE PROC definition doesn't have much sense. Unlike ANSI_NULLS or QUOTED_IDENTIFIER, this option is not stored as a property of the stored procedure. I would say that you should set XACT_ABORT immediately after CREATE PROC myProc AS or after the first BEGIN, where you would also place the SET NOCOUNT ON.
I would not bother setting XACT_ABORT off. If any code portion needs to have it off (for example, inside a TRY-CATCH block that ignores some errors), it should set it off inside that specific TRY block, and set it back on at the end of END CATCH.
For more info, see: http://www.sommarskog.se/error_handling/Part1.html

Are these code snippets equivalent ('set xact_abort on' vs 'try catch rollback')?

I used to use this code snippet within my stored procedure in SQL Server:
create procedure proc_name
--declare variables
as
set nocount on
begin transaction
begin try
--do something
commit transaction
end try begin catch
rollback transaction
;throw
end catch
go
but today I got to know 'set xact_abort on' statement.
Is the following code equivalent to previous one? Are there any differences between them?
create procedure proc_name
--declare variables
as
set nocount on
set xact_abort on
begin transaction
--do something
commit transaction
go
Quoting from MS docs
A TRY…CATCH construct catches all execution errors that have a severity higher than 10 that do not close the database connection.
So, try catch does not catch all possible errors. You can use xact_abort on in addition to try catch.
try/catch give you more flexibility, i.e., you are not limited to just a rollback when something is not happy.

Aborting a stored procedure on foreign key constraint violations

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

Resources