none
TransactionScope und "normale" Transactions RRS feed

  • Frage

  • Hallo zusammen,

    ich möchte/darf/muss in einer Transaktion Daten in mehrere Datenbanken schreiben. Dazu habe ich folgenden Code:

    using(TransactionScope scope = new TransactionScope()) {
      connectionNames.ForEach(t => {
      var entities = this._collection.Where(u => u.Key.AllocatedConnectionName == t);
       if(!entities.IsNullOrEmpty()) {
         using(connection = DbDataSource.CreateConnection(t)) {
          using(DbCommand command = connection.CreateCommand()) {
            using(DbTransaction transaction = connection.BeginTransaction(this.IsolationLevel)) {
              command.Transaction = transaction;
    
              StringBuilder sb = new StringBuilder();
              entities.ForEach(u => sb.AppendFormat("{0};", this.ToSqlString(u.Key, u.Value)));
              command.CommandText = sb.ToString();
    
              try {
               command.ExecuteNonQuery();
               command.Transaction.Commit();
              } catch(Exception en) {
               command.Transaction.Rollback();
               throw en;
              }
            }
           }
         }
        }
      });
      scope.Complete();
    }
    

    Nun bin ich mir nicht sicher, ob das so funktioniert. Wenn ich z.b. zwei innere Transaktionen commite und die dritte geht in die Hose wird ja kein "scope.Complete()" aufgerufen. Wird dann für die bereits committeten Transaktionen ebenfalls noch ein Rollback durchgeführt? Ich kann es mir ehrlich gesagt nicht vorstellen.

    Bestimmt wisst ihr es besser... ;)

    Viele Dank und viele Grüße
    Holger M. Rößler


    Kaum macht man es richtig, schon funktioniert es
    Donnerstag, 1. September 2011 09:40

Antworten

  • Hallo Holger,

    man sollte (muss) sich an der Stelle entscheiden:
    Entweder System.Transactions oder lokale (SQL Server) Transaktionen.
    Beides zu mischen geht sonst über kurz oder lang schief, denn:

    Der SQL Server kennt keine mehrstufigen Transaktionen.
    Wenn Du mehrere Transaktionen schachtelst, setzen innere Transaktionen nur
    den Transaktionszähler (@@TRANCOUNT) hoch (bzw. runter beim Committen).
    Ein Rollback wirkt jedoch auf die äußere Transaktion und alle inneren Transaktionen
    werden abgebrochen, siehe auch Schachteln von Transaktionen

    (Und die dort erwähnten Sicherungspunkte würde ich gar nicht erst weiter studieren,
    auch da gibt es schneller Probleme als man denkt)

    Um eine explizite Steuerung mit System.Transaction zu bekommen,
    kannst Du CommittableTransaction verwenden, was mehr Steuerungsmöglichkeiten
    als der implizite TransactionScope bietet; aber auch komplexer wird.

    Gruß Elmar

     

    Donnerstag, 1. September 2011 09:59
    Beantworter
  • Hallo Holger und Elmar,

    Eine Alternative zu den expliziten Transaktionen mittels CommittableTransaction (die auch ich wärmstens empfehle), könnten implizite Transaktionen sein, die über try/catch-Konstrukte den Abbruch von inneren Transaktionen abfangen, z.B.:

    using System;
    using System.Data.SqlClient;
    using System.Transactions;
    
    namespace ConsoleApplication1
    {
      class Program
      {
        static void Main(string[] args)
        {
          SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder { 
            DataSource = @".\SQLEXPRESS", 
            InitialCatalog = "TestDb", 
            IntegratedSecurity = true 
          };
    
          using (SqlConnection connection = new SqlConnection(connectionBuilder.ConnectionString)) {
            SqlDataAdapter authorDataAdapter = new SqlDataAdapter("SELECT AuthorID, FirstName, LastName FROM Author", connection);
            SqlCommandBuilder commandBuilderAuthor = new SqlCommandBuilder(authorDataAdapter);
            SqlCommand insertAuthorCommand = commandBuilderAuthor.GetInsertCommand(true);
            insertAuthorCommand.CommandText += "; SELECT Scope_Identity()";
    
            SqlDataAdapter bookDataAdapter = new SqlDataAdapter("SELECT BookID, AuthorID, Title, ISBN FROM Book", connection);
            SqlCommandBuilder commandBuilderBook = new SqlCommandBuilder(bookDataAdapter);
            SqlCommand insertBookCommand = commandBuilderBook.GetInsertCommand(true);
    
            using (TransactionScope transactionScope1 = new TransactionScope()) {
              insertAuthorCommand.Parameters["@FirstName"].Value = "John";
              insertAuthorCommand.Parameters["@LastName"].Value="Milton";
              connection.Open();
    
              object authorID = insertAuthorCommand.ExecuteScalar();
    
              try
              {
                using (TransactionScope transactionScope2 = new TransactionScope())
                {
                  // Absichtlicher Tippfehler: AuthroID sollte AuthorID heißen
                  insertBookCommand.Parameters["@AuthroID"].Value = authorID;
                  insertBookCommand.Parameters["@Title"].Value = "Das verlorene Paradies";
                  insertBookCommand.Parameters["@ISBN"].Value = "978-3866472945";
                  insertBookCommand.ExecuteNonQuery();
    
                  // Diese Zeile wird z.Z. nie erreicht
                  transactionScope2.Complete();
                }
    
                // Wenn im vorherigen TransactionScope eine Ausnahme geworfen wird
                // wird diese vom try/catch-Konstrukt abgefangen und wir erreichen
                // nie diese Zeile. Achtung beim Abfangen der Fehler im vorherigen
                // TransactionScope: Würden wir dort die IndexOutOfRange-Ausnahme
                // behandeln, würde die Ausführung transactionScope1.Complete() 
                // erreichen, was eine TransactionAbortedException auslösen würde.
                transactionScope1.Complete();
              }
              catch(IndexOutOfRangeException iorex) {
                Console.WriteLine(iorex.Message);
              }
              catch (SqlException sex) {
                Console.WriteLine(sex.Message);
              }
            }
          }
    
          Console.ReadKey(true);
        }
      }
    }
    
    

    Gruß
    Marcel

    Donnerstag, 1. September 2011 13:21

