2010-04-23 7 views
15

Dans mon code C# j'utilise TransactionScope car on m'a dit de ne pas compter sur le fait que mes programmeurs sql utiliseront toujours des transactions et nous sommes responsables et yada yada.TransactionScope et Transactions

Cela dit

Il ressemble objet TransactionScope Rolls retour avant la SqlTransaction? Est-ce possible et si oui quelle est la méthode correcte pour encapsuler un TransactionScope dans une transaction.

Voici le test sql

CREATE PROC ThrowError 
AS 

BEGIN TRANSACTION --SqlTransaction 
SELECT 1/0 

IF @@ERROR<> 0 
BEGIN 
    ROLLBACK TRANSACTION --SqlTransaction 
    RETURN -1 
END 
ELSE 
BEGIN 
    COMMIT TRANSACTION --SqlTransaction 
    RETURN 0 
END 

go 

DECLARE @RESULT INT 

EXEC @RESULT = ThrowError 

SELECT @RESULT 

Et si je lance ce que je Parvenez diviser par 0 et retour -1

Appel à partir du code C# Je reçois un message d'erreur supplémentaire

Diviser par zéro erreur rencontrée.
Le compte de transaction après EXECUTE indique qu'un tat COMMIT ou ROLLBACK TRANSACTION est manquant. Nombre précédent = 1, nombre actuel = 0.

Si je donne la transaction sql un nom puis

ne pouvez pas revenir SqlTransaction. Aucune transaction ou point de sauvegarde de ce nom n'a été trouvé. Le compte de transaction après EXECUTE indique qu'une instruction COMMIT ou ROLLBACK TRANSACTION est manquante. Nombre précédent = 1, nombre actuel = 2.

quelques fois il semble que le nombre monte, jusqu'à ce que l'application sort complètement

Le C# est juste

 using (TransactionScope scope = new TransactionScope()) 
     { 
      ... Execute Sql 

      scope.Commit() 
     } 

EDIT:

Le code sql doit fonctionner pour 2000 et 2005

Répondre

21

Il y avait une mise à jour massive à l'erreur de manipulation dans SQL Server 2005. Ces articles sont assez nombreuses: Error Handling in SQL 2005 and Later by Erland Sommarskog et Error Handling in SQL 2000 – a Background by Erland Sommarskog

La meilleure façon est quelque chose comme ceci:

Créer votre procédure stockée comme:

CREATE PROCEDURE YourProcedure 
AS 
BEGIN TRY 
    BEGIN TRANSACTION --SqlTransaction 
    DECLARE @ReturnValue int 
    SET @ReturnValue=NULL 

    IF (DAY(GETDATE())=1 --logical error 
    BEGIN 
     SET @ReturnValue=5 
     RAISERROR('Error, first day of the month!',16,1) --send control to the BEGIN CATCH block 
    END 

    SELECT 1/0 --actual hard error 

    COMMIT TRANSACTION --SqlTransaction 
    RETURN 0 

END TRY 
BEGIN CATCH 
    IF XACT_STATE()!=0 
    BEGIN 
     ROLLBACK TRANSACTION --only rollback if a transaction is in progress 
    END 

    --will echo back the complete original error message to the caller 
    --comment out if not needed 
    DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int 

    SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE() 
    RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine) 

    RETURN ISNULL(@ReturnValue,1) 

END CATCH 

GO 

Toutefois, ce n'est que pour SQL Server 2005 et versions ultérieures.Sans utiliser les blocs TRY-CATCH dans SQL Server 2005, vous avez du mal à supprimer tous les messages que SQL Server renvoie. Le extra messages vous renvoie à sont causés par la nature de la façon dont sont traitées à l'aide rollbacks @@ trancount:

de http://www.sommarskog.se/error-handling-I.html#trancount

@@ trancount est une variable globale qui reflète le niveau de transactions imbriquées. Chaque BEGIN TRANSACTION augmente @@ trancount par 1, et chaque COMMIT TRANSACTION diminue @@ trancount par 1. Rien n'est en fait commis jusqu'à ce que @@ trancount atteint 0. ROLLBACK TRANSACTION annule tout au plus à l'extérieur BEGIN TRANSACTION (sauf si vous avez utilisé le assez exotique SAVE TRANSACTION), et force @@ trancount à 0, en ce qui concerne la valeur précédente.

Lorsque vous quittez une procédure stockée, si @@ trancount n'a pas la même valeur telle qu'elle avait lorsque la procédure d'exécution commencé, SQL Server génère erreur 266. Cette erreur ne se pose pas, cependant, si la procédure est appelée à partir d'un déclencheur, directement ou indirectement. Il n'est pas élevé si vous utilisez avec SET IMPLICIT TRANSACTIONS SUR

