Laden...

LINQ, Datareader schließen

Erstellt von phxql vor 14 Jahren Letzter Beitrag vor 14 Jahren 1.728 Views
P
phxql Themenstarter:in
12 Beiträge seit 2007
vor 14 Jahren
LINQ, Datareader schließen

DBMS: SQL-Server 2000

Hallo Leute,

ich habe folgendes Problem:

ich habe eine Datenmenge (ca. 40000 Datensätze), die ich in einem DGV mit VirtualMode = true anzeige. Geholt werden die Datensätze durch ein LINQ-Statement.

Mein Problem ist nun, dass der DataReader nicht geschlossen wird, weil ich die Daten nicht über .ToList() in den Speicher lade, sondern mit dem Enumerator auf der von LINQ zurückgegeben Datenmenge arbeite.

Wie kann ich nun LINQ dazu bringen, den drunterliegenden DataReader zu schließen?

Grüße, phXql

1.274 Beiträge seit 2005
vor 14 Jahren

Wie machst du den den DataReader auf?

"Das Problem kennen ist wichtiger, als die Lösung zu finden, denn die genaue Darstellung des Problems führt automatisch zur richtigen Lösung." Albert Einstein

P
phxql Themenstarter:in
12 Beiträge seit 2007
vor 14 Jahren

Manuell gar nicht. Das macht der ObjectContext für mich:

var result = from k in Context.Bla orderby k.Field1 select k;

Damit is ein DataReader geöffnet, denn wenn ich danach nochmal so eine LINQ-Abfrage ausführ, fliegt mir die berühmte "Es ist noch ein DataReader offen"-Exception um die Ohren.

1.564 Beiträge seit 2007
vor 14 Jahren

Hallo phxql

Kann sein, dass das mit deiner Version von SQL Server zusammenhängt. LINQ2SQL war, meines Wissens nach, für SQL Server 2005 und höher konzipiert. Ich kann mir vorstellen, dass LINQ2SQL davon ausgeht, dass dein SQL Server MARS (Multiple Active Result-Sets) unterstützt, was bei SQL Server 2000 nicht der Fall ist/war.

Grüße
Flo

Blog: Things about Software Architecture, .NET development and SQL Server
Twitter
Google+

Je mehr ich weiß, desto mehr weiß ich was ich noch nicht weiß.

5.299 Beiträge seit 2008
vor 14 Jahren

@phxl:
kannst du mal das generierte SQL loggen und posten?
Interessiert mich, wie der das macht, dass im VirtualMode nur die Datensätze abgerufen werden, die im Grid anzuzeigen sind.

Müssteja bei jedem Scrollen eine spezifische Query absetzen, eben nicht die 40000 Datensätze

Der frühe Apfel fängt den Wurm.

P
phxql Themenstarter:in
12 Beiträge seit 2007
vor 14 Jahren

Hier das erzeugte SQL-Statement:

SELECT 
1 AS [C1], 
[Extent1].[Id] AS [Id], 
[Extent1].[Datum] AS [Datum], 
[Extent1].[KNr] AS [KNr], 
[Extent1].[Aktion] AS [Aktion], 
1 AS [C2], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Kennung] AS [Kennung], 
[Extent2].[Passwort] AS [Passwort], 
[Extent2].[Salt] AS [Salt], 
[Extent2].[Name] AS [Name], 
[Extent2].[Vorname] AS [Vorname], 
[Extent2].[Aktiv] AS [Aktiv], 
[Extent2].[GruppenId] AS [GruppenId]
FROM  [dbo].[KurseTempLog] AS [Extent1]
LEFT OUTER JOIN [dbo].[Benutzer] AS [Extent2] ON [Extent1].[BenutzerId] = [Extent2].[Id]

Anscheinent holt er alle Daten vom Server, ich habe das Grid aber so implementiert, dass der Enumerator nur immer soweit läuft, wie Zeilen angezeigt werden. Wenn ich auf dieser Datenmenge .ToList() aufrufe, dann dauert das relativ lange (> 15 Sek.), und das ist einfach nicht hinnehmbar für eine Anzeige im DGV.

Das funktioniert ja alles schon ganz toll, 40k Datensätze ohne Zeitverlust anzuzeigen, aber das Problem ist halt, dass dieser zu Grunde liegende Datareader noch offen ist, und ich den irgendwie zubekommen will.

Ich könnte es über ObjectContext.Connection.Close() machen, aber gleich die gesamte Verbindung beenden nur weil ich einen Reader schließen will? Das kann es ja nich sein.

5.299 Beiträge seit 2008
vor 14 Jahren

Findich komisch. Wenner eh alle Daten vom Server holt, sollte ein angehängtes .ToList() da auch keinen Unterschied mehr machen.

Der frühe Apfel fängt den Wurm.

P
phxql Themenstarter:in
12 Beiträge seit 2007
vor 14 Jahren

Doch. Weil er die Objekte konstruieren muss. So is das in einer DataTable oder sowas drin, und wenn ich ToList() mache, macht er mir eine Liste von Objekten. Und das dauert.

1.564 Beiträge seit 2007
vor 14 Jahren

Hi

Grund ist, dass der DataReader einen sogenannten Firehouse Cursor darstellt. Bedeutet es handelt sich hierbei um einen serverseitigen, vorwärts gerichteten, read-only Cursor. (In T-SQL ein FAST_FORWARD Cursor)

Funktioniert so, dass die für das angegebene SELECT Statement zutreffenden Daten nicht sofort komplett zum Client transportiert, sondern erst mit DataReader.Read() zeilenweise abgerufen werden.

