Benutzer mit den meisten Antworten
Frage zu SqlConnection, Transaction und Timer-Event bei einer WinForm-Applikation

Frage
-
Hallo
jetzt arbeite ich doch schon eine längere Zeit mit .NET, aber bei diesem Problem muss ich hier im Forum einmal nachfragen.
Ich habe eine bestehende WinForm-Applikation (C#) übernommen, die mit einer SQL-Server-Datenbank via ADO.NET arbeitet. Bei dieser Applikation wird in einer static-Helperklasse die SqlConnection abgelegt und damit überall in dieser WinForm-App. gearbeitet. Nun gibt es beim Mdi-Main-Fenster einen Timer-Event, der für Alert-Popup-Nachrichten bzw. für "Ticker"-Meldungen alle 30 Sekunden bzw. alle 60 Sekunden eine Abfage an die Datenbank schickt.
Das Problem ist folgendes - arbeitet der Benutzer in der Applikation gerade in einem Bereich wo auch mit einer SqlTransaction gearbeitet wird, und diese Transaktion dauert länger / bzw. durch eine MessageBox-Rückfrage sperrt der Benutzer (weil nicht gleich die Messagebox beantwortet wird), und hält die SqlTransaction länger offen - kann es dann beim TimerEvent zu Problemen kommen. Meist mit der Fehlermeldung "....der Befehl muss über eine Transaktion verfügen..."
Jetzt meine Frage - ist es gut, wenn die Abfragen aus dem Timer-Event über eine eigene SqlConnection laufen ? Bringt das überhaupt etwas ?
Oder (anders gefragt) wie können überhaupt am besten die Abfragen im Timer-Event gelöst werden?
Besten Dank schon mal für eine Info dazu & schönen Gruß
Michael
Michael Erlinger
Antworten
-
Hallo Michael,
das Problem ist eher:
Während einer Transaktion zeigt man keine modalen Dialoge an - vielmehr vermeidet man generell alles, was den Abschluss der Datenbanktransaktion in die Länge zieht.Ob dabei ein Timer zum Überlaufen gebracht wird, ist eher Nebensache. Denn eine Transaktion sperrt ggf. Datenzeilen in der Datenbank kann auch andere Anwender blockieren[1].
Das es zur Fehlermeldung kommt weist darauf hin, dass Du die SqlTansaction nicht zuweist. Ist eine Transaktion offen, muss sie jedem SqlCommand zugewiesen werden, dass über die Verbindung ausgeführt wird. Ein Commit (oder Rollback) muss am Ende erfolgen.
Lösung:
Was die Verbindung angeht: Eine "globale" Verbindung bringt eher Nachteile - hier verschärft, dass mit Transaktionen gearbeitet wird. Das Erzeugen von lokalen Verbindungen ist vorzuziehen. Man kann es dem Connection Pool überlassen Verbindungen zu verwalten.
Stricke den Code so um, dass die Abfrage und Transaktion abgeschlossen wird. Ist eine Aktion des Anwenders erforderlich, die eine Datenänderung bewirkt, mache das in einer zweiten (Trans)Aktion.
Gruß Elmar
[1] Auch Dein Timeout-Problem könnte daher kommen (und Deine "Lösung" ist mehr eine Symptombehandlung) .
- Als Antwort vorgeschlagen Elmar BoyeEditor Dienstag, 11. Juni 2013 09:59
- Als Antwort markiert Marcel RomaModerator Mittwoch, 26. Juni 2013 14:01
-
Hallo zusammen,
Statische Klassen sind per se nicht böse. In einer geeigneten Archtitektur eingebunden (klass. Beispiel das dependency-System in WPF), können sie hilfreich sein.
Im Fall einer Connection aber (ob ADO.NET oder das darauf aufbauende EF) sollte man immer bestrebt sein die Verbindung kurz vor der DB-Operation zu öffnen und anschließend zu schließen *und* das Connection-Objekt freizugeben. Auf diesem Hintergrund gesehen, ist die statische Verwaltung einer Connection kontraproduktiv.
Der von Michael genannte Fehler (der Befehl muss über eine Transaktion verfügen) rührt daher, dass die selbe Verbindung mit und gleichzeitig ohne Transaktion verwendet wird. Da hilft - wie Elmar schon schrieb - alle Operationen transaktionell auszuführen.
Falls aber der Datenbankzugriff konkurrent aus mehreren Threads ausgeführt wird (welchen Timer verwendet ihr?), kann es sowohl zu Synchronisierungsproblemen als auch zu DB-Deadlocks kommen, wenn verschiedene Transaktionen Sperren einrichten, welche die jeweils andere Transaktion an der erfolgreichen Ausführung hindern.
Ich fürchte daher, im konkreten Fall kommt man mit schnellen "Patches" nicht wirklich zum Ziel. Die Datenzugriffsarchitektur muss neu durchdacht werden, und ein solides Refactoring muss her.
Gruß
Marcel- Als Antwort vorgeschlagen Marcel RomaModerator Dienstag, 11. Juni 2013 09:00
- Als Antwort markiert Marcel RomaModerator Mittwoch, 26. Juni 2013 14:01
-
Hallo Michael,
Refactoring wie Marcel es vorschlug ist zwar der bessere Weg, aber zugegeben nicht immer machbar, sei es aus zeitlichen und/oder finanziellen Gründen.
Was Du aber versuchen solltest, wäre die identifizierten kritischen Stellen zu bereinigen; denn so wie es von hier erkennbar ist, würde ich es als Fehler in der Software einstufen.
Eine kleinere Lösung wäre das Auslagern des Datenzugriffsmethoden in eigene Klassen, und sei es in statischer CRUD Logik. So kann vermieden werden, das Aufrufe wie MessageBox(en) verwendet werden - wenn man die eine Grundregel beherzigt, das Daten-Zugriffsklassen keinen Oberflächencode oder Verweise auf übergeordnete (Business-)Klassen haben dürfen.
Führst Du dabei sukzessive die Zugriffe je Tabelle / Sicht / Domäne zusammen erhältst Du langfristig besser wartbaren Code.
Gruß Elmar
P. S.: Zum anderen schreibe ich im Original-Thread, damit es zusammen bleibt.
- Bearbeitet Elmar BoyeEditor Freitag, 31. Mai 2013 17:51
- Als Antwort vorgeschlagen Marcel RomaModerator Dienstag, 11. Juni 2013 09:00
- Als Antwort markiert Marcel RomaModerator Mittwoch, 26. Juni 2013 14:01
Alle Antworten
-
Hallo Michael,
das Problem ist eher:
Während einer Transaktion zeigt man keine modalen Dialoge an - vielmehr vermeidet man generell alles, was den Abschluss der Datenbanktransaktion in die Länge zieht.Ob dabei ein Timer zum Überlaufen gebracht wird, ist eher Nebensache. Denn eine Transaktion sperrt ggf. Datenzeilen in der Datenbank kann auch andere Anwender blockieren[1].
Das es zur Fehlermeldung kommt weist darauf hin, dass Du die SqlTansaction nicht zuweist. Ist eine Transaktion offen, muss sie jedem SqlCommand zugewiesen werden, dass über die Verbindung ausgeführt wird. Ein Commit (oder Rollback) muss am Ende erfolgen.
Lösung:
Was die Verbindung angeht: Eine "globale" Verbindung bringt eher Nachteile - hier verschärft, dass mit Transaktionen gearbeitet wird. Das Erzeugen von lokalen Verbindungen ist vorzuziehen. Man kann es dem Connection Pool überlassen Verbindungen zu verwalten.
Stricke den Code so um, dass die Abfrage und Transaktion abgeschlossen wird. Ist eine Aktion des Anwenders erforderlich, die eine Datenänderung bewirkt, mache das in einer zweiten (Trans)Aktion.
Gruß Elmar
[1] Auch Dein Timeout-Problem könnte daher kommen (und Deine "Lösung" ist mehr eine Symptombehandlung) .
- Als Antwort vorgeschlagen Elmar BoyeEditor Dienstag, 11. Juni 2013 09:59
- Als Antwort markiert Marcel RomaModerator Mittwoch, 26. Juni 2013 14:01
-
Hallo Elmar
danke für die Rückmeldung.
Das Timeout-Problem hat damit nichts zu tun (außerdem glaube ich schon gelöst zu haben) - und diese Applikation verwendet ausschließlich Entity-Framework - dieses Problem betrifft eine ganz andere Applikation.
Wie gesagt - leider ist das eine Anwendung die ich übernommen habe -und somit schon einiges Programmier-KnowHow von anderen Leuten d'rinnen.
Das habe ich mir schon gedacht, dass die Transaktionen mit einem modalen Dialog sicherlich schlecht sind; nur ist das jetzt auf die schnelle in der gesamten App. nicht so einfach auszubessern. Auch das eine "globale" Verbindung vorhanden ist.....hatte ich ebenfalls schon in Verdacht.....und so schnell nicht aus der Welt zu schaffen.
Jetzt war halt meine Idee - mich ausschließlich auf den "Timer" zu konzentrieren, und dort eine eigene lokale Verbindung zu verwenden. Nur weiß ich eben nicht - wenn die "globale" Verbindung in der Transaktion steht (und zum Beispiel ein Dialog mit dem Benutzer offen ist) - wie ist dann das Verhalten einer 2. oder 3. lokalen Verbindung.....??
Weißt Du vielleicht dazu mehr.
Danke & schönen Gruß
Michael
Michael Erlinger
-
Hallo zusammen,
Statische Klassen sind per se nicht böse. In einer geeigneten Archtitektur eingebunden (klass. Beispiel das dependency-System in WPF), können sie hilfreich sein.
Im Fall einer Connection aber (ob ADO.NET oder das darauf aufbauende EF) sollte man immer bestrebt sein die Verbindung kurz vor der DB-Operation zu öffnen und anschließend zu schließen *und* das Connection-Objekt freizugeben. Auf diesem Hintergrund gesehen, ist die statische Verwaltung einer Connection kontraproduktiv.
Der von Michael genannte Fehler (der Befehl muss über eine Transaktion verfügen) rührt daher, dass die selbe Verbindung mit und gleichzeitig ohne Transaktion verwendet wird. Da hilft - wie Elmar schon schrieb - alle Operationen transaktionell auszuführen.
Falls aber der Datenbankzugriff konkurrent aus mehreren Threads ausgeführt wird (welchen Timer verwendet ihr?), kann es sowohl zu Synchronisierungsproblemen als auch zu DB-Deadlocks kommen, wenn verschiedene Transaktionen Sperren einrichten, welche die jeweils andere Transaktion an der erfolgreichen Ausführung hindern.
Ich fürchte daher, im konkreten Fall kommt man mit schnellen "Patches" nicht wirklich zum Ziel. Die Datenzugriffsarchitektur muss neu durchdacht werden, und ein solides Refactoring muss her.
Gruß
Marcel- Als Antwort vorgeschlagen Marcel RomaModerator Dienstag, 11. Juni 2013 09:00
- Als Antwort markiert Marcel RomaModerator Mittwoch, 26. Juni 2013 14:01
-
Hallo Marcel
ja - leider ist das in dieser Applikation nicht passiert. Hier wurde eine Applikation aus Access mit VB nachgebaut im .NET schon vor zig Jahren. Die "Datenzugriffsarchitektur" ist keine wirkliche Architektur.....
Jetzt wird komplett mit der Statischen-Klasse - und der darin gehaltenen Verbindung - gearbeitet; je nach Bedarf manchmal eine SqlTransaction begonnen (wenn mehrere Tabellen bei Updates betroffen sind) usw.
Timer wird der Standard-Timer "System.Windows.Forms.Timer" verwendet.
Also Du meinst, dass in der Mdi-Form für die Timer-Datenbankzugriffe eine 2. oder 3. lokale SQL-Verbindung (die ich mit using() {....} verwenden würde) nichts bringt (auf die schnelle).
Gruß
Michael
Michael Erlinger
-
Hallo Michael,
Also Du meinst, dass in der Mdi-Form für die Timer-Datenbankzugriffe eine 2. oder 3. lokale SQL-Verbindung (die ich mit using() {....} verwenden würde) nichts bringt (auf die schnelle).
Damit könntest Du evtl. den angezeigten Fehler beheben, aber Du handelst dir vermutlich viel mehr Ärger ein in Form von datenbankseitigen Deadlocks. Aber das hängt m.E. vom Inhaltlichen ab, d.h. davon, was genau diese neuen Abfragen tun. Die Server-Last wird dadurch erhöht und der Datenbankserver könnte bei vielen Clients und unzureichenden Ressourcen in die Knie gehn.
Gruß
Marcel -
Hallo Michael,
am Timer rumbasteln bringt nun wirklich nichts.
Wenn das Design auf einer statische Verbindung basiert und das nicht so auf die Schnelle änderbar ist, wirst Du die Stellen überprüfen müssen, die mit Transaktionen arbeiten.
Und sicherstellen, das sie zum einen die Transaktion abschließen (Commit/Rollback) und die Verbindung freigeben, also keine offenen DataReader mehr haben (das ist implizit für die Datenbank auch eine Transaktion).
Und Dialoge (oder auch anderer Zeitvertreib wie längere Schleifen) nicht Transaktionen in die Länge ziehen.
Für einige Szenarien mag es dabei einfacher sein, eine weitere Verbindung zu erzeugen. Wenn Du der jetzige Klasse eine Methode verpasst, die eine weitere Verbindung erzeugt, wie z. B.:
public static SqlConnection CreateConnectio() { return new SqlConnection("Identischer ConnectionString zur globaelen"); }
kannst Du den Code in etwa so umformulieren:
using(var localConnection = GlobaleKlasse.CreateConnection()) { // ... }
wobei dort Transaktionen etc. stattfinden können.
Potentielle Stolperfalle: Zwei separate Verbindungen werden aus Sicht des SQL Servers wie zwei Anwendungsinstanzen behandelt, d. h. sie können sich gegenseitig blockieren oder gar zum Deadlock führen.
Gruß Elmar
P.S.: Deine "Lösung" bei der anderen EF Anwendung ist vermutlich keine - aber das mag die Zeit zeigen.
- Bearbeitet Elmar BoyeEditor Freitag, 31. Mai 2013 11:59
-
Hallo Elmar & Marcel
Danke wieder mal für Eure Rückmeldungen und Infos!! Hat mir auf jeden Fall einmal weitergeholfen.
Mir ist schon klar, dass d. Konzept auf der aktuellen Basis keine Zukunft hat; aber der Aufwand muss natürlich auch mit dem Kunden abgeklärt werden. Und reines "Refactoring" bei dem der Kunde im Vordergrund gar nichts bemerkt ist immer schwer mit jemand zu besprechen, der keine Ahnung davon hat. Aber das ist eine andere Geschichte.....
@Elmar -> noch zu Deinem PS (was aber nicht das Thema dieses Threads ist) - warum vermutest d. es keine Lösung ist, das CommandTimeout hinaufzusetzen.
Schönen Gruß - Michael
Michael Erlinger
-
Hallo Michael,
Refactoring wie Marcel es vorschlug ist zwar der bessere Weg, aber zugegeben nicht immer machbar, sei es aus zeitlichen und/oder finanziellen Gründen.
Was Du aber versuchen solltest, wäre die identifizierten kritischen Stellen zu bereinigen; denn so wie es von hier erkennbar ist, würde ich es als Fehler in der Software einstufen.
Eine kleinere Lösung wäre das Auslagern des Datenzugriffsmethoden in eigene Klassen, und sei es in statischer CRUD Logik. So kann vermieden werden, das Aufrufe wie MessageBox(en) verwendet werden - wenn man die eine Grundregel beherzigt, das Daten-Zugriffsklassen keinen Oberflächencode oder Verweise auf übergeordnete (Business-)Klassen haben dürfen.
Führst Du dabei sukzessive die Zugriffe je Tabelle / Sicht / Domäne zusammen erhältst Du langfristig besser wartbaren Code.
Gruß Elmar
P. S.: Zum anderen schreibe ich im Original-Thread, damit es zusammen bleibt.
- Bearbeitet Elmar BoyeEditor Freitag, 31. Mai 2013 17:51
- Als Antwort vorgeschlagen Marcel RomaModerator Dienstag, 11. Juni 2013 09:00
- Als Antwort markiert Marcel RomaModerator Mittwoch, 26. Juni 2013 14:01
-
Hallo Marcel
Danke der Nachfrage - ja die Antworten bzw. die Diskussion darüber hat mir weitergeholfen; ich gehe es halt an, Modul für Modul bzw. Klasse für Klasse umzustellen auf eine using()-Anweisung mit der SqlConnection.
Auf lange Sicht, bringt alles andere hier nichts.......
Danke & schönen Gruß
Michael
Michael Erlinger