der henry: C#, mysql open datareader

Halllo,

ich habe eine Fehlermeldung/Absturz den ich nicht beseitigen bzw. finden kann

Fehlermeldung: **There is already an open DataReader associated with this Connection which must be closed first. **

Funktion

  • "connection" ist die geöffnete Datenbank. Die Datenbank muss nicht geschlossen werden, die Datenbank wir im Intervall beschrieben.

  • starte eine mysql transaction

  • lese aus der datenbank nach bestimmten Kriterien

  • wird etwas gefunden -> datareader zum auslesen und Daten "intern" speichern -> gefundene Daten in der Datenbank löschen

  • transaction beenden.

Ich habe alles mit "using" try/catch geschrieben ... da am Ende von "using" der datareader automatisch geschlossen wird ... so dachte ich.

public static bool read_writedatapoints(string plcname)
        {
            bool varfind = false;

            using (MySqlTransaction transaction = connection.BeginTransaction())
            {
                try
                {
                    using MySqlCommand cmd = connection.CreateCommand();
                    {
                        try
                        {
                            cmd.Transaction = transaction;
                            cmd.CommandText =
                                $@"SELECT plcvarname, setpoint FROM writeplc WHERE plcvarname LIKE '{MySqlHelper.EscapeString(plcname)}%'";
                       
                            using MySqlDataReader rdr = cmd.ExecuteReader();
                            {
                                try
                                {
                                    // prüfe ob Daten für die SPS vorhanden sind
                                    if (rdr.HasRows)
                                    {   
                                        varfind = true;
                                        
                                        // gefundene Daten aus der SPS sichern
                                        while (rdr.Read())
                                        {
                                            WRITEPLCdatapoint item = new WRITEPLCdatapoint();
                                            item.Plcvarname = (string)rdr["plcvarname"];
                                            item.Setpoint = (string)rdr["setpoint"];
                                            writeplcdatapoints.Add(item);

                                            if (Program.debuglevel == 1)
                                            {
                                                Console.WriteLine($@"Es ein Schreibefehl für die gewählte SPS gefunden: 
												        			{item.Plcvarname}, 
														        	Wert: {item.Setpoint}");
                                            }
                                        }
                                    }
                                    else
                                    {
                                        varfind = false;
                                        if (Program.debuglevel == 2)
                                        {
                                            Console.WriteLine(
                                                "Es wurde keine Schreibbefehle für die gewählte SPS gefunden");
                                            return false;
                                        }
                                    }
                                }
                                catch (Exception e)
                                {
                                    Console.WriteLine(
                                        "Fehler DataReader in Funktion: read_writedatapoints. Fehler {e}");
                                    return false;
                                }
                            }

                            if (varfind)
                            {
                                cmd.CommandText =
                                    $@"DELETE FROM writeplc WHERE plcvarname LIKE '{MySqlHelper.EscapeString(plcname)}%'";
                                cmd.ExecuteNonQuery();

                                if (Program.debuglevel == 1)
                                { Console.WriteLine("Schreibdaten gelesen und zugleich gelöscht."); }
                            }

							transaction.Commit();
                            return true;
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Fehler beim lesen der Schreibdaten für SPS {0}", e.GetType());
                            Console.WriteLine("Message: {0}", e.Message);
                            transaction.Rollback();
                            Console.WriteLine("Error Rollback Exception Type: {0}", e.GetType());
                        }
                    }
                }
                catch (Exception e)
                { Console.WriteLine("Fehler bei transaction in Funktion: read_writedatapoints. Fehler {e}"); }
            }
            return false;
        }

Der Fehler kommt nur, wenn er Daten in der Datenbank findet, also if (rdr.HasRows) TRUE

Ich sehe es nicht, vermutlich suche ich schon zu lange 😉

Vielen Dank !!

  1. Hallo Henry,

    auch wenn ich dienstlich immer noch bei C# 7 stecke (weil wir .net Framework 4.x verwenden), hab ich das schon mal gesehen und kann Dir den Tipp geben.

    using-Anweisung versus using-Deklaration

    Das ist etwas, das mit C# 8 eingeführt wurde und was ich persönlich für einen grandiosen Missgriff halte. Man verwechselt es sehr leicht. So wie Du. C# führt eine Menge Bequemlichkeits-"Verbesserungen" ein, die man aber genau kennen muss, um darauf nicht reinzufallen.

    Du hast die Deklaration verwendet, nicht die Anweisung. Die geschweiften Klammern hinter der Deklaration sind deshalb lediglich Dekoration, der Reader wird erst am Ende des try-Blocks verworfen.

    Ich zeig's mal kürzer. Hinweis zu var: während JavaScript die var-Deklaration abschafft, führt C# sie ein. Man kann sie verwenden, wenn der Typ der Variablen aus dem Kontext klar ist (hier durch die Methodensignatur) und spart sich Tipparbeit.

    try 
    {
       using (var rdr = cmd.ExecuteReader())   // Anweisung!
       {   // <-- Scope-Beginn von rdr 
           // tu was mit dem Reader
           // Dispose erfolgt hier
       }   // <-- Scope-Ende von rdr 
       // der Reader ist weg, rdr gilt nicht mehr
       // Neues Statement kann ausgeführt werden
       cmd.CommandText = ...
       cmd.ExecuteNonQuery();
    }
    catch () ...
    
    try 
    {  // <-- Scope-Beginn von rdr 
       using var rdr = cmd.ExecuteReader();   // Deklaration!
       {   // <-- Statement-Block ohne besondere Wirkung
           // tu was mit dem Reader
       }
       // der Reader ist immer noch da
       // eins der beiden folgenden Statements crasht
       cmd.CommandText = ...
       cmd.ExecuteNonQuery();
    
       // Dispose erfolgt hier
    }  // <-- Scope-Ende von rdr 
    catch () ...
    

    Du siehst den Unterschied in der Schreibweise? Der Unterschied in der WIRKUNG ist, dass die Deklaration den Reader disposed, wenn der Block endet, in dem sich die Deklaration befindet. Die Anweisung hingegen hat einen untergeordneten Block, an dessen Ende der Dispose erfolgt.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      vielen Dank und soweit verstanden. Ich habe nun alle Funktionen überprüft und dies geändert. Jetzt habe ich ein Problem, gleiche Fehlermeldung, aber erst beim > 200 Durchlauf, teilweise 500 - 600 Durchläufe bevor der Fehler mit dem datareader kommt.

      Fehlerzeile cmd.ExecuteNonQuery();

      Hier kommt auch deine Idee mit der sql-Befehlsverkettung vor 👍

      Es kommt immer 1-2 mal vorher ein catch, danach der offene DataReader

      Der "counter" ist nur zu Testzwecken eingebaut und hat keinerlei Bewandtnis

      public static bool write_plcactvalue_database(Dictionary<string, string> plcactvalue)
              {
                  string sql = ""; 
                  
                  // Werte über Parameter hinzufügen
                  foreach (var dic in plcactvalue)
                  {
                      sql += $"UPDATE datapoints SET actvalue = '{MySqlHelper.EscapeString(dic.Value)}' WHERE plcvarname = '{MySqlHelper.EscapeString(dic.Key)}';";
                      
                      if (Program.debuglevel == 2)
                      { Console.WriteLine($"Schlüssel: {dic.Key}, Wert: {dic.Value}"); }
                  }
      
                  try
                  {
      				using (MySqlCommand cmd = new MySqlCommand(sql, connection))
      				{
                          cmd.ExecuteNonQuery(); 
                          if (Program.debuglevel == 2)
                          { Console.WriteLine($"SPS-Werte geschrieben: {sql}"); }
      
                          ConnectSql.sqlProgramm.mycounter += 1;
                          
                          // Console.Clear();
                          Console.WriteLine($"\nCounter: {ConnectSql.sqlProgramm.mycounter}");
      				}
      				return true;
      			}
      			catch (Exception e)
      				{ Console.WriteLine($"Fehler beim schreiben der SPS-Werte in die Datenbank: {e}"); }
      		 return false;
      		}
      

      Vielleicht fällt dir oder jemand anderen etwas auf.

      Vielen Dank !!!

      1. Hallo Henry,

        Es kommt immer 1-2 mal vorher ein catch, danach der offene DataReader

        Da Du in diesem Code keinen DataReader verwendest, würde ich annehmen, dass die Ursache nicht hier zu finden ist, sondern an der Stelle, wo die write-Funktion aufgerufen wird, ein DataReader offen ist.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Guten Morgen,

          das hier kein datareader ist verstehe ich, aber in der Konsole wird der Fehler mit dazugehöriger Fehlermeldung und Zeilennummer angegeben.

          Ist das bei C# normal, so kenne ich das nicht, das eine Fehlerausgabe/Fehlerzeile nicht zwingend richtig ist ?

          1. Hallo Henry,

            ein DB Command ist ein Highlander: Es kann nur eins geben (das gerade aktiv ist). Und ein DbDataReader ist aktiv, bis er geschlossen wird. D.h. du musst auf dem Reader Close() aufrufen oder Du musst ihn mit Dispose() verwerfen. Deswegen verwendet man die using-Anweisung: die führt den Dispose() für Dich automatisch aus, sobald der using-Block auf welchem Weg auch immer verlassen wird.

            In PHP ist das per Default etwas anders - siehe buffered mode. Deswegen sieht man dort nie einen close()-Aufruf auf einem Result.

            Dass auf der Connection noch ein DataReader offen ist, merkt .net erst, wenn eine neue Query ausgeführt werden soll. Deswegen fliegt die Exception beim ExecuteNonQuery Aufruf. Aber der Fehler ist früher passiert. Entweder hast Du vergessen, einen Reader zu schließen, oder du rufst die write-Methode in einer Schleife auf, wo Du einen Reader verarbeitest. Das geht nicht, dafür müsstest Du eine zweite Connection verwenden.

            Deswegen gilt für Connections in .net eigentlich das „heiße Kartoffel“-Prinzip: Halte sie nicht länger, als Du unbedingt musst.

            Also für jedes Command eine neue Connection holen?! Das ist doch viel zu langsam!

            Nein. Nicht in .net. Sowohl für den Treiber von MySQL als auch für den Treiber wie auch den mysqlconnector.net Treiber ist Connection Pooling per Default aktiv, d.h. wenn Du eine Connection schließt, wird sie tatsächlich NICHT geschlossen, sondern offengehalten und in einen Pool zurückgegeben. Der nächste Open()-Aufruf muss dadurch nicht die Runde über den DB-Server fliegen, sondern bekommt aufwandsarm eine offene Connection aus dem Pool.

            Das macht man in PHP normalerweise nicht so, ich weiß, aber da ist das Connection Pooling (bei mysqli persistent connections genannt) nicht implizit aktiv. Man kann es einschalten und auch da mit heißen Kartoffeln werfen. Aber das ist eine andere Geschichte und soll ein andermal erzählt werden…

            Deswegen könntest Du deine write-Methode so abändern:

            static bool write_thingy() {
               ...
            
               try 
               {
                  using var connection = new MySqlConnection(connectionstring);
                  using var command = new MySqlCommand(sqlstring, connection);
                  command.ExecuteNonQuery();
               }
               catch (Exception e)
               {
                  ...
               }
            }
            

            Damit verwenden die UPDATEs eine eigenständige Connection und können nicht mit einem Reader kollidieren. Kontrolliere trotzdem dort, wo Du die write-Methode aufrufst, ob Du vergessen hast, einen Reader abzuräumen.

            Aber – using mit Semikolon, also Deklaration statt Anweisung? Hab ich nich neulich noch was anderes erzählt? Schon, aber genau hier ist der Anwendungsfall für die using-Deklaration. Andernfalls brauchst Du zwei using-Blöcke (in einem using kann man nicht zwei Typen vermischen) und es ist genau richtig, dass der Dispose am Ende des try-Blocks erfolgt.

            Rolf

            --
            sumpsi - posui - obstruxi