Si vous ne voulez pas obtenir l'avertissement au sujet de la transaction ne correspond pas à compter, vous devez avoir seulement une transaction ouverte à tout moment . Vous faites cela en créant toute votre procédure comme ceci:

CREATE PROC YourProcedure 
AS 
DECLARE @SelfTransaction char(1) 
SET @SelfTransaction='N' 

IF @@trancount=0 
BEGIN 
    SET @SelfTransaction='Y' 
    BEGIN TRANSACTION --SqlTransaction 
END 

SELECT 1/0 

IF @@ERROR<> 0 
BEGIN 
    IF @SelfTransaction='Y' 
    BEGIN 
     ROLLBACK TRANSACTION --SqlTransaction 
    END 
    RETURN -1 
END 
ELSE 
BEGIN 
    IF @SelfTransaction='Y' 
    BEGIN 
     COMMIT TRANSACTION --SqlTransaction 
    END 
    RETURN 0 
END 

GO 

En faisant cela, vous émettez que la transaction commandes si vous n'êtes pas déjà dans une transaction. Si vous codez toutes vos procédures de cette façon, seule la procédure ou le code C# qui émet BEGIN TRANSACTION lancera le COMMIT/ROLLBACK et le nombre de transactions correspondra toujours (vous n'obtiendrez pas d'erreur).

en C# de TransactionScope Class Documentation:

static public int CreateTransactionScope(
    string connectString1, string connectString2, 
    string commandText1, string commandText2) 
{ 
    // Initialize the return value to zero and create a StringWriter to display results. 
    int returnValue = 0; 
    System.IO.StringWriter writer = new System.IO.StringWriter(); 

    try 
    { 
     // Create the TransactionScope to execute the commands, guaranteeing 
     // that both commands can commit or roll back as a single unit of work. 
     using (TransactionScope scope = new TransactionScope()) 
     { 
      using (SqlConnection connection1 = new SqlConnection(connectString1)) 
      { 
       // Opening the connection automatically enlists it in the 
       // TransactionScope as a lightweight transaction. 
       connection1.Open(); 

       // Create the SqlCommand object and execute the first command. 
       SqlCommand command1 = new SqlCommand(commandText1, connection1); 
       returnValue = command1.ExecuteNonQuery(); 
       writer.WriteLine("Rows to be affected by command1: {0}", returnValue); 

       // If you get here, this means that command1 succeeded. By nesting 
       // the using block for connection2 inside that of connection1, you 
       // conserve server and network resources as connection2 is opened 
       // only when there is a chance that the transaction can commit. 
       using (SqlConnection connection2 = new SqlConnection(connectString2)) 
       { 
        // The transaction is escalated to a full distributed 
        // transaction when connection2 is opened. 
        connection2.Open(); 

        // Execute the second command in the second database. 
        returnValue = 0; 
        SqlCommand command2 = new SqlCommand(commandText2, connection2); 
        returnValue = command2.ExecuteNonQuery(); 
        writer.WriteLine("Rows to be affected by command2: {0}", returnValue); 
       } 
      } 

      // The Complete method commits the transaction. If an exception has been thrown, 
      // Complete is not called and the transaction is rolled back. 
      scope.Complete(); 
     } 
    } 
    catch (TransactionAbortedException ex) 
    { 
     writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message); 
    } 
    catch (ApplicationException ex) 
    { 
     writer.WriteLine("ApplicationException Message: {0}", ex.Message); 
    } 

    // Display messages. 
    Console.WriteLine(writer.ToString()); 

    return returnValue; 
} 

Juste une pensée, mais vous pourriez être en mesure d'utiliser les TransactionAbortedException prises pour obtenir l'erreur réelle et ignorer le nombre de transactions d'avertissement de non-concordance.

+0

@KM, Vous avez 'IF @@ trancount <0' tôt dans l'exemple de proc. Est-ce que @@ trancount peut être négatif? Ne devrait-il pas être IF @@ trancount = 0' ?? –

+0

@Charles Bretana, vous avez raison, c'est un type-o. Je vais le réparer ... –

+0

@KM, Thx! ça a fonctionné dans mon test sproc avec le changement, mais ça a été ici non perturbé depuis avril/mai ... Donc mon hypothèse naturelle est qu'il me manque quelque chose ... je n'étais pas complètement sûr d'une façon ou d'une autre ... Heureux Vacances ! –