Alle Antworten

  • Hallo Holger,

    man sollte (muss) sich an der Stelle entscheiden:
    Entweder System.Transactions oder lokale (SQL Server) Transaktionen.
    Beides zu mischen geht sonst über kurz oder lang schief, denn:

    Der SQL Server kennt keine mehrstufigen Transaktionen.
    Wenn Du mehrere Transaktionen schachtelst, setzen innere Transaktionen nur
    den Transaktionszähler (@@TRANCOUNT) hoch (bzw. runter beim Committen).
    Ein Rollback wirkt jedoch auf die äußere Transaktion und alle inneren Transaktionen
    werden abgebrochen, siehe auch Schachteln von Transaktionen

    (Und die dort erwähnten Sicherungspunkte würde ich gar nicht erst weiter studieren,
    auch da gibt es schneller Probleme als man denkt)

    Um eine explizite Steuerung mit System.Transaction zu bekommen,
    kannst Du CommittableTransaction verwenden, was mehr Steuerungsmöglichkeiten
    als der implizite TransactionScope bietet; aber auch komplexer wird.

    Gruß Elmar

     

    Donnerstag, 1. September 2011 09:59
    Beantworter
  • Hallo Holger und Elmar,

    Eine Alternative zu den expliziten Transaktionen mittels CommittableTransaction (die auch ich wärmstens empfehle), könnten implizite Transaktionen sein, die über try/catch-Konstrukte den Abbruch von inneren Transaktionen abfangen, z.B.:

    using System;
    using System.Data.SqlClient;
    using System.Transactions;
    
    namespace ConsoleApplication1
    {
      class Program
      {
        static void Main(string[] args)
        {
          SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder { 
            DataSource = @".\SQLEXPRESS", 
            InitialCatalog = "TestDb", 
            IntegratedSecurity = true 
          };
    
          using (SqlConnection connection = new SqlConnection(connectionBuilder.ConnectionString)) {
            SqlDataAdapter authorDataAdapter = new SqlDataAdapter("SELECT AuthorID, FirstName, LastName FROM Author", connection);
            SqlCommandBuilder commandBuilderAuthor = new SqlCommandBuilder(authorDataAdapter);
            SqlCommand insertAuthorCommand = commandBuilderAuthor.GetInsertCommand(true);
            insertAuthorCommand.CommandText += "; SELECT Scope_Identity()";
    
            SqlDataAdapter bookDataAdapter = new SqlDataAdapter("SELECT BookID, AuthorID, Title, ISBN FROM Book", connection);
            SqlCommandBuilder commandBuilderBook = new SqlCommandBuilder(bookDataAdapter);
            SqlCommand insertBookCommand = commandBuilderBook.GetInsertCommand(true);
    
            using (TransactionScope transactionScope1 = new TransactionScope()) {
              insertAuthorCommand.Parameters["@FirstName"].Value = "John";
              insertAuthorCommand.Parameters["@LastName"].Value="Milton";
              connection.Open();
    
              object authorID = insertAuthorCommand.ExecuteScalar();
    
              try
              {
                using (TransactionScope transactionScope2 = new TransactionScope())
                {
                  // Absichtlicher Tippfehler: AuthroID sollte AuthorID heißen
                  insertBookCommand.Parameters["@AuthroID"].Value = authorID;
                  insertBookCommand.Parameters["@Title"].Value = "Das verlorene Paradies";
                  insertBookCommand.Parameters["@ISBN"].Value = "978-3866472945";
                  insertBookCommand.ExecuteNonQuery();
    
                  // Diese Zeile wird z.Z. nie erreicht
                  transactionScope2.Complete();
                }
    
                // Wenn im vorherigen TransactionScope eine Ausnahme geworfen wird
                // wird diese vom try/catch-Konstrukt abgefangen und wir erreichen
                // nie diese Zeile. Achtung beim Abfangen der Fehler im vorherigen
                // TransactionScope: Würden wir dort die IndexOutOfRange-Ausnahme
                // behandeln, würde die Ausführung transactionScope1.Complete() 
                // erreichen, was eine TransactionAbortedException auslösen würde.
                transactionScope1.Complete();
              }
              catch(IndexOutOfRangeException iorex) {
                Console.WriteLine(iorex.Message);
              }
              catch (SqlException sex) {
                Console.WriteLine(sex.Message);
              }
            }
          }
    
          Console.ReadKey(true);
        }
      }
    }
    
    

    Gruß
    Marcel

    Donnerstag, 1. September 2011 13:21
  • Hallo Elmar, hallo Marcel,

    vielen Dank an Euch. Mit diesen Informationen komme ich weiter :)

    Viele Grüße
    Holger M. Rößler


    Kaum macht man es richtig, schon funktioniert es
    Freitag, 2. September 2011 06:20