Is this good writen transaction in stored procedure - sql-server

This is the first time that I use transactions and I just wonder am I make this right. Should I change something?
I insert post(wisp). When insert post I need to generate ID in commentableEntity table and insert that ID in wisp table.
ALTER PROCEDURE [dbo].[sp_CreateWisp]
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding new wisp.', 16, 1)
RETURN
END
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding commentable entity.', 16, 1)
RETURN
END
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding wisp commentable entity id.', 16, 1)
RETURN
END
COMMIT
Using try/catch based on #gbn answer:
ALTER PROCEDURE [dbo].[sp_CreateWisp]
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR ('Error in adding new wisp', 16, 1)
END CATCH
GO

You'd use TRY/CATCH since SQL Server 2005+
Your rollback goes into the CATCH block but your code looks good otherwise (using SCOPE_IDENTITY() etc). I'd also use SET XACT_ABORT, NOCOUNT ON
This is my template: Nested stored procedures containing TRY CATCH ROLLBACK pattern?
Edit:
This allows for nested transactions as per DeveloperX's answer
This template also allows for higher level transactions as per Randy's comment

i think its not good all the time ,but if you want to use more than one stored procedure same time its not good be cause each stored procedure handles the transaction independently
but in this case,you should use try catch block , for exception handling , and preventing keeping transaction open on when an exception raising

I've never considered it a good idea to put transactions in a stored procedure. I think it's much better to start a transaction at a higher level so that can better coordinate multiple database (e.g. stored procedure) calls and treat them all as a single transaction.

Related

Stored procedure commit after DELETE and UPDATE run successfully

