web-dev-qa-db-fra.com

Exécutez plusieurs procédures stockées en séquence

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.

5
user48112

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.

Bouclage plus traditionnel

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
5
Filip De Vos

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) + ';
5
Aaron Bertrand