Sollte ich überprüfen, ob etwas in der Datenbank vorhanden ist und schnell ausfallen oder auf eine Datenbankausnahme warten?

32

Zwei Klassen haben:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

Soll ich beim Zuweisen ChildIdzu Parentzuerst überprüfen, ob es in der Datenbank vorhanden ist, oder warten, bis die Datenbank eine Ausnahme auslöst?

Zum Beispiel (mit Entity Framework Core):

HINWEIS: Diese Überprüfungsarten sind auch in den offiziellen Microsoft-Dokumenten im gesamten Internet verfügbar: https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- mvc / handling-concurrency-with-the-entity-framework-in-einem-asp-net-mvc-application # Abteilung-Controller modifizieren, aber es gibt zusätzliche Ausnahmebehandlung fürSaveChanges

Beachten Sie außerdem, dass die Hauptabsicht dieser Überprüfung darin bestand, dem Benutzer der API eine angezeigte Nachricht und einen bekannten HTTP-Status zurückzugeben und Datenbankausnahmen nicht vollständig zu ignorieren. Und der einzige Ort, an dem eine Ausnahme ausgelöst wird, ist inside SaveChangesoder SaveChangesAsynccall ... also gibt es keine Ausnahme, wenn Sie FindAsyncoder anrufen Any. Wenn also ein untergeordnetes Element vorhanden ist, das jedoch zuvor gelöscht wurde, SaveChangesAsyncwird eine Nebenläufigkeitsausnahme ausgelöst.

Dies geschah aufgrund der Tatsache, dass foreign key violationdie Formatierung der Ausnahme für die Anzeige "Kind mit der ID {parent.ChildId} konnte nicht gefunden werden" erheblich schwieriger sein wird.

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    // is this code redundant?
   // NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

gegen:

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync();  // handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

Das zweite Beispiel in Postgres löst einen 23503 foreign_key_violationFehler aus: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html

Der Nachteil der Behandlung von Ausnahmen auf diese Weise in ORM wie EF ist, dass sie nur mit einem bestimmten Datenbank-Backend funktionieren. Wenn Sie jemals zu SQL Server oder etwas anderem wechseln wollten, funktioniert dies nicht mehr, da sich der Fehlercode ändert.

Wenn die Ausnahme für den Endbenutzer nicht richtig formatiert wird, können einige Dinge angezeigt werden, die nur von Entwicklern angezeigt werden sollen.

Verbunden:

https://stackoverflow.com/questions/6171588/preventing-race-condition-of-exists-update-else-insert-in-entity-framework

https://stackoverflow.com/questions/4189954/implementing-if-not-exists-insert-using-entity-framework-without-race-conditions

https://stackoverflow.com/questions/308905/sollte-es-ein-Transaktion-für-Leseanfragen sein