This is my stored procedure
ALTER Proc [dbo].[DeleteQualityAssemblyProduction]
#id int,
#Quantity int,
#idPartShip int,
#FK_idNextProcess int
AS
DELETE FROM [dbo].DailyQualityAssemblyProduction
WHERE id=#id
if #FK_idNextProcess=11
Begin
UPDATE [dbo].[ProjectShipping]
SET
QualityAssemblyQty = QualityAssemblyQty- #Quantity
WHERE id=#idPartShip
End
I want when both DELETE and UPDATE run successfully COMMIT the changes otherwise ROLLBACK .
I was wondering if adding COMMIT in the end of stored procedure do the job or I need other method
Here is one way you could tackle this. This is adding a transaction which you will need to handle multiple DML statements in one autonomous block. Then added a try/catch so that if either statement fails the transaction will deal with both statements as one unit of work.
ALTER Proc [dbo].[DeleteQualityAssemblyProduction]
(
#id int,
#Quantity int,
#idPartShip int,
#FK_idNextProcess int
) AS
set nocount on;
begin transaction
begin try
DELETE FROM [dbo].DailyQualityAssemblyProduction
WHERE id = #id
if #FK_idNextProcess = 11
begin
UPDATE [dbo].[ProjectShipping]
SET QualityAssemblyQty = QualityAssemblyQty - #Quantity
WHERE id = #idPartShip
end
commit transaction
end try
begin catch
rollback transaction
declare #error int
, #message varchar(4000);
select #error = ERROR_NUMBER()
, #message = ERROR_MESSAGE()
raiserror ('DeleteQualityAssemblyProduction: %d: %s', 16, 1, #error, #message) ;
end catch

Exit Gracefully from Stored Procedure and avoid BEGIN/COMMIT mismatch

After having some trouble trapping SQL errors in my VBA application, I redesigned my stored procedures so that if an error occurs, the return value is the error code and an output variable contains the error message. I do not re-throw the error in my catch blocks. I'll call this a "graceful exit" for lack of a better term. It has made things easier on the client-side, but now I have an issue when a trigger fired by a nested stored procedure rolls back a transaction.
Take the below example. TEST_INNER_PROC begins with a ##TRANCOUNT of 1, performs an insert which fires the trigger, which rolls back the transaction, and when TEST_INNER_PROC exits it throws error
266: Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements
Normally, I would pattern both of these procedures the same; I've simplified them here. The inner procedure does not attempt to start a transaction (it wouldn't make a difference), and the outer procedure does re-throw the error, just so that I can see the error information printed. Normally, I would return the error code to the client via the return code and #ERR_MSG output variable.
I like #gbn's pattern here: Nested stored procedures containing TRY CATCH ROLLBACK pattern? However, It does not appear to accommodate my "graceful exit" if the rollback happens in a trigger. I'm also not sure if Rusanu's pattern
would accommodate it either.
CREATE TABLE TEST (
COL1 INT
)
GO
CREATE TRIGGER TEST_TRIGGER
ON TEST FOR INSERT
AS
BEGIN TRY
THROW 50001, 'TEST Trigger produced an error.', 1
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE()<>0
ROLLBACK TRAN;
THROW
END CATCH
GO
CREATE PROC TEST_INNER_PROC
AS
SET NOCOUNT, XACT_ABORT ON
DECLARE #RTN INT = 0
BEGIN TRY
INSERT TEST (COL1) VALUES (1)
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE()<>0
ROLLBACK TRAN
SET #RTN = ERROR_NUMBER();
--THROW
END CATCH
RETURN #RTN
GO
CREATE PROC TEST_OUTER_PROC
AS
SET NOCOUNT, XACT_ABORT ON
DECLARE #RTN INT = 0
BEGIN TRY
BEGIN TRAN
EXEC #RTN = TEST_INNER_PROC
IF #RTN <> 0 THROW 50000, 'Execution of TEST_INNER_PROC produced an error.', 1
COMMIT TRAN
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 AND XACT_STATE()<>0
ROLLBACK TRAN;
THROW
END CATCH
GO
EXEC TEST_OUTER_PROC
GO
DROP TABLE TEST
DROP PROC TEST_OUTER_PROC
DROP PROC TEST_INNER_PROC
GO
The above code results in:
Msg 266, Level 16, State 2, Procedure TEST_INNER_PROC, Line 63
Transaction count after EXECUTE indicates a mismatching number of BEGIN
and COMMIT statements. Previous count = 1, current count = 0.
But if you uncomment the "THROW" statement in TEST_INNER_PROC, it throws:
Msg 50001, Level 16, State 1, Procedure TEST_TRIGGER, Line 69
TEST Trigger produced an error.
which is the error I want to handle in TEST_OUTER_PROC.
Is it possible to use stored procedures that "exit gracefully", returning the error code and error message as variables, and avoid the mismatching number of BEGIN and COMMIT statement?
You can store all the errors in a table and select them from it
CREATE TABLE LOG_ERROR (
SPID INT DEFAULT ##SPID
,DATE DATETIME DEFAULT GETDATE()
,ERROR_NUMBER INT DEFAULT ERROR_NUMBER())
In your procedures:
begin try
--code...
end try
begin catch
INSERT LOG_ERROR DEFAULT VALUES;
throw
end catch
After the procedure execution:
SELECT * FROM LOG_ERROR
WHERE SPID = ##SPID
AND DATE > CONVERT(DATE,GETDATE()) --Errors from today
Update: Create a view so you get the text too:
CREATE VIEW VW_LOG_ERROR AS
SELECT
E.*
,M.TEXT
FROM LOG_ERROR E
JOIN SYS.MESSAGES M WITH(NOLOCK) ON E.ERROR_NUMBER = M.MESSAGE_ID
JOIN SYS.SYSLANGUAGES L
ON M.LANGUAGE_ID = L.MSGLANGID
AND L.LANGID = ##LANGID
WHERE
SPID = ##SPID
AND DATE > CONVERT(DATE,GETDATE()) --Errors from today

Insertion transaction- rollback SQL

