Wird die Datenbankverbindung geschlossen, wenn wir die Datenreihenfolge abrufen und nicht alle Datensätze lesen?

14

Während yieldich verstand, wie ein Schlüsselwort funktioniert, bin ich bei StackOverflow auf Link1 und Link2 gestoßen, die die Verwendung von befürworten, yield returnwährend sie über den DataReader iterieren, und die auch meinen Anforderungen entsprechen. Aber ich frage mich, was passiert, wenn ich yield returnwie unten gezeigt verwende und nicht durch den gesamten DataReader iteriere. Bleibt die DB-Verbindung für immer offen?

IEnumerable<IDataRecord> GetRecords()
{
    SqlConnection myConnection = new SqlConnection(@"...");
    SqlCommand myCommand = new SqlCommand(@"...", myConnection);
    myCommand.CommandType = System.Data.CommandType.Text;
    myConnection.Open();
    myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
    try
       {               
          while (myReader.Read())
          {
            yield return myReader;          
          }
        }
    finally
        {
            myReader.Close();
        }
}


void AnotherMethod()
{
    foreach(var rec in GetRecords())
    {
       i++;
       System.Console.WriteLine(rec.GetString(1));
       if (i == 5)
         break;
  }
}

Ich habe das gleiche Beispiel in einer Beispiel-Konsolen-App ausprobiert und beim Debuggen festgestellt, dass der finally-Block von GetRecords()nicht ausgeführt wird. Wie kann ich dann die Schließung der DB-Verbindung sicherstellen? Gibt es einen besseren Weg als das Verwenden von yieldSchlüsselwörtern? Ich versuche, eine benutzerdefinierte Klasse zu entwerfen, die für die Ausführung ausgewählter SQL-Anweisungen und gespeicherter Prozeduren in der Datenbank verantwortlich ist und das Ergebnis zurückgibt. Ich möchte den DataReader aber nicht an den Aufrufer zurückgeben. Außerdem möchte ich sicherstellen, dass die Verbindung in allen Szenarien geschlossen wird.

Bearbeiten Die Antwort auf Bens Antwort wurde geändert, da nicht erwartet werden kann, dass Methodenaufrufer die Methode korrekt verwenden. In Bezug auf die DB-Verbindung ist es teurer, wenn die Methode ohne Grund mehrmals aufgerufen wird.

Danke Jakob und Ben für die ausführliche Erklärung.

GawdePrasad
quelle
Soweit ich weiß, öffnen Sie die Verbindung erst gar nicht
Paparazzo
@Frisbee Bearbeitet :)
GawdePrasad

Antworten:

12

Ja, Sie werden mit dem von Ihnen beschriebenen Problem konfrontiert: Bis Sie das Ergebnis durchlaufen haben, bleibt die Verbindung geöffnet. Ich kann mir zwei allgemeine Ansätze vorstellen, um damit umzugehen:

Schieben, nicht ziehen

Derzeit geben Sie IEnumerable<IDataRecord>eine Datenstruktur zurück, aus der Sie Daten abrufen können. Stattdessen können Sie Ihre Methode wechseln, um die Ergebnisse zu veröffentlichen. Der einfachste Weg wäre, eine zu übergeben, Action<IDataRecord>die bei jeder Iteration aufgerufen wird:

void GetRecords(Action<IDataRecord> callback)
{
    // ...
      while (myReader.Read())
      {
        callback(myReader);
      }
}

Beachten Sie, dass gegebenen Sie mit einer Sammlung von Gegenständen zu tun, IObservable/ IObservermöglicherweise eine etwas geeignete Datenstruktur sein, aber wenn Sie es brauchen, ein einfaches Actionist viel einfacher.

Eifrig auswerten

Eine Alternative besteht nur darin, sicherzustellen, dass die Iteration vollständig abgeschlossen ist, bevor Sie zurückkehren.

Normalerweise können Sie dies tun, indem Sie die Ergebnisse in eine Liste einfügen und diese dann zurückgeben. In diesem Fall ist es jedoch zusätzlich kompliziert, dass jedes Element dieselbe Referenz für den Leser darstellt. Sie benötigen also etwas, um das gewünschte Ergebnis aus dem Reader zu extrahieren:

IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
    // ...
     var result = new List<T>();
     try
     {
       while (myReader.Read())
       {
         result.Add(extractor(myReader));         
       }
     }
     finally
     {
         myReader.Close();
     }
     return result;
}
Ben Aaronson
quelle
Ich benutze den Ansatz, den Sie Eagerly Evaluate genannt haben. Wird es nun ein Speicher-Overhead sein, wenn der Daten-Reader meines SQLCommands mehrere Ausgaben zurückgibt (wie myReader.NextResult ()) und ich eine Liste mit Listen erstelle? Oder ist es sehr effizient, den Daten-Reader an den Anrufer zu senden (ich persönlich bevorzuge es nicht)? [aber die Verbindung wird länger offen sein]. Was mich hier genau verwirrt, ist der Kompromiss, die Verbindung länger offen zu halten, als eine Liste mit Listen zu erstellen. Sie können mit Sicherheit davon ausgehen, dass mehr als 500 Personen meine Webseite besuchen, die eine Verbindung zur DB herstellt.
GawdePrasad
1
@ GawdePrasad Es kommt darauf an, was der Anrufer mit dem Leser machen würde. Wenn nur Elemente in eine Sammlung aufgenommen werden, entsteht kein nennenswerter Overhead. Wenn es sich um eine Operation handelt, bei der nicht alle Werte gespeichert werden müssen, entsteht ein Speicheroverhead. Wenn Sie jedoch nicht sehr große Mengen speichern, ist dies wahrscheinlich kein Aufwand, um den Sie sich kümmern sollten. In jedem Fall sollte es keinen nennenswerten Zeitaufwand geben.
Ben Aaronson
Wie löst der "Push, Don't Pull" -Ansatz das Problem, dass die Verbindung offen bleibt, bis Sie das Ergebnis durchlaufen haben? Die Verbindung bleibt so lange bestehen, bis Sie die Iteration abgeschlossen haben, nicht wahr?
BornToCode
1
@BornToCode Siehe die Frage: "Wenn ich Yield Return wie unten gezeigt verwende und nicht durch den gesamten DataReader iteriere, bleibt die DB-Verbindung für immer offen." Das Problem ist, dass Sie eine IEnumerable zurückgeben, und jeder Anrufer, der das erledigt, muss sich dieses besonderen Problems bewusst sein: "Wenn Sie nicht fertig sind, über mich zu iterieren, halte ich eine Verbindung offen". Bei der Push-Methode entziehen Sie dem Anrufer diese Verantwortung - er hat keine Möglichkeit, sie teilweise zu iterieren. Wenn die Zeitsteuerung an sie zurückgegeben wird, wird die Verbindung geschlossen.
Ben Aaronson
1
Dies ist eine hervorragende Antwort. Ich mag den Push-Ansatz, denn wenn Sie eine große Abfrage haben, die Sie seitenweise präsentieren, können Sie den Lesevorgang aus Gründen der Geschwindigkeit abbrechen, indem Sie eine Funktion <> anstelle einer Aktion <> übergeben. Dies fühlt sich sauberer an, als wenn die GetRecords-Funktion auch Paging ausführt.
Whelkaholism
10

Ihr finallyBlock wird immer ausgeführt.

Wenn Sie yield returnden Compiler verwenden, wird eine neue verschachtelte Klasse erstellt, um einen Zustandsautomaten zu implementieren.

Diese Klasse enthält den gesamten Code aus finallyBlöcken als separate Methoden. Es verfolgt die finallyBlöcke, die abhängig vom Status ausgeführt werden müssen. Alle notwendigen finallyBlöcke werden in DisposeMethode ausgeführt.

Laut C # -Sprachenspezifikation foreach (V v in x) embedded-statementwird auf erweitert

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            embedded - statement
        }
    }
    finally {
         // Dispose e
    }
}

Der Enumerator wird also verworfen, auch wenn Sie die Schleife mit breakoder verlassen return.

Weitere Informationen zur Iterator-Implementierung finden Sie in diesem Artikel von Jon Skeet

Bearbeiten

Das Problem bei einem solchen Ansatz ist, dass Sie sich auf die Clients Ihrer Klasse verlassen, um die Methode korrekt zu verwenden. Sie könnten beispielsweise den Enumerator direkt abrufen und ihn in einer whileSchleife durchlaufen, ohne ihn zu entsorgen.

Sie sollten eine der von @BenAaronson vorgeschlagenen Lösungen in Betracht ziehen.

Jakub Lortz
quelle
1
Dies ist äußerst unzuverlässig. Beispiel: var records = GetRecords(); var first = records.First(); var count = records.Count()Diese Methode wird tatsächlich zweimal ausgeführt, wobei die Verbindung und der Reader jedes Mal geöffnet und geschlossen werden. Das zweimalige Durchlaufen bewirkt dasselbe. Sie können das Ergebnis auch lange weitergeben, bevor Sie es tatsächlich durchlaufen usw. Nichts in der Methodensignatur deutet auf diese Art von Verhalten hin
Ben Aaronson,
2
@BenAaronson In meiner Antwort habe ich mich auf die spezifische Frage nach der aktuellen Implementierung konzentriert. Wenn Sie sich das gesamte Problem ansehen, ist es meiner Meinung nach viel besser, die von Ihnen in Ihrer Antwort vorgeschlagene Methode zu verwenden.
Jakub Lortz
1
@ JakubLortz Fair genug
Ben Aaronson
2
@EsbenSkovPedersen Wenn Sie normalerweise versuchen, etwas idiotensicheres zu machen, verlieren Sie einige Funktionen. Sie müssen es ausgleichen. Hier verlieren wir nicht viel durch eifriges Laden der Ergebnisse. Jedenfalls ist das größte Problem für mich die Benennung. Wenn ich eine Methode namens GetRecordsreturning IEnumerablesehe, erwarte ich, dass sie die Datensätze in den Speicher lädt. Wenn es sich andererseits um eine Eigenschaft handelt Records, würde ich eher annehmen , dass es sich um eine Art Abstraktion über eine Datenbank handelt.
Jakub Lortz
1
@EsbenSkovPedersen Nun, siehe meine Kommentare zur Antwort von nvoigt. Ich habe im Allgemeinen kein Problem mit Methoden, die eine IEnumerable zurückgeben, die bei einer Iteration erhebliche Arbeit leistet, aber in diesem Fall scheint sie nicht den Implikationen der Methodensignatur zu entsprechen
Ben Aaronson,
5