Konrad
quelle
2
Das Teilen Ihrer Forschung hilft allen . Sagen Sie uns, was Sie versucht haben und warum es nicht Ihren Bedürfnissen entsprach. Dies zeigt, dass Sie sich die Zeit genommen haben, um sich selbst zu helfen, es erspart uns, offensichtliche Antworten zu wiederholen, und vor allem hilft es Ihnen, eine spezifischere und relevantere Antwort zu erhalten. Siehe auch Wie man fragt
Mücke
5
Wie bereits erwähnt, besteht die Möglichkeit, dass ein Datensatz gleichzeitig mit Ihrer Überprüfung auf NotFound eingefügt oder gelöscht wird. Aus diesem Grund erscheint die Überprüfung zunächst als inakzeptable Lösung. Wenn Sie Postgres-spezifische Ausnahmebehandlungen schreiben möchten, die nicht auf andere Datenbank-Backends übertragbar sind, versuchen Sie, den Ausnahmebehandler so zu strukturieren, dass die Kernfunktionalität durch datenbankspezifische Klassen (SQL, Postgres usw.)
billrichards
3
Wenn ich mir die Kommentare ansehe, muss ich Folgendes sagen: Hör auf, in Plattitüden zu denken . "Fail fast" ist keine isolierte, nicht kontextbezogene Regel, die blind befolgt werden kann oder sollte. Es ist eine Faustregel. Analysieren Sie immer, was Sie tatsächlich erreichen möchten, und überlegen Sie sich dann, welche Technik Ihnen dabei hilft, dieses Ziel zu erreichen. "Fail fast" hilft Ihnen, unbeabsichtigte Nebenwirkungen zu vermeiden. Und außerdem bedeutet "schnell scheitern" wirklich "scheitern, sobald Sie feststellen, dass ein Problem vorliegt". Beide Techniken schlagen fehl, sobald ein Problem erkannt wird. Sie müssen sich also andere Überlegungen ansehen.
jpmc26
1
@Konrad was haben Ausnahmen damit zu tun? Hör auf, die Rassenbedingungen als etwas zu betrachten, das in deinem Code lebt: Es ist eine Eigenschaft des Universums. Alles, was eine Ressource berührt, die nicht vollständig kontrolliert wird (z. B. direkter Speicherzugriff, gemeinsamer Speicher, Datenbank, REST-API, Dateisystem usw. usw.) und erwartet, dass es unverändert bleibt, hat potenzielle Race-Bedingungen. Heck, beschäftigen wir uns mit dieser in C , die noch nicht haben Ausnahmen. Verzweigen Sie niemals nach dem Status einer Ressource, die Sie nicht kontrollieren, wenn mindestens einer der Zweige mit dem Status dieser Ressource in Konflikt gerät.
Jared Smith
1
@DanielPryden In meiner Frage habe ich nicht gesagt, dass ich keine Datenbankausnahmen behandeln möchte (ich weiß, dass Ausnahmen unvermeidlich sind). Ich denke viele Leute haben das missverstanden, ich wollte eine freundliche Fehlermeldung für meine Web-API (für Endbenutzer zum Lesen) haben Child with id {parent.ChildId} could not be found.. Und die Formatierung "Fremdschlüsselverletzung" finde ich in diesem Fall schlimmer.
Konrad

Antworten:

3

Eher eine verwirrte Frage, aber JA, Sie sollten zuerst prüfen und nicht nur eine DB-Ausnahme behandeln.

Zunächst befinden Sie sich in Ihrem Beispiel auf der Datenebene und verwenden EF direkt in der Datenbank, um SQL auszuführen. Ihr Code entspricht dem Ausführen

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

Die Alternative, die Sie vorschlagen, ist:

insert into parents (blah)
//if exception, perform logic

Die Verwendung der Exception zur Ausführung der bedingten Logik ist langsam und allgemein verpönt.

Sie haben eine Racebedingung und sollten eine Transaktion verwenden. Dies kann jedoch vollständig in Code erfolgen.

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

Der Schlüssel ist, sich zu fragen:

"Erwarten Sie, dass diese Situation eintritt?"

Wenn nicht, dann setzen Sie es sicher ein und lösen Sie eine Ausnahme aus. Behandeln Sie die Ausnahme jedoch wie jeden anderen Fehler, der auftreten kann.

Wenn Sie damit rechnen, ist dies NICHT außergewöhnlich und Sie sollten zuerst prüfen, ob das Kind vorhanden ist. Wenn dies nicht der Fall ist, antworten Sie mit der entsprechenden freundlichen Nachricht.

Bearbeiten - Es gibt eine Menge Kontroversen darüber, wie es scheint. Bevor Sie abstimmen, bedenken Sie:

A. Was wäre, wenn es zwei FK-Einschränkungen gäbe? Würden Sie es befürworten, die Ausnahmemeldung zu analysieren, um herauszufinden, welches Objekt fehlt?

B. Wenn Sie einen Fehler haben, wird nur eine SQL-Anweisung ausgeführt. Nur Treffer verursachen den Mehraufwand einer zweiten Abfrage.

C. Normalerweise ist Id ein Ersatzschlüssel. Es ist schwer vorstellbar, dass Sie einen kennen und nicht sicher sind, ob er sich in der Datenbank befindet. Überprüfen wäre seltsam. Aber was ist, wenn es sich um einen natürlichen Schlüssel handelt, den der Benutzer eingegeben hat? Das könnte eine hohe Wahrscheinlichkeit haben, nicht anwesend zu sein