1

Vous devez utiliser une prise d'essai

BEGIN TRANSACTION --SqlTransaction 
BEGIN TRY 
    SELECT 1/0 
    COMMIT TRANSACTION --SqlTransaction 
    RETURN 0 
END TRY 
BEGIN CATCH 
    ROLLBACK TRANSACTION --SqlTransaction 
    RETURN -1 
END CATCH 

Et cette question devrait répondre à votre question sur TransactionScope et Rollbacks How does TransactionScope roll back transactions?

+0

j'ai oublié de mentionner que cela doit travailler pour sql 2000 une nd 2005 mais même pour 2005 quelle est la différence entre l'instruction if et le bloc try catch. La procédure de stockage fonctionne de manière autonome, c'est-à-dire lorsqu'elle s'exécute dans la fenêtre de requête. – Mike

+0

le problème est que l'erreur Diviser par zéro plante votre SP. Le try-catch permet à votre code d'échouer sans quitter le SP. Dans l'instruction IF, dès que l'erreur se produit, le SP est quitté. – Glennular

+0

Je ne suis pas convaincu de cela parce que si je cours sql ci-dessus j'obtiens le résultat de -1 qui est dans l'instruction de retour de la condition si. – Mike

-1

Je sais que c'est une suggestion incroyablement banal, mais je pas une bonne solution pour être d'empêcher la division par zéro en premier lieu ? Pratiquement toutes les opérations DML (insérer, sélectionner, mettre à jour) peuvent être réécrites pour éviter la division par des zéros grâce à l'utilisation des instructions CASE.

+1

Correct. Mais la division par zéro est un exemple, c'est pourquoi j'ai choisi de le coder en dur. Le problème est la manière correcte de capturer des erreurs et d'utiliser l'objet de portée de transaction. – Mike

12

Ne pas utiliser les transactions dans à la fois votre code C# et les sprocs. Un seul suffit. Ce qui devrait presque toujours être votre code C#, seulement il sait quel ensemble de mises à jour à la base de données devrait être rejeté ou engagé en entier.

+2

Je ne suis pas sûr d'être d'accord avec ça. Il se peut que vous écriviez une API de procédure stockée destinée à être utilisée par de nombreux publics. En tant qu'auteur de l'API, vous savez quelles procédures stockées doivent être mieux traitées que les clients. (Cela pourrait être quelque chose d'aussi banal qu'un raiserror quand @@ trancount est 0.) – Paul

2

Si vous devez prendre en charge SQL Server 2000, utilisez TransactionScope pour vous simplifier la vie. Cependant, voir en bas pourquoi il y a des limites.

La gestion des erreurs SQL avant TRY/CATCH est erronée. L'article d'Erland publié par KM explique les erreurs d'abandon de déclaration/portée/lot qui le font. Fondamentalement, le code peut simplement cesser d'exécuter et vous êtes laissé avec des verrous sur les lignes, etc.

C'est ce qui se passe ci-dessus afin que votre rollback ne fonctionne pas, donc vous obtenez l'erreur 226 sur le nombre de transactions.

Si vous ne prenez en charge que SQL Server 2005+, utilisez TRY/CATCH pour capturer toutes les erreurs et utiliser SET XACT_ABORT ON. TRY/CATCH rend SQL Server beaucoup plus résilient et piège toutes les erreurs d'exécution. SET XACT_ABORT ON supprime également l'erreur 226 car elle génère automatiquement l'annulation et garantit que tous les verrous sont libérés.

BTW:

SELECT 1/0 est un excellent exemple de la raison pour laquelle vous devez utiliser la gestion des erreurs SQL.

Utilisez un DataAdapter pour remplir

  • un Datatable d'un proc stocké avec SELECT 1/0 -> aucune erreur piégé
  • un DataSet à partir d'une procédure stockée avec SELECT 1/0 -> erreur piégé

SQL try/catch traitera de cette ...

0
public string ExecuteReader(string SqlText) 
{ 
    SqlCommand cmd; 
    string retrunValue = ""; 
    try 
    { 
     c.Open(); 
     cmd = new SqlCommand(); 
     cmd.CommandType = CommandType.Text;     
     cmd.Connection = c; 
     cmd.CommandText = SqlText; 
     retrunValue = Convert.ToString(cmd.ExecuteScalar()); 
     c.Close(); 
    } 
    catch (Exception SqlExc) 
    { 
     c.Close(); 
     throw SqlExc; 

    } 
    return (retrunValue); 
} 
Questions connexes