Ungeachtet des spezifischen yieldVerhaltens enthält Ihr Code Ausführungspfade, über die Ihre Ressourcen nicht richtig verteilt werden. Was ist, wenn Ihre zweite Zeile eine Ausnahme auslöst oder Ihre dritte? Oder sogar dein vierter? Sie benötigen eine sehr komplexe try / finally-Kette, oder Sie können usingBlöcke verwenden.

IEnumerable<IDataRecord> GetRecords()
{
    using(var connection = new SqlConnection(@"..."))
    {
        connection.Open();

        using(var command = new SqlCommand(@"...", connection);
        {
            using(var reader = command.ExecuteReader())
            {
               while(reader.Read())
               {
                   // your code here.
                   yield return reader;
               }
            }
        }
    }
}

Die Leute haben bemerkt, dass dies nicht intuitiv ist und dass die Leute, die Ihre Methode aufrufen, möglicherweise nicht wissen, dass eine zweimalige Aufzählung des Ergebnisses die Methode zweimal aufruft. Pech gehabt. So funktioniert die Sprache. Das ist das Signal, das IEnumerable<T>sendet. Es gibt einen Grund, warum dies nicht zurückkommt List<T>oder T[]. Leute, die das nicht wissen, müssen erzogen und nicht umgangen werden.

Visual Studio verfügt über eine Funktion namens statische Code-Analyse. Sie können damit feststellen, ob Sie Ressourcen ordnungsgemäß entsorgt haben.

nvoigt
quelle
Die Sprache ermöglicht IEnumerablees, alle möglichen Dinge durchlaufen zu lassen, z. B. völlig unabhängige Ausnahmen auszulösen oder 5-GB-Dateien auf die Festplatte zu schreiben. Nur weil die Sprache dies zulässt, bedeutet dies nicht, dass es akzeptabel ist, eine Funktion IEnumerablevon einer Methode zurückzugeben, die keinen Hinweis darauf gibt, dass dies passieren wird.
Ben Aaronson
1
Eine Rücksendung IEnumerable<T> ist der Hinweis darauf , dass es zweimal Aufzählen in zwei Anrufen führen. Sie erhalten sogar hilfreiche Warnungen in Ihren Tools, wenn Sie eine Aufzählung mehrmals durchführen. Der Punkt zum Auslösen von Ausnahmen ist unabhängig vom Rückgabetyp gleichermaßen gültig. Wenn diese Methode a List<T>zurückgibt, werden die gleichen Ausnahmen ausgelöst.
19.
Ich verstehe Ihren Standpunkt, und bis zu einem gewissen Grad stimme ich Ihnen zu, wenn Sie eine Methode anwenden, die Ihnen eine IEnumerableEinschränkung bietet . Aber ich denke auch, dass Sie als Methodenschreiber die Verantwortung haben, sich an das zu halten, was Ihre Signatur impliziert. Die Methode wird aufgerufen GetRecords. Wenn sie zurückgegeben wird, haben Sie, wie Sie wissen, die Datensätze erhalten . Wenn es heißt GiveMeSomethingICanPullRecordsFrom(oder realistischer GetRecordSourceoder ähnlicher), dann wäre ein fauler IEnumerableakzeptabler.
Ben Aaronson
@BenAaronson ich zustimmen beispielsweise in File, ReadLinesund ReadAllLinesleicht auseinander gesagt werden kann , und das ist eine gute Sache. (Obwohl dies eher auf die Tatsache zurückzuführen ist, dass sich eine Methodenüberladung nicht nur nach dem Rückgabetyp unterscheiden kann). Vielleicht passen ReadRecords und ReadAllRecords auch hier als Namensschema.
Nvoigt
2

Was du getan hast, scheint falsch zu sein, wird aber gut funktionieren.

Sie sollten einen IEnumerator <> verwenden , da dieser auch IDisposable erbt .

Da Sie jedoch foreach verwenden, verwendet der Compiler weiterhin IDisposable , um einen IEnumerator <> für foreach zu generieren .

Tatsächlich beinhaltet der foreach eine Menge interner Dinge.

Überprüfen Sie /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use

Avlin
quelle