2009-05-11 9 views
10

Wir haben eine Menge von Datenschicht Code, der diese sehr allgemeine Muster folgt:Return Datareader von Datalayer in Using-Anweisung

public DataTable GetSomeData(string filter) 
{ 
    string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter"; 

    DataTable result = new DataTable(); 
    using (SqlConnection cn = new SqlConnection(GetConnectionString())) 
    using (SqlCommand cmd = new SqlCommand(sql, cn)) 
    { 
     cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter; 

     result.Load(cmd.ExecuteReader()); 
    } 
    return result; 
} 

Ich denke, wir können ein wenig besser machen. Meine Hauptbeschwerde ist, dass alle Datensätze in den Speicher geladen werden müssen, auch für große Sets. Ich möchte in der Lage sein, die Fähigkeit eines DataReaders zu nutzen, nur einen Datensatz in Ram zu einem Zeitpunkt zu behalten, aber wenn ich den DataReader direkt zurückschicke, wird die Verbindung abgeschnitten, wenn der using-Block verlassen wird.

Wie kann ich dies verbessern, um die Rückgabe einer Zeile zu einer Zeit zu ermöglichen?

+0

Aber nicht alle Datensätze in den Speicher im Allgemeinen besser als Laden mit einem Datareader eine Verbindung offen für die Datenbank zu halten? – codeulike

+0

Es kommt darauf an. Für eine winforms App, ja. Für eine Web-App, bei der Speicher knapp ist und Abfragen ohnehin schnell beendet werden müssen, wahrscheinlich nicht. –

+0

Können Sie genauer angeben, welchen "echten Vorteil von DataReader" Sie haben möchten? –

Antwort

13

Noch einmal, der Akt des Komponierens meiner Gedanken für die Frage enthüllt die Antwort. Insbesondere der letzte Satz, in dem ich "eine Zeile nach der anderen" geschrieben habe. Mir wurde klar, dass es mir wirklich egal ist, dass es ein Datenleser ist, solange ich ihn Zeile für Zeile aufzählen kann. Das führt mich dies zu:

public IEnumerable<IDataRecord> GetSomeData(string filter) 
{ 
    string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter"; 

    using (SqlConnection cn = new SqlConnection(GetConnectionString())) 
    using (SqlCommand cmd = new SqlCommand(sql, cn)) 
    { 
     cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter; 
     cn.Open(); 

     using (IDataReader rdr = cmd.ExecuteReader()) 
     { 
      while (rdr.Read()) 
      { 
       yield return (IDataRecord)rdr; 
      } 
     } 
    } 
} 

Dies funktioniert sogar besser, wenn wir bis 3,5 bewegen und können mit anderen LINQ-Operatoren auf die Ergebnisse starten, und Ich mag es, weil es uns aufstellt beginnen in Bezug auf ein Denken "Pipeline" zwischen jeder Ebene für Abfragen, die viele Ergebnisse zurückgeben.

Der Nachteil ist, dass es für Leser mit mehr als einer Ergebnismenge umständlich sein wird, aber das ist äußerst selten.

aktualisieren
Da ich mit diesem Muster zuerst im Jahr 2009 angefangen zu spielen, ich habe gelernt, dass es am besten ist, wenn ich es auch ein generischer IEnumerable<T> Rückgabetyp machen und fügen Sie einen Func<IDataRecord, T> Parameter, um den Datareader Zustand zu Business-Objekten konvertieren in die Schleife. Andernfalls kann es bei der verzögerten Iteration zu Problemen kommen, sodass Sie jedes Mal das letzte Objekt in der Abfrage sehen.

+0

Ich verwende eine ähnliche Implementierung für .NET 3.5. Es funktioniert gut, aber aus irgendeinem Grund fühle ich einen Missbrauch des Iterator-Pattern, aber das ist sehr subjektiv. Es wird wirklich unordentlich, wenn Sie eine Ausnahmebehandlung umbrechen wollen, die verhindert, dass dieser Block beendet wird, weil die Umwandlung aus irgendeinem Grund fehlschlägt. ;-) –

+1

@RunningMonkey: Da Sie dieses Muster live ausgeführt haben, haben Sie gesehen, dass die (IDataRecord) -Wiedergabe häufig fehlschlägt? Wie besorgt sollte ich sein? –

+0

Vielleicht nicht viel, weil meine Implementierung http://StackOverflow.com/Questions/47521/using-Yield-to-iterate-over-a-datareader-might-not-close-the-connection ähnlicher ist. Dort konvertiere ich mit Convert.ChangeType in Basistypen, bevor ich eine Renditerückgabe mache. Das weinte fast vor Schwierigkeiten. :-) –

7

Was Sie wollen, ist ein unterstütztes Muster, werden Sie

cmd.ExecuteReader(CommandBehavior.CloseConnection); 

verwenden, und entfernen Sie beide using() ‚s Ihre GetSomeData() -Methode bilden. Die Ausnahmesicherheit muss vom Anrufer bereitgestellt werden, um ein Schließen des Lesers zu gewährleisten.

0

Ich war nie ein großer Fan davon, dass die Datenschicht ein generisches Datenobjekt zurückgibt, da dies den ganzen Punkt, den Code in seine eigene Schicht zu trennen, ziemlich auflöst (wie kann man Datenschichten abschalten, wenn die Schnittstelle nicht ist) nicht definiert?).

Ich denke, Ihre beste Wette ist für alle Funktionen wie diese, um eine Liste von benutzerdefinierten Objekten zurückgeben, die Sie selbst erstellen, und in Ihren Daten später rufen Sie Ihre Prozedur/Abfrage in einen Datenreader und durchlaufen durch das Erstellen der Liste.

