Ich habe ein kleines konzeptionelles Problem mit der Concurrency in einer Anwendung. Zwar nicht .NET-spezifisch, aber ggf. gibt es da mit ADO.NET oder O/R Mappern vielleicht out-of-the-box Lösungen.
Ich konstruiere mal ein allgemeines Beispiel: - Tabelle "ProductCategories" - Tabelle "Products"
Ein Nutzer bearbeitet oder erstellt gerade ein "Product" und selektiert dabei "ProductCategories" . Die Anwendung speichert die "ProductCategories" Zuordnung. Falls parallel (kurz vorher) von einem anderen Nutzer die ProductCategory gelöscht wird, wird es zu einer ForeignKey-Exception kommen.
Jetzt ist die Frage, wie man dies handhaben kann, sodass der Nutzer eine im Idealfall aussagekräftige Meldung erhält und weiß was los ist, aber zumindest dass die Anwendung nicht in einer rohen Exception landet, wobei ein generisches Abfangen von SQL-Exceptions die letzte Lösung sein sollte, wenn nichts an anderes hilft.
In einer klassischen Webanwendung wäre das Problem normalerweise wohl kleiner. Per Post wird eine CategoryID gesendet, die nicht mehr existiert, damit muss die Anwendung sowieso umgehen (aus Sicherheitsgründen) und wird diese einfach ignorieren.
In einer Desktop-Anwendung wäre das Problem schon größer, denn da gibt es eine Verarbeitung wie bei Webanwendungen nicht und man vertraut den Daten ja normal.
Was wären denn sinnvolle Lösungen? - Vor dem Einfügen prüfen ob das Zielobjekt existiert (langsam, aufwendig wenn überall implementiert?) - Den ErrorCode der SQL-Exception auf FK-Violations prüfen (hier allerdings Gefahr von falsch interpretierten Fehlern)
Ich finde, Du solltest das Problem nicht zuerst von der technischen Seite aus anpacken, sondern von der fachlichen. Je nachdem, um was es bei Deinen echten Daten geht, ist die eine oder die andere Lösung angezeigt: Transaktion abbrechen und Nutzer informieren, Datensatz mit Referenz auf eine auf eine nur gelöscht oder ungültig markierte Kategorie speichern, Datensatz ohne Referenz speichern, ... - das ist alles erst mal keine technische Fragestellung, sondern sollte sich daran orientieren, was im konkreten Fall wirklich inhaltlich sinnvoll ist.
Gute Antwort :) In meinem Fall ist das Auftreten unwahrscheinlich. Es würde ausreichen, wenn der Nutzer darüber informiert wird. Allerdings lag eben gerade das Problem darin zu unterscheiden -> FK Exception, weil wirklich Concurrency-Situation vorliegt (das ist kein Programmfehler, sondern ein erwartbares Verhalten, was z.B. nicht in den Errorlog gehört) oder FK Exception aus anderen Grund -> was ein Programmfehler ist und in den Errorlog gehört. Für meinen Fall war es nicht schlimm, diese Unterscheidung nicht treffen zu können und alles als generischen DB Fehler zu behandeln.
Eine einfache und saubere Lösung wäre es das ganze im SQL in einer Transaktion zu lösen:
create proc spAddProducts @PcId int, @Produkte dbo.ProductTable as begin try begin tran if not exists(select * from ProductCategories where id=@PcId) raiserror('Productcategorie ''%s'' existiert nicht', 16, 1, @PcId)
insert into Products (...) select @PcId, * from @Produkte
commit tran end tran end try begin catch if @@TRANCOUNT>0 rollback tran exec dbo.spRethrowError end catch GO
create PROCEDURE [dbo].[spRethrowError] AS -- Return if there is no error information to retrieve. IF ERROR_NUMBER() IS NULL RETURN ;
-- Assign variables to error-handling functions that -- capture information for RAISERROR. SELECT @ErrorNumber = ERROR_NUMBER(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(), @ErrorLine = ERROR_LINE(), @ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-') ;
-- Building the message string that will contain original -- error information. SELECT @ErrorMessage = N'Error %d, Level %d, State %d, Procedure %s, Line %d, ' + 'Message: ' + ERROR_MESSAGE() ;
-- Raise an error: msg_str parameter of RAISERROR will contain -- the original error information. RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber, -- parameter: original error number. @ErrorSeverity, -- parameter: original error severity. @ErrorState, -- parameter: original error state. @ErrorProcedure, -- parameter: original error procedure name. @ErrorLine-- parameter: original error line number. ) ;
Danke, das ist ziemlich viel Holz bei noch mehr Beziehungen (nutzt du dafür einen Generator?). Du ersetzt den generischen FK-Fehler (ermittelbar durch ErrorCode) durch einen Benutzer-definierten. Damit lässt sich aber nicht wirklich ermitteln, ob die FK-Violation durch einen Programmfehler oder auf Concurrency (kein Fehler, Situation) zurückzuführen ist?
Eine einfache aber dafür sehr effektive Variante besteht darin die Daten NICHT zu löschen, sondern einfach nur die (in Deinem Beispiel zu löschende) Kategorie über ein Status-Flag zu kennzeichnen: 0 = OK -1 = gelöscht
So macht das z.B. DotNetNuke, die ja eine Papierkorbfunktion haben, wo alle Objekte, die einen Status -1 haben dann endgültig gelöscht werden können.
Eine andere oft eingesetzte Möglichkeit ist das Setzen von Gültigkeitszeiträumen. Statt die Kategorie zu löschen, wird ein "Gültig-Bis"-Datum gesetzt. Deine UI kann das dann auswerten und solche Kategorien dann z.B. disablen oder durchgestrichen formatieren.
solche Soft Deletes habe ich in der Vergangenheit auch schon benutzt, aber sie verlagern das Problem eher. "Gültig-bis" ist denke ich eine wirklich gute Geschichte für so etwas wie Preise. Das Beispiel mit Product und ProductCategory war eher als Beispiel gemeint. In meinem konkreten Fall wäre "Gültig bis" da overkill
In meinem Fall ist das Auftreten unwahrscheinlich. Es würde ausreichen, wenn der Nutzer darüber informiert wird. Allerdings lag eben gerade das Problem darin zu unterscheiden -> FK Exception, weil wirklich Concurrency-Situation vorliegt (das ist kein Programmfehler, sondern ein erwartbares Verhalten, was z.B. nicht in den Errorlog gehört) oder FK Exception aus anderen Grund -> was ein Programmfehler ist und in den Errorlog gehört. Für meinen Fall war es nicht schlimm, diese Unterscheidung nicht treffen zu können und alles als generischen DB Fehler zu behandeln.