J'ai une procédure stockée écrite en T-SQL (SQL Server 2008 R2). Il s'agit d'une procédure principale qui appelle essentiellement plusieurs autres sous-procédures en séquence. La gestion des appels et des erreurs est identique pour chacun sauf pour le nom de la procédure.
Dans un langage OO j'utiliserais une abstraction telle qu'une interface ou un foncteur et ferais une boucle sur un tas d'objets. Cela ne fonctionne pas en SQL, mais je veux trouver un moyen de faire ce code plus concis avec moins de répétitions de copier-coller. Oui, je sais que fondamentalement SQL concerne les opérations de définition et ne prend pas très bien en charge ce que je veux faire, mais s'il existe un moyen, cela rendra le code beaucoup plus concis. J'ai également besoin de capturer le résultat de chaque appel de procédure stockée et de faire quelque chose avec elle qui n'est pas pertinent pour cette question.
Voici ce que j'ai jusqu'à présent:
CREATE PROCEDURE dbo.testproc
AS BEGIN
DECLARE @step INT, @result INT
DECLARE @tbl TABLE([step] INT, [pname] NVARCHAR(40))
INSERT INTO @tbl ([step], [pname]) VALUES (1, N'proc1')
INSERT INTO @tbl ([step], [pname]) VALUES (2, N'proc2')
INSERT INTO @tbl ([step], [pname]) VALUES (3, N'proc3')
-- Potentially many more procedures here
SET @step = 1
WHILE @step <= (SELECT MAX([step]) FROM @tbl)
BEGIN
DECLARE @sql NVARCHAR(60)
SET @sql = N'EXEC @result = dbo.' + (SELECT [pname] FROM @tbl WHERE [step] = @step)
EXEC (@sql)
IF @result <> 0
BEGIN
INSERT INTO SomeTable error code and step number
RETURN
END
SET @step = @step + 1
END
END
GO
Lorsque j'exécute la procédure, SQL Server me donne une erreur car le @result
la variable faisant partie du SQL dynamique n'est pas définie comme faisant partie du lot contenu dans le @sql
variable. Si je le modifie comme ceci:
SET @sql = N'EXEC dbo.' + (SELECT [pname] FROM @tbl WHERE [step] = @step)
EXEC @result = (@sql)
J'obtiens une erreur de syntaxe.
Cela fonctionne très bien, sauf pour récupérer la valeur de retour des sous-procédures. Existe-t-il un moyen d'atteindre mon objectif déclaré, et si oui, comment?
REMARQUE: sur la base de ce que j'ai demandé ici, un le curseur sonnerait comme une meilleure implémentation qu'une boucle WHILE
surtout compte tenu de la variable de table. Une partie du code qui n'est pas essentielle à cette question implique de connaître le numéro d'itération, d'où l'utilisation d'une variable de contrôle de boucle.
Si vous n'avez pas besoin des valeurs de résultat plus tard, vous pouvez le faire de cette façon:
-- procedures to test with
create proc proc1 as print '1' return 0
GO
create proc proc2 as print '2' return 1
GO
create proc proc3 as print '3' return 0
GO
if object_id('dbo.testproc') is null exec('create procedure dbo.testproc as return(0)')
GO
alter PROCEDURE dbo.testproc
AS
DECLARE @result INT
, @sql nvarchar(max) = N''
DECLARE @tbl TABLE([step] INT, [pname] nvarchar(513))
INSERT INTO @tbl ([step], [pname])
VALUES (1, N'proc1'),
(2, N'proc2'),
(3, N'proc3')
-- Potentially many more procedures here
select @sql = @sql + 'exec @result = ' + QUOTENAME(pname) + ' if @result <> 0 return;'
from @tbl order by step
exec sp_executesql @sql, N'@result int output', @result output
if @result <> 0
begin
print 'do your cleanup'
end
GO
exec testproc
Le moteur variable produira le lot de requêtes suivant
exec @result = [proc1] if @result <> 0 return;
exec @result = [proc2] if @result <> 0 return;
exec @result = [proc3] if @result <> 0 return;
Lorsque le lot est exécuté, il s'arrête de s'exécuter lorsqu'il y a un @result différent de zéro et conserve cette valeur dans le paramètre de sortie.
Si vous souhaitez parcourir les procédures. Puisqu'il n'y a pas de paramètres (ou les paramètres sont tous les mêmes), vous pouvez simplement appeler exec @result = @proc
if object_id('dbo.testproc') is null exec('create procedure dbo.testproc as return(0)')
GO
alter PROCEDURE dbo.testproc
AS
DECLARE @result INT
, @proc sysname
DECLARE @tbl TABLE([step] INT, [pname] nvarchar(513))
INSERT INTO @tbl ([step], [pname])
VALUES (1, N'proc1'),
(2, N'proc2'),
(3, N'proc3')
-- Potentially many more procedures here
declare c cursor fast_forward local
for select pname from @tbl order by step
open c
fetch next from c into @proc
while @@FETCH_STATUS = 0
begin
exec @result = @proc
if @result <> 0
BREAK
fetch next from c into @proc
end
close c
deallocate c
if @result <> 0
begin
print 'do your cleanup'
end
GO
exec testproc
Une autre approche, qui utilise toujours du SQL dynamique mais pas d'échafaudage de curseur laid (et vous permet d'examiner l'étape qui a échoué et le numéro d'erreur généré, sans propager l'erreur à l'appelant):
DECLARE @step INT = 0, @result INT = 0, @sql NVARCHAR(MAX) = N'';
DECLARE @tbl TABLE([step] INT PRIMARY KEY, [pname] NVARCHAR(513));
INSERT @tbl([step],[pname]) VALUES(1,N'dbo.proc1'),(2,N'dbo.proc2'),(3,N'dbo.proc3');
SELECT @sql += N'
SET @step = ' + CONVERT(VARCHAR(12),
ROW_NUMBER() OVER (ORDER BY step)) + ';
IF @result = 0
BEGIN
BEGIN TRY
EXEC @result = ' + pname + ';
END TRY
BEGIN CATCH
SET @result = ERROR_NUMBER();
RETURN;
END CATCH'
FROM @tbl ORDER BY [step] OPTION (MAXDOP 1);
SET @sql += REPLICATE(N' END ', @@ROWCOUNT);
EXEC sp_executesql @sql, N'@step INT OUTPUT, @result INT OUTPUT',
@step = @step OUTPUT, @result = @result OUTPUT;
PRINT 'Failed at step ' + CONVERT(VARCHAR(12), @step);
PRINT 'Error number was ' + CONVERT(VARCHAR(12), @result);
Je ne sais pas si vous vouliez que la valeur "Échec à l'étape" soit le numéro de l'étape (comme votre variable de boucle) ou la valeur réelle de step
dans le tableau. Vous pouvez changer en changeant ceci:
SET @step = ' + CONVERT(VARCHAR(12),
ROW_NUMBER() OVER (ORDER BY step)) + ';
Pour ça:
SET @step = ' + CONVERT(VARCHAR(12), step) + ';