I am new to databases and i try to create a stored procedure that inserts data in tables that are in a many to many relation.If any part of the operation fails then it must try to recover as much as possible from the entire operation. For example, if one wants to create a record regarding publishers and books and succeeds creating the publisher but fails with the book, then it should roll back
the creation of the book, but not of the publisher.
My code looks like this:
BEGIN TRY
BEGIN TRANSACTION
DECLARE #serviciuKey int
DECLARE #specializareKey int
IF NOT EXISTS (SELECT denumire, moneda, pret FROM Serviciu where denumire=#denumire and moneda=#moneda and pret=#pret)
BEGIN
INSERT INTO Serviciu ( denumire, moneda, pret)
VALUES (#denumire, #moneda, #pret)
END
SET #serviciuKey=##IDENTITY
SAVE TRANSACTION savepoint
IF NOT EXISTS (SELECT denumire, descriere FROM Specializare where denumire=#denumire_spec AND descriere=#descriere)
BEGIN
INSERT INTO Specializare( denumire, descriere)
VALUES (#denumire_spec, #descriere)
END
SET #specializareKey=##IDENTITY
SAVE TRANSACTION savepoint
IF NOT EXISTS (SELECT * FROM Specializare_Serviciu where cod_specializare=#specializareKey and cod_serviciu=#serviciuKey)
BEGIN
INSERT INTO Specializare_Serviciu( cod_specializare, cod_serviciu)
VALUES (#specializareKey, #serviciuKey)
END
SAVE TRANSACTION savepoint
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##trancount > 0 ROLLBACK TRANSACTION savepoint
DECLARE #msg nvarchar(2048) = error_message()
RAISERROR (#msg, 16, 1)
RETURN 55555
END CATCH
When i execute the procedure, i have this error:
Msg 3931, Level 16, State 1, Procedure AddData0, Line 76
The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction.
Also when i try to insert some data that already exists, it is inserted with another ID, which means that IF NOT EXIST statement is not working.
Any help, please?
Try this version:
DECLARE #serviciuKey int
DECLARE #specializareKey int
BEGIN TRY
BEGIN TRAN
IF NOT EXISTS (SELECT denumire, moneda, pret FROM Serviciu where denumire=#denumire and moneda=#moneda and pret=#pret)
BEGIN
INSERT INTO Serviciu ( denumire, moneda, pret)
VALUES (#denumire, #moneda, #pret)
END
SET #serviciuKey=scope_identity()
IF NOT EXISTS (SELECT denumire, descriere FROM Specializare where denumire=#denumire_spec AND descriere=#descriere)
BEGIN
INSERT INTO Specializare( denumire, descriere)
VALUES (#denumire_spec, #descriere)
END
SET #specializareKey=scope_identity()
IF NOT EXISTS (SELECT * FROM Specializare_Serviciu where cod_specializare=#specializareKey and cod_serviciu=#serviciuKey)
BEGIN
INSERT INTO Specializare_Serviciu( cod_specializare, cod_serviciu)
VALUES (#specializareKey, #serviciuKey)
END
COMMIT TRAN
END TRY
BEGIN CATCH
IF ##trancount > 0
ROLLBACK TRAN
DECLARE #msg nvarchar(2048) = error_message()
RAISERROR (#msg, 16, 1)
RETURN 55555
END CATCH

Transaction is rolled back after commit?

I'm experiencing some problems that look a LOT like a transaction in a stored procedure has been rolled back, even though I'm fairly certain that it was committed, since the output variable isn't set until after the commit, and the user gets the value of the output variable (I know, because they print it out and I also set up a log table where i input the value of the output variable).
In theory someone COULD manually delete and update the data such that it would look like a rollback, but it is extremely unlikely.
So, I'm hoping someone can spot some kind of structural mistake in my stored procedure. Meet BOB:
CREATE procedure [dbo].[BOB] (#output_id int OUTPUT, #output_msg varchar(255) OUTPUT)
as
BEGIN
SET NOCOUNT ON
DECLARE #id int
DECLARE #record_id int
SET #output_id = 1
-- some preliminary if-statements that doesn't alter any data, but might do a RETURN
SET XACT_ABORT ON
BEGIN TRANSACTION
BEGIN TRY
--insert into table A
SET #id = SCOPE_IDENTITY()
--update table B
DECLARE csr cursor local FOR
SELECT [some stuff] and record_id
FROM temp_table_that_is_not_actually_a_temporary_table
open csr
fetch next from csr into [some variables], #record_id
while ##fetch_status=0
begin
--check type of item + if valid
IF (something)
BEGIN
SET SOME VARIABLE
END
ELSE
BEGIN
ROLLBACK TRANSACTION
SET #output_msg = 'item does not exist'
SET #output_id = 0
RETURN
END
--update table C
--update table D
--insert into table E
--execute some other stored procedure (without transactions)
if (something)
begin
--insert into table F
--update table C again
end
DELETE FROM temp_table_that_is_not_actually_a_temporary_table WHERE record_id=#record_id
fetch next from csr into [some variables], #record_id
end
close csr
deallocate csr
COMMIT TRANSACTION
SET #output_msg = 'ok'
SET #output_id = #id
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SET #output_msg = 'transaction failed !'
SET #output_id = 0
INSERT INTO errors (record_time, sp_name, sp_msg, error_msg)
VALUES (getdate(), 'BOB', #output_msg, error_message())
END CATCH
RETURN
END
I know, my user gets an #output_id that is the SCOPE_IDENTITY() and he also gets an #output_msg that says 'ok'. Is there ANY way he can get those outputs without the transaction getting committed?
Thank you.
You know the problem is that transaction dose NOT support rollback on variables because there is no data change inside database. Either commit or rollback of the transactions ONLY make difference on those database objects (tables, temp table, etc.), NOT THE VARIABLES (including table variables).
--EDIT
declare #v1 int = 0, #v2 int = 0, #v3 int = 0
set #v2 = 1
begin tran
set #v1 = 1
commit tran
begin tran
set #v3 = 1
rollback tran
select #v1 as v1, #v2 as v2, #v3 as v3
RESULT is as follows
Personally I never used transactions in stored procedures, especially when they are used simultaniously by many people. I seriously avoid cursors as well.
I think I would go with passing the involved rows of temp_table_that_is_not_actually_a_temporary_table into a real temp table and then go with an if statement for all rows together. That's so simple in tsql:
select (data) into #temp from (normal_table) where (conditions).
What's the point of checking each row, doing the job and then rollback the whole thing if say the last row doesn't meet the condition? Do the check for all of them at once, do the job for all of them at once. That's what sql is all about.

Rollback the inner transaction of nested transaction

suppose I have following sql statement in sql server 2008:
BEGIN TRANSACTION
SqlStatement1
EXEC sp1
SqlStatement3
COMMIT TRANSACTION
The code of sp1
BEGIN TRANSACTION
SqlStatement2
ROLLBACK TRANSACTION
My question is: Is SqlStatement3 actually executed?
SQL Server doesn't really support nested transactions. There is only one transaction at a time.
This one transaction has a basic nested transaction counter, ##TRANCOUNT. Each consecutive begin transaction increments the counter by one, each commit transaction reduces it by one. Only the commit that reduces the counter to 0 really commits the one transaction.
A rollback transaction undoes the one transaction and clears ##TRANCOUNT.
In your case, the funny result is that SqlStatement3 is run outside a transaction! Your final commit will throw an "The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION" exception, but the effects of SqlStatement3 are permanent.
For example:
create table #t (col1 int)
insert #t (col1) values (1)
BEGIN TRANSACTION
update #t set col1 = 2 -- This gets rolled back
BEGIN TRANSACTION
update #t set col1 = 3 -- This gets rolled back too
ROLLBACK TRANSACTION
update #t set col1 = 4 -- This is run OUTSIDE a transaction!
COMMIT TRANSACTION -- Throws error
select col1 from #t
Prints 4. Really. :)
You can use transaction savepoints. sp1 can use a pattern like the one described in Error Handling and Nested 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
Such a pattern allow for the work done in sp1 to rollback, but keep the encompassing transaction active.
Nested transactions can be used. To only rollback the inner transaction, use a savepoint and rollback to the savepoint. In case the inner transaction does not whether it is nested or not, IF statements can be used to find out whether to set a savepoint and whether to rollback or to rollback to a savepoint:
BEGIN TRAN
DECLARE #WILL_BE_NESTED_TRANSACTION BIT = CASE WHEN (##TRANCOUNT > 0) THEN 1 ELSE 0 END
IF #WILL_BE_NESTED_TRANSACTION = 1
SAVE TRAN tran_save
BEGIN TRAN
-- do stuff
IF #WILL_BE_NESTED_TRANSACTION = 1
ROLLBACK TRAN tran_save
ELSE
ROLLBACK
ROLLBACK
Rollback transaction on its own rolls back all transactions.
http://msdn.microsoft.com/en-us/library/ms181299(v=sql.100).aspx
The statement will still be executed - try this
create table #t (i int)
insert #t values (1) -- t contains (1)
begin tran
update #t set i = i +1
select * from #t -- t contains (2)
begin tran
update #t set i = i +1
select * from #t -- t contains (3)
rollback tran -- transaction is rolled back
select * from #t -- t contains (1)
update #t set i = i +1
select * from #t -- t contains (2)
commit -- error occurs
select * from #t -- t contains (2)
drop table #t
Committing inner transactions is ignored by the SQL Server Database Engine. The transaction is either committed or rolled back based on the action taken at the end of the outermost transaction. If the outer transaction is committed, the inner nested transactions are also committed. If the outer transaction is rolled back, then all inner transactions are also rolled back, regardless of whether or not the inner transactions were individually committed.
Nesting Transactions in Microsoft TechNet

Resources