Is the following the correct way to use transactions within a cursor:
SET CURSOR_CLOSE_ON_COMMIT ON;
DECLARE cur CURSOR LOCAL FOR
SELECT * FROM #ordersToProcess;
OPEN cur;
DECLARE #OrderId int;
FETCH NEXT FROM cur INTO #OrderId;
WHILE ##FETCH_STATUS = 0 BEGIN
BEGIN TRY
BEGIN TRAN;
EXEC process_order #OrderId;
COMMIT TRAN;
DEALLOCATE cur;
SET CURSOR_CLOSE_ON_COMMIT OFF;
END TRY
BEGIN CATCH
ROLLBACK TRAN;
DEALLOCATE cur;
SET CURSOR_CLOSE_ON_COMMIT OFF;
THROW;
END CATCH;
FETCH NEXT FROM cur INTO #OrderId;
END;
No. You have this code:
WHILE ##FETCH_STATUS = 0 BEGIN
BEGIN TRY
BEGIN TRAN;
EXEC process_order #OrderId;
COMMIT TRAN;
DEALLOCATE cur;
SET CURSOR_CLOSE_ON_COMMIT OFF;
END TRY
. . .
This runs one time through the loop, deallocates the cursor and then . . . well, you have a problem on the second time through the loop.
I think you intend to dealloc after the while loop.
Deallocate and Close after the cursor has finished:
DECLARE cur CURSOR LOCAL FOR
SELECT * FROM #ordersToProcess;
OPEN cur;
DECLARE #OrderId int;
FETCH NEXT FROM cur INTO #OrderId;
WHILE ##FETCH_STATUS = 0 BEGIN
BEGIN TRY
BEGIN TRAN;
EXEC process_order #OrderId;
COMMIT TRAN;
END TRY
BEGIN CATCH
ROLLBACK TRAN;
THROW;
END CATCH;
FETCH NEXT FROM cur INTO #OrderId;
END;
BEGIN TRY
CLOSE Cursor1
DEALLOCATE Cursor1
END TRY
BEGIN CATCH
--Do nothing
END CATCH
Related
I have added exception handling in my stored procedure as below.
ALTER PROCEDURE [dbo].[BUDGETUPDATE]
#DistrictID int
AS
BEGIN
SET NOCOUNT ON ;
BEGIN TRY
BEGIN TRAN
UPDATE bud
SET bud.BudgetStateID = #BudgetStateID
FROM [dbo].[BudgetOffice] bud
INNER JOIN [dbo].[vw_Office] vw
ON (vw.OfficeID = bud.OfficeID)
WHERE vw.DistrictID = #DistrictID
IF ##ERROR = 0
BEGIN
COMMIT TRAN;
SELECT ##ROWCOUNT AS AffectedRow;
END
END TRY
BEGIN CATCH
SELECT ##ERROR AS ERROR
ROLLBACK TRAN;
END CATCH
SET NOCOUNT OFF ;
END
I need to return the number of affected rows using ##ROWCOUNT. But this stored procedure always returns rowcount as 0. Any reason for this. Do I need to write the ##rowcount statement right after update?
You need to select ##ROWCOUNT after your UPDATE statement. As per the documentation:
Statements such as USE, SET , DEALLOCATE CURSOR, CLOSE CURSOR,
BEGIN TRANSACTION or COMMIT TRANSACTION reset the ROWCOUNT value to 0.
Since your ##ROWCOUNT is after the COMMIT TRAN, ##ROWCOUNT returns 0.
You need to store result of global variables in local variable because it will change after next instruction like:
ALTER PROCEDURE [dbo].[BUDGETUPDATE]
#DistrictID int
AS
BEGIN
SET NOCOUNT ON ;
DECLARE #rowcount INT, #error INT;
BEGIN TRY
BEGIN TRAN
UPDATE bud
SET bud.BudgetStateID = #BudgetStateID
FROM [dbo].[BudgetOffice] bud
JOIN [dbo].[vw_Office] vw
ON vw.OfficeID = bud.OfficeID
WHERE vw.DistrictID = #DistrictID;
SELECT #error = ##ERROR, #rowcount = ##ROWCOUNT;
IF #error = 0
BEGIN
COMMIT TRAN;
SELECT #rowcount AS AffectedRow;
END
END TRY
BEGIN CATCH
SELECT ##ERROR AS ERROR
ROLLBACK TRAN;
END CATCH
END
Or even better resign for using ##ERROR in TRY CATCH block:
ALTER PROCEDURE [dbo].[BUDGETUPDATE]
#DistrictID int
AS
BEGIN
SET NOCOUNT ON ;
BEGIN TRY
BEGIN TRAN
UPDATE bud
SET bud.BudgetStateID = #BudgetStateID
FROM [dbo].[BudgetOffice] bud
JOIN [dbo].[vw_Office] vw
ON vw.OfficeID = bud.OfficeID
WHERE vw.DistrictID = #DistrictID;
SELECT ##ROWCOUNT AS AffectedRow;
COMMIT TRAN;
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE() AS ERROR
ROLLBACK TRAN;
END CATCH
END
And where is #BudgetStateID defined?
I am using cursor to delete set of tables. I need to set flag to identify whether all transactions are success or not. Can you please give me some sample queries?
DEclare #intErrorCode int;
DECLARE #TblName NVARCHAR(MAX);
DECLARE TBL_Cursor CURSOR
FOR ( select name from sysobjects where name like 'tbl_flat%');
OPEN TBL_Cursor;
FETCH NEXT FROM TBL_Cursor INTO #TblName
WHILE (##FETCH_STATUS <> -1)
BEGIN
IF LEN(#TblName) >0
BEGIN
DECLARE #strsql nvarchar(max)
BEGIN
something here
BEGIN TRAN
EXEC sp_executesql #strsql
COMMIT TRAN
SELECT #intErrorCode = ##ERROR
IF (#intErrorCode <> 0) GOTO PROBLEM
END
END
FETCH NEXT FROM TBL_Cursor INTO #TblName
END
CLOSE TBL_Cursor
DEALLOCATE TBL_Cursor
PROBLEM:
IF (#intErrorCode <> 0) BEGIN
PRINT 'Unexpected error occurred!'
END
BEGIN TRY
BEGIN TRANSACTION
--your code
EXEC sp_executesql #strsql
COMMIT TRAN -- Transaction Success!
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN --RollBack in case of Error
-- you can Raise ERROR with RAISEERROR() Statement including the details of the exception
RAISERROR(ERROR_MESSAGE(), ERROR_SEVERITY(), 1)
END CATCH
I'm pretty new in T-SQL and I'm in trouble with some huge scripts with transactions, cursors and storage procedures. So, my code is something like this (this code is just an example of the structure of my scripts, in fact I have multiples procedures inside OuterProc cursor and multiple operations inside InnerProc cursor):
create proc InnerProc
as
begin
declare #Id int
begin tran
declare mycursor cursor local static read_only forward_only
for select Id
from MyOtherTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
select 1/0
if ##ERROR <> 0
begin
rollback tran
return ##ERROR
end
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
commit tran
end
create proc OuterProc
as
begin
declare #Id int
begin tran
declare mycursor cursor local static read_only forward_only
for select Id
from MyTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
exec #error = InnerProc
if ##ERROR <> 0
begin
rollback tran
return
end
else
commit tran
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
end
With this structure I have this error:
Msg 515, Level 16, State 2, Procedure InnerProc, Line 448
Cannot insert the value NULL into column 'InitialQuantity', table 'MySecondTable'; column does not allow nulls. INSERT fails.
The statement has been terminated.
Msg 266, Level 16, State 2, Procedure InnerProc, Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
Msg 3903, Level 16, State 1, Procedure CreateSASEExtraction, Line 79
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
What is wrong with my code? If something goes wrong inside innerProc, I want all operations for that outer cursor rollback and stop the inner cursor. If something goes wrong in the outerProc I want all operations for that cursor to rollback but I want that cursor continue to looping...
There is a better way to do this?
UPDATE:
After I correct some errors #Bernd Linde detected, I add a try-catch in InnerProc and I named the InnerProc transaction. Now I have this code:
create proc InnerProc
as
begin
declare #Id int
begin tran
begin try
declare mycursor cursor local static read_only forward_only
for select Id
from MyOtherTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
select 1/0
if ##ERROR <> 0
return ##ERROR
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
commit tran
return 0
end try
begin catch
return ##ERROR
end catch
end
create proc OuterProc
as
begin
declare #Id int
declare mycursor cursor local static read_only forward_only
for select Id
from MyTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
begin tran
exec #error = InnerProc
if ##ERROR <> 0
begin
rollback tran
return
end
else
commit tran
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
end
But now I have other error message:
Msg 266, Level 16, State 2, Procedure InnerProc, Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 2.
How can I solve this?
From the first look, you are committing transactions inside your loops, but you are only starting them once outside the loop.
So each time the loop goes into it's second iteration, it will try to either commit or rollback a transaction that does not exist, hence why you are getting the error "The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION."
I would suggest reading up on transactions in SQLServer on MSDN here
After many attemps, finally I get it.
The InnerProc must only have COMMITs and the OuterProc will be responsible for rollback.
For that, when InnerProc causes some error that must be catch in the OuterProc and forced to act like a exception.
How I want to continue looping in the OuterProc, that procedure must have a try-catch where the looping is forced and the rollback is done.
For a better transaction number control I used the ##TRANCOUNT.
So I solve the problem with this code:
create proc InnerProc
as
begin
declare #Id int
begin try
begin tran
declare mycursor cursor local static read_only forward_only
for select Id
from MyOtherTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
select 1/0
IF ##ERROR <> 0
begin
if ##TRANCOUNT > 0
rollback tran
close mycursor
deallocate mycursor
return ##ERROR
end
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
commit tran
return 0
end try
begin catch
close mycursor
deallocate mycursor
return ##ERROR
end catch
end
create proc OuterProc
as
begin
declare #Id int
declare mycursor cursor local static read_only forward_only
for select Id
from MyTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
begin tran
begin try
exec #error = InnerProc
if ##ERROR <> 0
RAISERROR('Exception',1,1)
if##TRANCOUNT > 0
commit tran
fetch next from mycursor into #Id, #Name, #CodeDGAE, #Code, #NUIT, #Project
end try
begin catch
if ##TRANCOUNT > 0
rollback tran
fetch next from mycursor into #Id, #Name, #CodeDGAE, #Code, #NUIT, #Project
end catch
end
close mycursor
deallocate mycursor
end
In C# language we use continue statement in a loop to move to next iteration. But in using Cursor in TSQL how can I perform the same. Let say I have,
DECLARE db_cursor CURSOR FOR SELECT age, name, color FROM table;
DECLARE #myName VARCHAR(256);
DECLARE #myAge INT;
DECLARE #myFavoriteColor VARCHAR(40);
OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
WHILE ##FETCH_STATUS = 0
BEGIN
--Do stuff
IF #myAge=1
BEGIN
-- Use continue here
END
--Do stuff
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
END;
CLOSE db_cursor;
DEALLOCATE db_cursor;
CONTINUE does go back to the start of the WHILE loop, but it's not exactly like in C#, since the cursor looping idiom in T-SQL is broken into two separate statements, and the WHILE is the second of them - the cleanest, requiring the least repetition, may be our old friend GOTO:
DECLARE db_cursor CURSOR FOR SELECT age, name, color FROM table;
DECLARE #myName VARCHAR(256);
DECLARE #myAge INT;
DECLARE #myFavoriteColor VARCHAR(40);
OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
WHILE ##FETCH_STATUS = 0
BEGIN
--Do stuff
IF #myAge=1
BEGIN
Goto Cont
END
--Do stuff
Cont:
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
END;
CLOSE db_cursor;
DEALLOCATE db_cursor;
you can use CONTINUE in this manner
DECLARE db_cursor CURSOR FOR SELECT age, name, color FROM table;
DECLARE #myName VARCHAR(256);
DECLARE #myAge INT;
DECLARE #myFavoriteColor VARCHAR(40);
OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
WHILE ##FETCH_STATUS = 0
BEGIN
--Do stuff
IF #myAge=1
BEGIN
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
CONTINUE;
END
--Do stuff
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
END;
CLOSE db_cursor;
DEALLOCATE db_cursor;
Try this one -
DECLARE
#myName VARCHAR(256)
, #myAge INT
, #myFavoriteColor VARCHAR(40)
DECLARE cursor_name CURSOR FAST_FORWARD READ_ONLY FOR
SELECT age, name, color
FROM [table]
OPEN db_cursor
FETCH NEXT FROM db_cursor INTO
#myName
, #myAge
, #myFavoriteColor
WHILE ##FETCH_STATUS = 0 BEGIN
IF #myAge = 1 BEGIN
FETCH NEXT FROM db_cursor INTO
#myName
, #myAge
, #myFavoriteColor
END
FETCH NEXT FROM db_cursor INTO
#myName
, #myAge
, #myFavoriteColor
END
CLOSE db_cursor
DEALLOCATE db_cursor
I have a cursor containing several columns from the row it brings back that I would like to process at once. I notice most of the examples I've seeing on how to use cursors show them assigning a particular column from the cursor to a scalar value one at a time, then moving to the next row,
e.g.
OPEN db_cursor
FETCH NEXT FROM db_cursor INTO #name
WHILE ##FETCH_STATUS = 0
BEGIN
--Do Stuff with #name scalar value, then get next row from cursor
FETCH NEXT FROM db_cursor INTO #name
END
What I want to know is if it's possible to do something like the following:
OPEN db_cursor
FETCH NEXT FROM db_cursor;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #myName = db_cursor.name;
SET #myAge = db_cursor.age;
SET #myFavoriteColor = db_cursor.favoriteColor;
--Do stuff with scalar values
FETCH NEXT FROM db_cursor;
END
Help is always appreciated.
This should work:
DECLARE db_cursor CURSOR FOR SELECT name, age, color FROM table;
DECLARE #myName VARCHAR(256);
DECLARE #myAge INT;
DECLARE #myFavoriteColor VARCHAR(40);
OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
WHILE ##FETCH_STATUS = 0
BEGIN
--Do stuff with scalar values
FETCH NEXT FROM db_cursor INTO #myName, #myAge, #myFavoriteColor;
END;
CLOSE db_cursor;
DEALLOCATE db_cursor;
Do not use ##fetch_status - this will return status from the last cursor in the current connection. Use the example below:
declare #sqCur cursor;
declare #data varchar(1000);
declare #i int = 0, #lastNum int, #rowNum int;
set #sqCur = cursor local static read_only for
select
row_number() over (order by(select null)) as RowNum
,Data -- you fields
from YourIntTable
open #cur
begin try
fetch last from #cur into #lastNum, #data
fetch absolute 1 from #cur into #rowNum, #data --start from the beginning and get first value
while #i < #lastNum
begin
set #i += 1
--Do your job here
print #data
fetch next from #cur into #rowNum, #data
end
end try
begin catch
close #cur --|
deallocate #cur --|-remove this 3 lines if you do not throw
;throw --|
end catch
close #cur
deallocate #cur