Ewan
quelle
Kommentare sind nicht für längere Diskussionen gedacht. Diese Unterhaltung wurde in den Chat verschoben .
maple_shaft
1
Das ist völlig falsch und irreführend! Es sind Antworten wie diese, die schlechte Profis hervorbringen, gegen die ich immer kämpfen muss. SELECT sperrt niemals eine Tabelle, daher kann sich der Datensatz zwischen SELECT und INSERT, UPDATE oder DELTE ändern. Es ist also eine schlechte Softwareentwicklung und ein Unfall, der auf die Produktion wartet.
Daniel Lobo
1
@ DanielLobo Transactionscope behebt das
Ewan
1
Testen Sie es, wenn Sie mir nicht glauben
Ewan
1
@yusha Ich habe den Code hier
Ewan
111

Das Prüfen auf Eindeutigkeit und anschließende Einstellen ist ein Gegenmuster. Es kann immer vorkommen, dass die ID gleichzeitig zwischen Prüf- und Schreibzeit eingefügt wird. Datenbanken sind in der Lage, dieses Problem durch Mechanismen wie Einschränkungen und Transaktionen zu lösen. Die meisten Programmiersprachen sind es nicht. Wenn Sie Wert auf Datenkonsistenz legen, überlassen Sie dies daher dem Experten (der Datenbank).

Kilian Foth
quelle
34
Prüfen und Scheitern ist nicht schneller als nur "Ausprobieren" und auf das Beste hoffen. Frühere impliziert 2 Operationen, die von Ihrem System und 2 von der Datenbank implementiert und ausgeführt werden müssen, während die neueste nur eine davon impliziert. Die Prüfung wird an den DB-Server delegiert. Dies impliziert auch einen Sprung weniger in das Netzwerk und eine Aufgabe weniger, die von der DB erledigt werden muss. Wir denken vielleicht, dass eine weitere Abfrage für die DB erschwinglich ist, aber wir vergessen oft, groß zu denken. Denken Sie an eine hohe Parallelität, die die Abfrage mehr als hundert Mal auslöst. Es könnte den gesamten Verkehr zur DB dupen. Ob das wichtig ist, entscheiden Sie.
Laiv,
6
@Konrad Mein Standpunkt ist, dass die standardmäßig richtige Auswahl eine Abfrage ist, die von sich aus fehlschlägt, und es ist der separate Ansatz zur Abfragevorabprüfung, der die Beweislast trägt, um sich selbst zu rechtfertigen. Wie bei „wird ein Problem“: so Sie sind Transaktionen oder anderweitig sicherzustellen , dass Sie gegen ToCToU Fehler sicher sind , nicht wahr? Aus dem veröffentlichten Code ist für mich nicht ersichtlich, dass Sie es sind, aber wenn Sie es nicht sind, ist es bereits zu einem Problem geworden, wie eine tickende Bombe zu einem Problem wird, lange bevor sie tatsächlich explodiert.
mtraceur
4
@Konrad EF Core wird nicht implizit sowohl Ihren Scheck als auch die Beilage in eine Transaktion einfügen. Sie müssen dies explizit anfordern. Ohne die Transaktion ist es sinnlos, zuerst zu prüfen, da sich der Datenbankstatus zwischen Prüfen und Einfügen ohnehin ändern kann. Selbst bei einer Transaktion können Sie möglicherweise nicht verhindern, dass sich die Datenbank unter Ihren Füßen ändert. Wir sind vor einigen Jahren auf ein Problem gestoßen, bei dem EF mit Oracle verwendet wurde. Obwohl die Datenbank dies unterstützt, löste Entity das Sperren der gelesenen Datensätze innerhalb einer Transaktion nicht aus, und nur die Einfügung wurde als Transaktion behandelt.
Mr.Mindor
3
"Auf Eindeutigkeit prüfen und dann einstellen ist ein Gegenmuster" würde ich nicht sagen. Es hängt stark davon ab, ob Sie davon ausgehen können, dass keine anderen Änderungen vorgenommen werden, und ob die Prüfung ein nützlicheres Ergebnis liefert (auch nur eine Fehlermeldung, die dem Leser tatsächlich etwas bedeutet), wenn sie nicht vorhanden ist. Bei einer Datenbank, die gleichzeitige Webanforderungen verarbeitet, können Sie nicht garantieren, dass keine anderen Änderungen vorgenommen werden. Es gibt jedoch Fälle, in denen dies eine vernünftige Annahme ist.
jpmc26
5
Die erstmalige Überprüfung auf Eindeutigkeit beseitigt nicht die Notwendigkeit, mögliche Fehler zu beheben. Wenn für eine Aktion jedoch mehrere Vorgänge ausgeführt werden müssten, ist es häufig besser, vor dem Starten eines Vorgangs zu überprüfen, ob alle Vorgänge erfolgreich sind, als Aktionen auszuführen, für die möglicherweise ein Rollback erforderlich ist. Bei den ersten Überprüfungen werden möglicherweise nicht alle Situationen vermieden, in denen ein Rollback erforderlich wäre. Dies kann jedoch dazu beitragen, die Häufigkeit solcher Fälle zu verringern.
Supercat
38

