web-dev-qa-db-fra.com

Ajouter une colonne au tableau, puis la mettre à jour dans la transaction

Je crée un script qui sera exécuté sur un serveur MS SQL. Ce script exécutera plusieurs instructions et doit être transactionnel.Si l'une des instructions échoue, l'exécution globale est arrêtée et toutes les modifications sont annulées.

Je rencontre des problèmes pour créer ce modèle transactionnel lors de l'émission d'instructions ALTER TABLE pour ajouter des colonnes à une table, puis pour mettre à jour la colonne nouvellement ajoutée. Afin d'accéder immédiatement à la colonne nouvellement ajoutée, j'utilise une commande GO pour exécuter l'instruction ALTER TABLE, puis j'appelle mon instruction UPDATE. Le problème auquel je suis confronté est que je ne peux pas émettre de commande GO dans une instruction IF. L'instruction IF est importante dans mon modèle transactionnel. Ceci est un exemple de code du script que j'essaie d'exécuter. Notez également que l'exécution d'une commande GO annulera la variable @errorCode et devra être déclarée dans le code avant d'être utilisée (ce n'est pas dans le code ci-dessous).

BEGIN TRANSACTION

DECLARE @errorCode INT
SET @errorCode = @@ERROR

-- **********************************
-- * Settings
-- **********************************
IF @errorCode = 0
BEGIN
 BEGIN TRY
  ALTER TABLE Color ADD [CodeID] [uniqueidentifier] NOT NULL DEFAULT ('{00000000-0000-0000-0000-000000000000}')
  GO
 END TRY
 BEGIN CATCH
  SET @errorCode = @@ERROR
 END CATCH
END

IF @errorCode = 0
BEGIN
 BEGIN TRY
  UPDATE Color
  SET CodeID= 'B6D266DC-B305-4153-A7AB-9109962255FC'
  WHERE [Name] = 'Red'
 END TRY
 BEGIN CATCH
  SET @errorCode = @@ERROR
 END CATCH
END

-- **********************************
-- * Check @errorCode to issue a COMMIT or a ROLLBACK
-- **********************************
IF @errorCode = 0
BEGIN
 COMMIT
 PRINT 'Success'
END
ELSE 
BEGIN
 ROLLBACK
 PRINT 'Failure'
END

Donc, ce que je voudrais savoir, c'est comment contourner ce problème, en émettant des instructions ALTER TABLE pour ajouter une colonne, puis en mettant à jour cette colonne, le tout dans un script s'exécutant en tant qu'unité transactionnelle.

64
Guillermo Gomez

GO n'est pas une commande T-SQL. Est un délimiteur par lots. L'outil client (SSM, sqlcmd, osql, etc.) l'utilise pour couper efficacement le fichier à chaque GO et envoyer au serveur les lots individuels. Donc, évidemment, vous ne pouvez pas utiliser GO dans IF, ni vous attendre à ce que les variables s'étendent sur plusieurs lots.

En outre, vous ne pouvez pas intercepter des exceptions sans vérifier la XACT_STATE() pour vous assurer que la transaction n'est pas condamnée.

L'utilisation de GUID pour les ID est toujours au moins suspecte.

Utiliser des contraintes NOT NULL et fournir un "guid" par défaut comme '{00000000-0000-0000-0000-000000000000}' ne peut pas non plus être correct.

Mis à jour:

  • Séparez ALTER et UPDATE en deux lots.
  • Utilisez les extensions sqlcmd pour interrompre le script en cas d'erreur. Ceci est pris en charge par SSMS lorsque le mode sqlcmd est activé , sqlcmd, et est trivial pour le prendre également en charge dans les bibliothèques clientes: dbutilsqlcmd .
  • utilisation XACT_ABORT pour forcer l'erreur à interrompre le lot. Ceci est fréquemment utilisé dans les scripts de maintenance (modifications de schéma). Les procédures stockées et les scripts de logique d'application utilisent généralement des blocs TRY-CATCH à la place, mais avec le soin approprié: Gestion des exceptions et transactions imbriquées .

exemple de script:

:on error exit

set xact_abort on;
go

begin transaction;
go

if columnproperty(object_id('Code'), 'ColorId', 'AllowsNull') is null
begin
    alter table Code add ColorId uniqueidentifier null;
end
go

update Code 
  set ColorId = '...'
  where ...
go

commit;
go

Seul un script réussi atteindra le COMMIT. Toute erreur annulera le script et la restauration.

J'ai utilisé COLUMNPROPERTY pour vérifier l'existence de la colonne, vous pouvez utiliser n'importe quelle méthode à votre place (par exemple, recherche sys.columns ).

42
Remus Rusanu

Orthogonale aux commentaires de Remus, ce que vous pouvez faire est d'exécuter la mise à jour dans un sp_executesql.

ALTER TABLE [Table] ADD [Xyz] NVARCHAR(256);

DECLARE @sql NVARCHAR(2048) = 'UPDATE [Table] SET [Xyz] = ''abcd'';';
EXEC sys.sp_executesql @query = @sql;

Nous avons dû le faire lors de la création de scripts de mise à niveau. Habituellement, nous utilisons simplement GO, mais il a été nécessaire de faire les choses de manière conditionnelle.

21
Mark Sowul

Je suis presque d'accord avec Remus mais vous pouvez le faire avec SET XACT_ABORT ON et XACT_STATE

Fondamentalement

  • SET XACT_ABORT ON annulera chaque lot en cas d'erreur et de ROLLBACK
  • Chaque lot est séparé par GO
  • L'exécution passe au lot suivant en cas d'erreur
  • Utiliser XACT_STATE () testera si la transaction est toujours valide

Des outils comme Red Gate SQL Compare utilisent cette technique

Quelque chose comme:

SET XACT_ABORT ON
GO
BEGIN TRANSACTION
GO

IF COLUMNPROPERTY(OBJECT_ID('Color'), 'CodeID', ColumnId) IS NULL
   ALTER TABLE Color ADD CodeID [uniqueidentifier] NULL
GO

IF XACT_STATE() = 1
  UPDATE Color
  SET CodeID= 'B6D266DC-B305-4153-A7AB-9109962255FC'
  WHERE [Name] = 'Red'
GO

IF XACT_STATE() = 1
 COMMIT TRAN
--else would be rolled back

J'ai également supprimé la valeur par défaut. Aucune valeur = NULL pour GUID valeurs. Il est censé être unique: n'essayez pas de définir chaque ligne sur tous les zéros car cela se terminera en larmes ...

18
gbn

L'avez-vous essayé sans le GO?

Normalement, vous ne devez pas mélanger les changements de table et les changements de données dans le même script.

2
HLGEM

Une autre alternative, si vous ne voulez pas diviser le code en lots séparés, est d'utiliser EXEC pour créer une étendue/un lot imbriqué comme ici

1
M.Sabaa