Я использую GoTo для закрытия и освобождения курсора. Есть ли лучший способ написать мой код? Обратите внимание: этот код предназначен для изучения того, как использовать курсоры и выходные параметры.
Я новичок в TSQL, поэтому не стесняйтесь комментировать другие темы. Например, я искал хранимую процедуру, которая закроет курсор, если он открыт, но я ничего не нашел через поиск Google.
Спасибо
-- Find the MAX, MIN, Avg for each Color in the Products table using Cursor
USE [AdventureWorks2019]
GO
/****** Object: StoredProcedure [dbo].[WithCursor] Script Date: 5/29/2024 11:30:17 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROC [dbo].[WithCursor]
(
@color VARCHAR(20) = NULL,
@MinStock INT OUTPUT,
@MaxStock INT OUTPUT,
@AvgStock INT OUTPUT
)
AS
SET NOCOUNT ON
BEGIN TRY
Declare @recordCount int = ( SELECT Count(*)
FROM Production.Product
WHERE (@color IS NULL AND Color IS NULL) OR (Color = @color))
-- ******************** NO Records Exist ********************
IF @recordCount = 0
Begin
DECLARE @errorMessage NVARCHAR(200);
IF @color IS NULL
SET @errorMessage = N'There are no products with the color NULL';
ELSE
SET @errorMessage = CONCAT(N'There are no products with the color ', @color);
RAISERROR(@errorMessage, 11, 1);
RETURN -2; -- no such color
End
-- ******************** Open the Cursor for Products ********************
DECLARE @level int;
IF EXISTS (
SELECT 1
FROM sys.dm_exec_cursors(0)
WHERE name = 'pCursor'
)
BEGIN
CLOSE pCursor
DEALLOCATE ProductCursor;
END
DECLARE pCursor CURSOR FOR
SELECT SafetyStockLevel
FROM Production.Product
WHERE (@color IS NULL AND Color IS NULL) OR (Color = @color);
--Open the Cursor. We know there is at least 1 record from the above check.
OPEN pCursor
FETCH NEXT FROM pCursor INTO @level
-- ******************** 1 Record Exist ********************
IF @recordCount = 1
Begin
Set @MinStock = @level
Set @MaxStock = @level
Set @AvgStock = @level
GoTo Cleanup
End
-- ******************** 2 or More Records Exist ********************
Declare @SumLevel int
Declare @MinLevel int
Declare @MaxLevel int
Set @SumLevel = @level
Set @MinLevel = @level
Set @MaxLevel = @level
--Move to the second record
FETCH NEXT FROM pCursor INTO @level
While @@FETCH_STATUS = 0
Begin
Set @SumLevel = @SumLevel + @level
If @level < @MinLevel
Set @MinLevel = @level
If @level > @MaxLevel
Set @MaxLevel = @level
FETCH NEXT FROM pCursor INTO @level
End
Set @MinStock = @Minlevel
Set @MaxStock = @Maxlevel
Set @AvgStock = @SumLevel / @recordCount
GoTo Cleanup
END TRY
BEGIN CATCH
THROW;
--Cleanup
IF EXISTS (
SELECT 1
FROM sys.dm_exec_cursors(0)
WHERE name = 'pCursor'
)
BEGIN
CLOSE pCursor
DEALLOCATE ProductCursor;
END
RETURN -1;
END CATCH;
--Success
Cleanup:
CLOSE pCursor
DEALLOCATE pCursor
Return 0
Я могу продублировать код, и это удалит GoTo, но я стараюсь максимально не дублировать код.
Случайные советы: (@color IS NULL AND Color IS NULL) OR (Color = @color) может быть @color is not distinct from Color в SQL Server 2022. N'There are no products with the color ' + Coalesce( @color, N'NULL' ). Добавьте терминаторы операторов (;).
Мои мысли: (1) Я бы отказался от первоначального @RowCount расчета и начального автономного FETCH NEXT. Просто просматривайте строки по одной, считая вхождения. Если после цикла счетчик равен нулю, выполните специальное действие. (2) Я никогда не утруждаюсь проверкой и освобождением существующего курсора заранее. Я просто предполагаю, что система находится в чистом состоянии для запуска. (Исключением может быть отладка и выборочное выполнение фрагментов кода.) (3) Если вы перехватываете исключение, возможно, вы можете просто установить флаг, перейти к логике очистки и, наконец, выполнить соответствующее действие возврата результата.
Если ProductCursor должно отличаться от pCursor?
Спасибо за ответы. Blufus: есть ли способ автоматически перенести этот тред в Code Review? Хабо: Спасибо. К сожалению, я использую SQL Server 2019, но включил N'Нет продуктов с цветом ' + Coalesce(@color, N'NULL'). ТН: Спасибо за замечание о ProductCursor. Почему SQL Server не создал ошибку? Я подумаю над остальными вашими пунктами.
Я опубликовал решение ниже. В блоке Catch, когда я бросаю перед возвратом -1, возврат не выполняется, и наоборот. Как лучше всего выбирать место проведения Броска? Спасибо
@TN Я почти уверен, что они имеют в виду только форму DECLARE @c CURSOR, а не форму DECLARE c CURSOR, но, возможно, это можно было бы сформулировать лучше.
@testing-ma-lady — Хороший улов. Ты прав. Я не заметил разницы между «курсором» и «переменной курсора». Тем не менее, с логикой броска происходит что-то сложное. Если я закомментирую операторы закрытия/освобождения курсора, курсор останется после обычного выхода, но исчезнет после повторного THROW, который перехватывается вызывающей стороной. Посмотрите эту скрипку. Для 3-го и 4-го (ультрафиолетового и черного) тестов курсор пропадает между «Здесь 2б» и «Здесь 3а», и я не понимаю почему. (Именно тогда я искал и наткнулся на этот оператор «неявно освобожденного».)





Я провел рефакторинг кода с учетом всех предложений. Всем спасибо. Было очень полезно прочитать ваш отзыв. В дальнейшем я буду использовать локальные курсоры.
USE [AdventureWorks2019];
GO
/****** Object: StoredProcedure [dbo].[WithCursor] Script Date: 5/29/2024 11:30:17 AM ******/
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROC [dbo].[WithCursor]
(
@color VARCHAR(20) = NULL,
@MinStock INT OUTPUT,
@MaxStock INT OUTPUT,
@AvgStock INT OUTPUT
)
AS
SET NOCOUNT ON;
BEGIN TRY
-- ******************** Open the Cursor for Products ********************
DECLARE @level INT;
--Local Cursor doesn't need to be deallocated
DECLARE @pCursor CURSOR;
SET @pCursor = CURSOR READ_ONLY FORWARD_ONLY
FOR
SELECT SafetyStockLevel
FROM Production.Product
WHERE Color = @color OR (@Color IS NULL AND Color IS NULL);
-- Open the Cursor.
OPEN @pCursor;
FETCH NEXT FROM @pCursor INTO @level;
-- ******************** Empty Recordset ********************
IF @@FETCH_STATUS <> 0
BEGIN
DECLARE @errorMessage NVARCHAR(200);
SET @errorMessage = N'There are no products with the color ' + COALESCE(@color, N'NULL');
RAISERROR(@errorMessage, 11, 1);
RETURN -2; -- no such color
END;
-- ******************** 1 or More Records Exist ********************
DECLARE @SumLevel INT = 0;
DECLARE @MinLevel INT = @level;
DECLARE @MaxLevel INT = @level;
DECLARE @recordCount INT = 0;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @SumLevel = @SumLevel + @level;
IF @level < @MinLevel
SET @MinLevel = @level;
IF @level > @MaxLevel
SET @MaxLevel = @level;
FETCH NEXT FROM @pCursor INTO @level;
SET @recordCount = @recordCount + 1;
END;
--Set the Output Parameters
SET @MinStock = @MinLevel;
SET @MaxStock = @MaxLevel;
SET @AvgStock = @SumLevel / @recordCount;
END TRY
BEGIN CATCH
THROW;
RETURN -1;
END CATCH;
Также помните о различиях между ЛОКАЛЬНЫМИ и ГЛОБАЛЬНЫМИ курсорами. Обычно рекомендуется использовать ЛОКАЛЬНЫЙ курсор, чтобы избежать таких ошибок, как: «Курсор с именем «YourCursorName» уже существует». Если вы не укажете явно, обычно по умолчанию используется значение GLOBAL (зависит от параметров базы данных).
Благодарим вас за вклад в сообщество Stack Overflow. Возможно, это правильный ответ, но было бы очень полезно предоставить дополнительные пояснения к вашему коду, чтобы разработчики могли понять ваши рассуждения. Это особенно полезно для новых разработчиков, которые не так хорошо знакомы с синтаксисом или пытаются понять концепции. Не могли бы вы отредактировать свой ответ, включив в него дополнительную информацию на благо сообщества?
Вместо этого вы можете задать свой вопрос в Code Review Stack Exchange.