Ich denke, was Sie "schnell scheitern" nennen und was ich es nenne, ist nicht dasselbe.

Erklären der Datenbank eine Änderung vornehmen und die Fehlerbehandlung, das ist schnell. Ihr Weg ist kompliziert, langsam und nicht besonders zuverlässig.

Ihre Technik geht nicht schnell kaputt, sie ist "Preflighting". Es gibt manchmal gute Gründe, aber nicht, wenn Sie eine Datenbank verwenden.

gnasher729
quelle
1
Es gibt Fälle, in denen Sie eine zweite Abfrage benötigen, wenn eine Klasse von einer anderen abhängt. In solchen Fällen haben Sie also keine Wahl.
Konrad
4
Aber nicht hier. Und Datenbankabfragen können sehr clever sein, daher bezweifle ich im Allgemeinen, dass es keine Wahl gibt.
gnasher729
1
Ich denke, es hängt auch von der Anwendung ab. Wenn Sie es nur für wenige Benutzer erstellen, sollte es keinen Unterschied machen und der Code ist mit zwei Abfragen besser lesbar.
Konrad
21
Sie gehen davon aus, dass Ihre DB inkonsistente Daten speichert. Mit anderen Worten, es sieht so aus, als ob Sie Ihrer Datenbank und der Konsistenz der Daten nicht vertrauen. Wenn das der Fall wäre, hätten Sie ein wirklich großes Problem und Ihre Lösung ist ein Walkaround. Eine palliative Lösung sollte früher als später außer Kraft gesetzt werden. Es kann Fälle geben, in denen Sie gezwungen sind, eine Datenbank außerhalb Ihrer Kontrolle und Verwaltung zu verwenden. Aus anderen Anwendungen. In diesen Fällen würde ich solche Validierungen in Betracht ziehen. Auf jeden Fall hat @gnasher recht, deines scheitert nicht schnell oder es ist nicht das, was wir als scheitern verstehen.
Laiv,
15

Dies begann als Kommentar, wurde aber zu groß.

Nein, wie in den anderen Antworten angegeben, sollte dieses Muster nicht verwendet werden. *

Bei Systemen, die asynchrone Komponenten verwenden, gibt es immer eine Race Condition, bei der sich die Datenbank (oder das Dateisystem oder ein anderes asynchrones System) zwischen der Prüfung und der Änderung ändern kann. Eine Überprüfung dieses Typs ist einfach keine zuverlässige Methode, um die Art von Fehler zu verhindern, die Sie nicht behandeln möchten.
Schlimmer noch, es gibt auf den ersten Blick den Eindruck, dass verhindert werden sollte, dass der doppelte Aufzeichnungsfehler ein falsches Sicherheitsgefühl hervorruft.

Die Fehlerbehandlung benötigen Sie trotzdem.

In Kommentaren haben Sie gefragt, was passiert, wenn Sie Daten aus mehreren Quellen benötigen.
Immer noch nein.

Das grundlegende Problem verschwindet nicht, wenn das, was Sie überprüfen möchten, komplexer wird.

Die Fehlerbehandlung brauchen Sie trotzdem.

Auch wenn diese Überprüfung eine zuverlässige Möglichkeit darstellt, um den bestimmten Fehler zu verhindern, vor dem Sie sich schützen möchten, können dennoch andere Fehler auftreten. Was passiert, wenn Sie die Verbindung zur Datenbank verlieren oder der Speicherplatz knapp wird oder?

Wahrscheinlich benötigen Sie ohnehin noch eine andere datenbankbezogene Fehlerbehandlung. Die Behandlung dieses speziellen Fehlers sollte wahrscheinlich ein kleiner Teil davon sein.