Dies erleichtert die allgemeine Handhabung (trotz der anfänglichen Zeit zum Erstellen der benutzerdefinierten Klassen), erleichtert die Handhabung Ihrer Verbindung (da Sie keine damit verbundenen Objekte zurückgeben) und sollte dies tun sei schneller. Der einzige Nachteil ist, dass alles in den Speicher geladen wird, wie du erwähnt hast, aber ich würde nicht denken, dass dies ein Grund zur Sorge wäre (wenn es so wäre, würde ich denken, dass die Abfrage angepasst werden müsste).

+1

Wir haben eine 4-Tier-Architektur. Die Datenschicht gibt generische Datenobjekte (DataTable, DataRow, DataReader) an eine Übersetzungsschicht zurück, die sie in stark typisierte Geschäftsobjekte konvertiert. Minimiert den Schaden. –

+0

Ich sehe. Ich nehme an, dass der einzige Nachteil darin besteht, die Schleife in beiden Schichten zu machen, um die Liste zu erstellen, aber ich würde denken, dass die Auswirkung minimal wäre. – John

+1

Das Tolle an IEnumerable ist, dass Sie die Daten nur einmal durchlaufen. Also ja, die Auswirkungen sollten nicht existent sein. –

3

In Zeiten wie diesen finde ich, dass Lambdas von großem Nutzen sein können.Betrachten Sie diese anstelle der Datenschicht uns die Daten geben, lassen Sie uns die Datenschicht unsere Datenverarbeitungsverfahren geben:

public void GetSomeData(string filter, Action<IDataReader> processor) 
{ 
    ... 

    using (IDataReader reader = cmd.ExecuteReader()) 
    { 
     processor(reader); 
    } 
} 

Dann wird die Business-Schicht würde es nennen:

GetSomeData("my filter", (IDataReader reader) => 
    { 
     while (reader.Read()) 
     { 
      ... 
     } 
    }); 
+0

.Net 2.0 für jetzt, aber ich werde dies für die nächste Aktualisierung berücksichtigen. –

+0

Eigentlich glaube ich nicht, dass dies funktionieren wird, weil ich möchte, dass diese die Kette in Richtung der Präsentationsschicht weitergegeben werden. Ich könnte das immer noch tun, indem ich dieses Muster innerhalb des Lambda wiederhole, aber das fühlt sich hässlich an. –

+0

Ich stimme zu. Andererseits tendiere ich dazu, den Datenleser so schnell wie möglich durchzuarbeiten und zu schließen und stattdessen die erforderlichen Daten auf der Datenschicht aufzubauen, wobei es den oberen Ebenen überlassen bleibt, wie diese Daten aussehen sollen. Mit dem Action-Prozessor diktiert die Business-Schicht das Format, während die Leserschleife so eng wie möglich gehalten wird (vorausgesetzt, dass die Business-Schicht in der Prozessormethode keinen zeitaufwendigen Prozess ausführt). –

2

Der Schlüssel ist yield Stichwort.

Ähnlich wie Joel ursprüngliche Antwort, etwas mehr konkretisiert:

public IEnumerable<S> Get<S>(string query, Action<IDbCommand> parameterizer, 
          Func<IDataRecord, S> selector) 
{ 
    using (var conn = new T()) //your connection object 
    { 
     using (var cmd = conn.CreateCommand()) 
     { 
      if (parameterizer != null) 
       parameterizer(cmd); 
      cmd.CommandText = query; 
      cmd.Connection.ConnectionString = _connectionString; 
      cmd.Connection.Open(); 
      using (var r = cmd.ExecuteReader()) 
       while (r.Read()) 
        yield return selector(r); 
     } 
    } 
} 

Und ich habe diese Erweiterung Methode:

public static void Parameterize(this IDbCommand command, string name, object value) 
{ 
    var parameter = command.CreateParameter(); 
    parameter.ParameterName = name; 
    parameter.Value = value; 
    command.Parameters.Add(parameter); 
} 

So nenne ich:

foreach(var user in Get(query, cmd => cmd.Parameterize("saved", 1), userSelector)) 
{ 

} 

Dies ist voll generisch, passt zu jedem Modell, das den ado.net-Schnittstellen entspricht. The connection and reader objects are disposed after the collection is enumerated. Füllung Auf jeden Fall ein DataTable mit IDataAdapter ‚s Fill Methode can be faster than DataTable.Load

+0

Ich bin ein wenig verwirrt durch Ihre Notiz, dass "das Verbindungsobjekt und der Leser erst nach der Auflistung angeordnet wird, wird einmal gezählt." Alles, was ich lese, einschließlich der SO Frage, die durch Ihren Satz verbunden ist, legt nahe, dass selbst wenn die Sammlung nicht vollständig aufgezählt ist (z. B. durch Ausbrechen einer "foreach", die sie verwendet), wird sie entsorgt. Wollen Sie damit sagen, dass dies nicht der Fall ist, oder zählt das Ausbrechen der Iteration, wenn die Sammlung einmal "aufgezählt" wird? Danke, dass Sie die Func Version übrigens ausgearbeitet haben. – bubbleking

+1

@bubbleking Ich habe dort eine irreführende Aussage gemacht. Ich habe versucht, den Punkt zu vermitteln, dass die Verbindungs- und Leserobjekte entsorgt werden, nachdem Sie die Liste aufgelistet haben - wenn Sie überhaupt aufzählen müssen - unabhängig davon, ob Sie aus der Schleife ausbrechen oder nicht. Wenn Sie nicht aufzählen, gibt es keine Frage von Verbindungsobjekt jemals verwendet wird. Ich werde die Antwort bearbeiten. Danke für das Aufzeigen. – nawfal