How to commit nested stored procedure, when general is raiserror - sql

I have one stored procedure proc_in which the insert data to tbl table
create table tbl(id int identity, val nvarchar(50))
create procedure proc_in
as
begin
insert into tbl(val)
values ('test')
end
and I have proc_out where I call proc_in
create procedure proc_out
as
begin
exec proc_in
DECLARE #MessageText NVARCHAR(100);
SET #MessageText = N'This is a raiserror %s';
RAISERROR(#MessageText, 16, 1, N'MSG')
end
How I can write proc_out that it return raiserror always to do insert in TBL table.
I calling proc_out like this
begin tran
declare #err int = 0
exec #err = proc_out
if #ERR = 0
commit tran
else
rollback tran

You are wrapping your call in a single transaction in the calling context, therefore:
begin tran
declare #err int = 0
exec #err = proc_out
if #ERR = 0
commit tran
else
rollback tran
will always roll back everything that has happened within that transaction.
One way to avoid this is to move the transaction inside your 'proc_out' SP e.g.
create procedure proc_out
as
begin
set nocount, xact_abort on;
exec proc_in;
begin tran;
-- All your other code
if #Err = 1 begin
rollback;
declare #MessageText nvarchar(100);
set #MessageText = N'This is a raiserror %s';
--raiserror(#MessageText, 16, 1, N'MSG');
-- Actually for most cases now its recommended to use throw
throw 51000, #MessageText 1;
end; else begin
commit;
end;
return 0;
end;
Alternatively, and I haven't tried this, you could try using a savepoint e.g.
create procedure proc_out
as
begin
set nocount on;
exec proc_in;
save transaction SavePoint1;
declare #MessageText nvarchar(100);
set #MessageText = N'This is a raiserror %s';
raiserror(#MessageText, 16, 1, N'MSG');
return 0;
end;
Then call it as:
begin tran;
declare #err int = 0;
exec #err = proc_out;
if #ERR = 0;
commit tran;
end; else begin
rollback tran SavePoint1;
commit tran;
end;
I don't like this approach though, because knowledge of the inner workings of your SP has now leaked out to the calling context.
And some errors will roll back the entire transaction regardless.
Its important to be aware of the XACT_ABORT setting here.
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. Depending upon the severity of the error, the entire transaction may be rolled back even when SET XACT_ABORT is OFF. OFF is the default setting in a T-SQL statement, while ON is the default setting in a trigger.

Related

Nested transactions and ##trancount count (issue with the practice question for 70-761)

Got a practice question from Measure up and not sure if it is badly worded or I'm missing something regarding nested transactions.
Basically gives me a definition of a stored procedure and states
When the sp is run, what is the value of ##trancount?
I get that SQL Server only cares about the outer transaction but ##trancount should be 0 since everything is committed and if it fails everything is rolled back which would still be 0 but it is telling me it should be 1.
It doesn't specify in the code where the ##trancount is run but the wording suggest it is run after the sp is executed.
I ran the sp with some dummy data with ##trancount at the end and got 0.
Create Procedure dbo.up_CreateSalesInvoice
(
#Date date,
#customerID int,
#stockItemID Int,
#quantity int,
#unitPrice decimal(8,2),
#invoiceID int out
)
As
Begin
Declare #retval int;
Begin Transaction;
Begin Try
Begin Transaction;
Insert into dbo.SalesInvoice (invoiceDate, CustomerID)
Values (#date, #customerID);
Set #invoiceID = Scope_identity();
Commit Transaction;
Begin Transaction;
Insert into dbo.SalesInvoiceLine (InvoiceID, StockItemID, Quantity,
UnitPrice)
Values (#InvoiceID, #stockItemID, #quantity, #UnitPrice);
Commit Transaction;
Commit transaction;
set #retval = 0;
End try
Begin catch
Rollback Transaction;
Set #retval = 1;
End catch
Return #retval;
End ;
Expect ##trancount to be 0 as there are no open transactions for it to count.

Create SQL Server procedure in a transaction

I need to create two procedures in a SQL Server transaction. If failure, I need to rollback the create(s) and any other executed queries in this transaction. I know the create statement must be the first statement in query batch, but I need to know how handle the transaction with multiple batches.
BEGIN TRANSACTION
CREATE PROCEDURE [dbo].[SP_SP-1]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SQL statements
END
GO
CREATE PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 statements
END
GO
UPDATE Table
SET Value = '1.0.0.5'
COMMIT TRANSACTION / ROLLBACK TRANSACTION
Below is one method to execute multiple batches in a transaction. This uses a temp table to indicate if any batch erred and perform a final COMMIT or ROLLLBACK accordingly.
Another method is to encapsulate statements that must be in single-statement batch (CREATE PROCEDURE, CREATE VIEW, etc.) but that can get rather ugly when quotes within the literal text must be escaped.
CREATE TABLE #errors (error varchar(5));
GO
BEGIN TRANSACTION
GO
CREATE PROCEDURE [dbo].[USP_SP-1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
-- SP Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
CREATE PROCEDURE [dbo].[USP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
UPDATE Table SET Value='1.0.0.5'
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
IF EXISTS(SELECT 1 FROM #errors)
BEGIN
IF ##TRANCOUNT > 0 ROLLBACK;
END
ELSE
BEGIN
IF ##TRANCOUNT > 0 COMMIT;
END;
GO
IF OBJECT_ID(N'tempdb..#errors', 'U') IS NOT NULL
DROP TABLE #errors;
GO
I suggest you to study more about this subject in Handling Transactions in Nested SQL Server Stored Procedures.
From the beginning, your syntax is wrong. You cannot begin a transaction and then create a procedure, you need to do just the opposite:
CREATE PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
SET NOCOUNT ON;
-- SP-2 Statments
Update Table set Value='1.0.0.5'
END TRY
BEGIN CATCH
--handle error and perform rollback
ROLLBACK
SELECT ERROR_NUMBER() AS ErrorNumber
SELECT ERROR_MESSAGE() AS ErrorMessage
END CATCH
END
It is best practice to use TRY and CATCH when attempting to perform update inside transaction scope.
Please read more and investigate using the link I provided to get a bigger picture.
To use Transaction, you need to know what is the meaning of transaction. It's meaning of 'Unit of work either in commit state or rollback state'.
So when you use transaction, you must know that where you declare and where you close. So you must start and end transaction in the parent procedure only than it will work as a unit of work i.e. whatever no of query execute of DML statement, it uses the same transaction.
I do not understand why your update statement outside of procedure and transaction portion too.
It should be (See my comments, you can use TRY Catch same as c sharp) :
Create PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
Begin Transaction
SET NOCOUNT ON;
-- SP Statments
Exec SP_SP-2 #id --here you can pass the parameter to another procedure, but do not use transaction in another procedure, other wise it will create another transaction
If ##Error > 0 than
Rollback
Else
Commit
End
END
GO
--Do not use transaction in another procedure, otherwise, it will create another transaction which has own rollback and commit and do not participate in the parent transaction
Create PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END
GO
i find this solution to execute the procedure as string execution , it`s a workaround to execute what i want
Begin Try
Begin Transaction
EXEC ('
Create PROCEDURE [dbo].[SP_1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
SP-1
END
GO
Create PROCEDURE [dbo].[SP_Inc_Discovery_RunDoc]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
Sp-2
END')
Update Table set Value='1.0.0.5'
Commit
End Try
Begin Catch
Rollback
Declare #Msg nvarchar(max)
Select #Msg=Error_Message();
RaisError('Error Occured: %s', 20, 101,#Msg) With Log;
End Catch

Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Commit in wrong place?

Here is a shell of my stored procedure with the necessary parts not omitted:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE procedure --name of sproc
--declare sproc params
AS
BEGIN
SET XACT_ABORT ON
SET NOCOUNT ON
BEGIN TRY
BEGIN TRANSACTION
--declare a few vars
--declare some table variables
--do some work
IF (--some condition here)
BEGIN
--actually do the work
END
ELSE
BEGIN
ROLLBACK TRANSACTION
SET #error = 'some value cannot be NULL'
RAISERROR(#error, 16, 1)
RETURN #error
END
COMMIT
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT #error = ERROR_NUMBER()
, #message = ERROR_MESSAGE()
, #severity = ERROR_SEVERITY()
, #state = ERROR_STATE()
RAISERROR(#message, #severity, #state)
RETURN #error
END CATCH
END
GO
I am getting a deadlock error (which is not the subject of this post) in the "--actually do some work" section, and then the "Transaction count..." error is thrown.
Is my COMMIT in the wrong place?
Move the Begin Transaction above the Begin Try. If the try fails and jumps to the catch, everything initialized in the try falls out of scope. Beginning the transaction outside the scope of the try/catch makes it available to both the try and the catch block.

T-SQL - Incorporating Try-Catch and Error-Handling (Transactions)

I am working on a Transaction using Repeatable Read Isolation Level.
I want to incorporate both Try-Catch and an Error-handler features in this Transaction.
When I run the code, I get an error message that :
Msg 102, Level 15, State 1, Line 18
Incorrect syntax near 'BEGIN'.
Msg 102, Level 15, State 1, Line 23
Incorrect syntax near '#errnum'.
How do I complete this Transaction successfully? OR
What is the correct way to write this Transaction?
This is my work as at now:
CREATE PROCEDURE ItemFlow (#Name VARCHAR(50),
#aPrice MONEY,
#bPrice MONEY)
AS
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
GO
BEGIN TRAN
IF EXISTS (SELECT 1
FROM Cashier
WHERE Item = '#Item')
UPDATE Cashier
SET bPrice = '#bPrice',
aprice = '#aprice'
WHERE Item = '#Item'
ELSE
INSERT INTO Cashier
(Item, aPrice, bPrice)
VALUES ('#Item', '#aPrice', '#bPrice')
END
BEGIN TRY
EXECUTE ItemFlow
END TRY
BEGIN CATCH
#errnum = ERROR_NUMBER(),
#severity = ERROR_SEVERITY(),
#errstate = ERROR_STATE(),
#proc = ERROR_PROCEDURE(),
#line = ERROR_LINE(),
#message = ERROR_MESSAGE()
END CATCH
One problem is the GO that terminates the create procedure statement. I always use begin/end with stored procedures:
CREATE PROCEDURE ItemFlow (#Name VARCHAR(50),
#aPrice MONEY,
#bPrice MONEY)
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRAN
IF EXISTS (SELECT 1
FROM Cashier
WHERE Item = '#Item')
UPDATE Cashier
SET bPrice = '#bPrice',
aprice = '#aprice'
WHERE Item = '#Item'
ELSE
INSERT INTO Cashier
(Item, aPrice, bPrice)
VALUES ('#Item', '#aPrice', '#bPrice')
COMMIT TRAN;
END; -- ItemFlow
Then, you need arguments when you call the stored procedure, or default values defined for them:
BEGIN TRY
EXECUTE ItemFlow #arg1, #arg2, #arg3
END TRY
BEGIN CATCH
#errnum = ERROR_NUMBER(),
#severity = ERROR_SEVERITY(),
#errstate = ERROR_STATE(),
#proc = ERROR_PROCEDURE(),
#line = ERROR_LINE(),
#message = ERROR_MESSAGE()
END CATCH;
Before implementing any of the suggested changes, please answer: 1) why you are calling the proc itself within the proc? and 2) why are you setting the ERROR_ functions into variables? Are you going to use them? If not, no need to declare variables.
Also:
Obviously the GO can't be a part of this. It is just a batch separator for SSMS.
You have an input param called #Name that is not used, and a variable called #Item that is not declared. I suspect they are the same thing so I have used #Item as the input param instead of #Name.
You use the input params as string literals in the three queries, which makes no sense. You need to remove the single-quotes from around them so that they can act as variables.
And, please do NOT separate the TRY / CATCH logic from the transaction!
Assuming that there was no real intention in calling an infinite loop (the proc calling itself with no condition to ever stop), your code should look as follows:
CREATE PROCEDURE ItemFlow
(
#Item VARCHAR(50),
#aPrice MONEY,
#bPrice MONEY
)
AS
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRY
BEGIN TRAN
IF EXISTS (SELECT 1
FROM Cashier
WHERE Item = #Item)
BEGIN
UPDATE Cashier
SET bPrice = #bPrice,
aprice = #aPrice
WHERE Item = #Item;
END;
ELSE
BEGIN
INSERT INTO Cashier
(Item, aPrice, bPrice)
VALUES (#Item, #aPrice, #bPrice);
END;
COMMIT TRAN;
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRAN;
END;
THROW;
END CATCH;
THROW was introduced in SQL Server 2012. If you are using anything from 2005 - 2008 R2, replace the THROW with:
DECLARE #ErrMessage NVARCHAR(4000);
SET #ErrMessage = ERROR_MESSAGE();
RAISERROR(#ErrMessage, 16, 1);
RETURN;
Your code has a few problems:
Error near BEGIN
GO is a batch separator. It's not valid T-SQL and only understood by SSMS. You are effectively submitting two queries:
CREATE PROCEDURE ItemFlow (#Name VARCHAR(50),
#aPrice MONEY,
#bPrice MONEY)
AS
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
and:
BEGIN TRAN
...
As you can see, the stored procedure body is empty. Remove GO to get rid of this.
Error near #errnum
Your variables are not declared. Also, you must use SELECT or SET to assign a value to these variables:
DECLARE #errnum int,
#serverity int,
(etc.)
SELECT #errnum = ERROR_NUMBER(),
#severity = ERROR_SEVERITY(),
#errstate = ERROR_STATE(),
#proc = ERROR_PROCEDURE(),
#line = ERROR_LINE(),
#message = ERROR_MESSAGE()
One last thing:
You have a BEGIN TRAN but not a COMMIT TRAN. Your transaction is still open by the end of the sproc's execution.

Managing synonyms in a proc with nested procs using sp_getapplock

Building on the topics previously addressed by gbn in these questions
Q1, Q2, Q3, Q4, and regarding the use of synonyms and re-creating the synonyms to keep the synonyms pointing at live data, it is not clear to me how prevent a "race condition" by using
"sp_getapplock after BEGIN TRAN in Transaction mode and trap/handle the return status as required."
MSDN documentation for sp_getapplock is a bit cryptic for me. For instance, can resource_name be any made-up string? But more to the point: if I run a single proc containing nested procs, where the first step is to build tables, and if those succeed, the next major step is to DROP and CREATE the existing synonyms, how would I properly implement sp_getapplock?
CREATE PROCEDURE [dbo].[some_old_proc]
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #nested_build_success varchar(3) = 'No'
DECLARE #starttrancount int
BEGIN TRY
SET #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
-- fill the tables that the synonyms don't point to yet...
EXEC dbo.nested_proc_1
EXEC dbo.nested_proc_2
EXEC dbo.nested_proc_3
EXEC dbo.nested_proc_4
EXEC dbo.nested_proc_5
IF #starttrancount = 0
BEGIN
COMMIT TRANSACTION
SET #nested_build_success = 'Yes'
END
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
-- RAISERROR... log error event
END CATCH
IF #nested_build_success = 'Yes'
BEGIN TRAN
-- simple talk article
-- http://www.simple-talk.com/sql/t-sql-programming/developing-modifications-that-survive-concurrency/
DECLARE #ret INT -- does it matter what the resource_name is?
EXEC #ret = sp_getapplock #Resource = 'DoesNameMatterHere', #LockMode = 'Exclusive';
IF #ret < 0
BEGIN
-- log error message?
END
ELSE
BEGIN
-- call the proc that a does a DROP and CREATE of the relevant synonyms
-- so the synonyms point at a different set of tables...
EXEC dbo.change_the_synonyms
END
COMMIT TRAN
Perhaps a different and better way exists to avoid a race condition than the use of sp_getapplock, or a good example of what I'm trying to do is available?
If I understand your question correctly the preparation steps and the synonym setup must be in a single transaction, not two separate transactions. Here is an example, based on the Exception handling and nested transactions template:
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;
EXEC sp_getapplock 'usp_my_procedure_name',
'Exclusive',
'TRANSACTION';
EXEC dbo.nested_proc_1;
EXEC dbo.nested_proc_2;
EXEC dbo.nested_proc_3;
EXEC dbo.nested_proc_4;
EXEC dbo.nested_proc_5;
EXEC dbo.change_the_synonyms;
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
This will do all the work atomically. It will use an applock to serialize access so that no two procedures execute this work concurrently. In case of error the work will either completely roll back or, in the case when the caller already has a transaction, it rolls back the work to a consistent state at the procedure entry w/o rolling back the caller (this is extremely useful in batch processing). XACT_ABORT has its use in deployment script, but mixing XACT_ABORT with TRY/CATCH is a big no-no in my book.