Wenn Sie Daten benötigen, um zu bestimmen, was geändert werden soll, müssen Sie diese offensichtlich irgendwo abrufen. (Je nachdem, welche Tools Sie verwenden, gibt es wahrscheinlich bessere Möglichkeiten als separate Abfragen, um sie zu erfassen.) Wenn Sie bei der Prüfung der von Ihnen erfassten Daten feststellen, dass Sie die Änderung nicht vornehmen müssen, großartig, nehmen Sie die Änderungen nicht vor Veränderung. Diese Feststellung ist völlig unabhängig von den Bedenken hinsichtlich der Fehlerbehandlung.

Die Fehlerbehandlung brauchen Sie trotzdem.

Ich weiß, dass ich mich wiederhole, aber ich halte es für wichtig, dies klar zu machen. Ich habe dieses Durcheinander schon einmal aufgeräumt.

Es wird irgendwann scheitern. Wenn dies nicht gelingt, wird es schwierig und zeitaufwändig sein, den Grund zu finden. Das Lösen von Problemen, die sich aus den Rennbedingungen ergeben, ist schwierig. Sie treten nicht durchgehend auf, sodass es schwierig oder sogar unmöglich ist, sie isoliert zu reproduzieren. Sie haben zunächst nicht die richtige Fehlerbehandlung vorgenommen, sodass Sie wahrscheinlich nicht viel zu tun haben: Möglicherweise ein Bericht eines Endbenutzers über einen kryptischen Text (den Sie ursprünglich verhindern wollten). Möglicherweise weist eine Stapelverfolgung auf die Funktion hin, dass der Fehler sogar möglich sein sollte, wenn Sie sich die Funktion anschauen, die offenkundig leugnet.

* Es kann berechtigte geschäftliche Gründe geben, diese vorhandenen Prüfungen durchzuführen, um zu verhindern, dass die Anwendung teure Arbeiten dupliziert, aber dies ist kein geeigneter Ersatz für eine ordnungsgemäße Fehlerbehandlung.

Mr.Mindor
quelle
2

Ich denke, eine sekundäre Sache, die Sie hier beachten sollten - einer der Gründe, warum Sie dies möchten, ist, dass Sie eine Fehlermeldung formatieren können, die der Benutzer sehen kann.

Ich kann Ihnen nur empfehlen:

a) Zeigen Sie dem Endbenutzer für jeden die gleiche generische Fehlermeldung .

b) protokollieren Sie die tatsächliche Ausnahme an einem Ort, auf den nur die Entwickler zugreifen können (wenn sie sich auf einem Server befindet), oder an einem Ort, den Ihnen Fehlerberichterstellungstools senden können (wenn der Client bereitgestellt ist).

c) Versuchen Sie nicht, die von Ihnen protokollierten Fehlerausnahmedetails zu formatieren, es sei denn, Sie können weitere nützliche Informationen hinzufügen. Sie möchten nicht versehentlich die einen nützlichen Informationen "formatiert" haben, mit denen Sie ein Problem hätten aufspüren können.


Kurz gesagt - Ausnahmen sind voll von sehr nützlichen technischen Informationen. Nichts davon sollte für den Endbenutzer bestimmt sein, und Sie verlieren diese Informationen auf eigene Gefahr.

Paddy
quelle
2
"Zeigen Sie dem Endbenutzer für jeden aufgetretenen Fehler dieselbe generische Fehlermeldung." Dies war der Hauptgrund dafür, dass das Formatieren der Ausnahme für Endbenutzer schrecklich erscheint.
Konrad,
1
In jedem vernünftigen Datenbanksystem sollten Sie programmgesteuert herausfinden können, warum etwas fehlgeschlagen ist. Es sollte nicht erforderlich sein, eine Ausnahmemeldung zu analysieren. Und allgemeiner: Wer sagt, dass dem Benutzer überhaupt eine Fehlermeldung angezeigt werden muss? Sie können das erste Einfügen und Wiederholen in einer Schleife fehlschlagen, bis Sie erfolgreich sind (oder bis zu einer gewissen Grenze von Wiederholungsversuchen oder Zeit). Tatsächlich ist Backoff-and-Retry etwas, das Sie sowieso irgendwann implementieren möchten.
Daniel Pryden