Der Aufruf von IList.ToList() oder die Verwendung einer DataTable führt dazu dass intern der komplette Reader durchlaufen wird und die Daten an den Client gesendet werden. Daher die plötzliche Wartezeit.

Wenn man jetzt Daten mit L2S selektiert, über diese loopt und versucht irgendwelche Referenz-Daten nachzuladen bekommt man den anfangs genannten Fehler. Liegt daran, dass SQL Server 2000 - wie bereits gesagt - kein MARS unterstützt.

Grüße
Flo

Blog: Things about Software Architecture, .NET development and SQL Server
Twitter
Google+

Je mehr ich weiß, desto mehr weiß ich was ich noch nicht weiß.

P
phxql Themenstarter:in
12 Beiträge seit 2007
vor 14 Jahren

Hi,

danke für die Erklärung. Aber wie krieg ich den DataReader nun wieder zu?

1.564 Beiträge seit 2007
vor 14 Jahren

Wie du auch schon gesagt hast, mit ToList() oder einer DataTable sollte der Reader zugehen. Dauert halt seine Zeit.

Weitere Optionen:
* Keine 40.000 in ein Grid laden. Ich kenne keinen User der die durchschauen würde. Vielleicht Filter-Möglichkeiten einbauen
* Komplett eigenes Paging implementieren.
* Hybrid-Lösung: Nur die IDs der Zeilen direkt laden und die Daten erst wenn benötigt nachladen.
* GBit Netzwerk, dicken Server und dicke Client-PCs 😉
* Auf SQL Server 2005 oder direkt 2008 umsteigen. Ende September läuft bei Microsoft eh der Support für SQL Server 2000 aus.

Blog: Things about Software Architecture, .NET development and SQL Server
Twitter
Google+

Je mehr ich weiß, desto mehr weiß ich was ich noch nicht weiß.

P
phxql Themenstarter:in
12 Beiträge seit 2007
vor 14 Jahren

Mhh okay. Danke euch allen!

1.274 Beiträge seit 2005
vor 14 Jahren

Hi Florian,

mir gefallen deine Ansätze. Muss dir recht geben.

Paging wird nicht funktionieren, da es sich ja um DataGrid handelt, müsste man das Control erweitern.

und die Idee mit den ID's ist vorallem wenn die Datenbank <> Client ist, genial.
Muss ich mich da an einen Event vom GridView hängen, und dann manuell die Daten abfragen?

Lg
LastGentleman

"Das Problem kennen ist wichtiger, als die Lösung zu finden, denn die genaue Darstellung des Problems führt automatisch zur richtigen Lösung." Albert Einstein

1.564 Beiträge seit 2007
vor 14 Jahren

Hi LastGentleman

Danke für dein Feedback. 😃

Paging geht zwar, ist aber umständlich und Fehleranfällig. Man müsste sich statt den eigentlichen Daten nur die Anzahl der Ergebniszeilen selektieren und dann abhängig vom Scrollen die aktuell sichtbaren Zeilen nachladen (wenn diese nicht schon geladen sind). Funktioniert aber nur wenn eine fixe Sortierung der Daten sichergestellt ist und mindestens ein UNIQUE Index mit einer einzigen Spalte existiert.

Das mit den IDs ist deutlich einfacher zu realisieren, aber immer noch etwas tricky. Erstmal selektiert man für den angegebenen Filter nur die IDs, nicht die gesamten Zeilen und hält sich diese im Client.

Man kann die Sache einerseits in der GUI "abladen" und beim DGV den Scroll Event handeln. Wenn gescrollt wird holt man sich über FirstDisplayedScrollingRowIndex und DisplayedRowCount die aktuell angezeigten Zeilen und prüft ob die Werte bereits geladen wurden, wenn nicht werden die Daten on-demand nachgeladen. Hierbei würde ich von einzelnen SELECT Statements oder IN-Kriterien absehen, da die Performance so recht schlecht ist und den Server strapaziert. Stattdessen die zu selektierenden IDs z.B. über Komma zusammenhängen, serverseitig splitten und über einen JOIN auf einen Schlag alle Werte selektieren. Ist wesentlich schneller! Hierfür kann ich dir eine T-SQL oder SQLCLR basierte Lösung posten (SQLCLR ist nochmal schneller als T-SQL, aber derzeit noch recht unbeliebt bei DBAs 😉 ).

Eine andere Möglichkeit wäre eine Listen-Klasse die sich um das Nachladen kümmert. Leider kommt man da an einer eigenen Liste nicht vorbei, weil die bestehenden Klassen keine Möglichkeit keine Möglichkeit bieten den Indexer[]_Get zu überschreiben um den aktuellen Datensatz nachzuladen. Wenn es nicht möglich sein muss Zeilen löschen oder hinzufügen zu können, muss man "nur" eine IList implementieren. Wenn das nicht reicht kommt man wohl nicht dran vorbei eine komplette IBindingList zu implementieren. Habe ich hinter mir, ist aber echt aufwändig... In beiden Fällen lädt man in die Liste entweder nur leere Objekte oder füllt sie mit NULL Werten. Beim Indexer[]_Get nun prüfen ob der angeforderte Datensatz bereits geladen ist, sonst wird on-demand nachladen.

Grüße
Flo

Blog: Things about Software Architecture, .NET development and SQL Server
Twitter
Google+

Je mehr ich weiß, desto mehr weiß ich was ich noch nicht weiß.