Soft Deletes löschen nicht, und das ist das Problem
Eine Zeile, und jede Unique-Constraint, jeder Fremdschlüssel und jede Löschanfrage bedeuten etwas anderes. Die meisten Codebasen übernehmen den Trait, ohne davon etwas zu entscheiden.
SoftDeletes zu einem Modell hinzuzufügen dauert eine Zeile, und jedes Tutorial
stellt es als kostenloses Rückgängigmachen dar. Ist es nicht. Es verändert die
Bedeutung von Löschen in der ganzen Anwendung, und die Folgen landen an Stellen,
die mit dem Modell, dem Sie es hinzugefügt haben, nichts zu tun haben.
Was tatsächlich passiert
delete() hört auf zu löschen. Es setzt deleted_at und die Zeile bleibt. Ein
globaler Scope versteckt sie vor Abfragen. Das ist der gesamte Mechanismus, und
jedes Problem unten folgt daraus, dass die Zeile noch da ist.
Unique-Constraints hören auf zu funktionieren
Ein Nutzer registriert sich mit einer Adresse, löscht sein Konto und versucht sich mit derselben Adresse erneut zu registrieren. Die Zeile steht noch in der Tabelle, der Unique-Index liegt noch auf der Spalte, und die Registrierung scheitert mit einem Datenbankfehler über einen Datensatz, den der Nutzer nicht sehen und den der Support nicht finden kann.
Die Anwendung sagt, das Konto existiert nicht. Die Datenbank sagt, es existiert. Beide sagen die Wahrheit über verschiedene Dinge.
Zwei Auswege, und es sind Entscheidungen statt Behebungen:
// eine lebende Zeile, beliebig viele gelöschte
$table->unique(['email', 'deleted_at']);Oder den Wert beim Löschen freigeben, durch Umschreiben:
public function delete(): bool
{
$this->email = "{$this->email}#deleted-{$this->id}";
$this->save();
return parent::delete();
}Das Erste behält den ursprünglichen Wert und kann Eindeutigkeit über lebende und gelöschte Zeilen hinweg nicht durchsetzen. Das Zweite gibt die Adresse frei und verliert das Original – was zählt, wenn Sie je wissen müssen, wem dieses Konto gehörte. Keines ist falsch; keines zu wählen schon.
Beziehungen kommen falsch aussehend zurück
Ein soft-gelöschter Elterndatensatz erfüllt weiterhin seine Fremdschlüssel, denn die Zeile existiert. Eine Kindzeile ist in der Datenbank also nicht verwaist, und ein Join, der von Soft Deletes nichts weiß, liefert bereitwillig den gelöschten Elternteil zurück.
Eloquents Scope versteckt ihn, wenn Sie das Modell abfragen. Rohe Abfragen, von Hand geschriebene Reports und selbstgebaute Aggregate tun das nicht – und genau dort hören zwei Bildschirme auf, sich über dieselbe Zahl einig zu sein.
Schlimmer noch: withTrashed() auf einer Seite einer Beziehung und nicht auf
der anderen ergibt ein Ergebnis, das in keiner Richtung korrekt ist.
Löschung ist nicht Löschen
Eine Person übt ihr Recht auf Löschung aus. Die Anwendung ruft delete() auf.
Die Zeile ist noch da, noch lesbar für jeden mit Datenbankzugriff, noch in jedem
Backup.
Nichts wurde gelöscht. Die rechtliche Pflicht ist nicht erfüllt, und das System meldet, dass sie es sei – was schlimmer ist als lautes Scheitern.
Ein Löschpfad muss den Mechanismus bewusst umgehen:
$user->forceDelete();Oder, wo Datensätze aus steuerlichen oder Prüfungsgründen bleiben müssen, anonymisieren statt entfernen – die personenbezogenen Felder ersetzen, die Zeile behalten und festhalten, dass es passiert ist. Das ist eine Entscheidung mit einem Juristen darin, und der Code hat umzusetzen, welche Antwort auch kommt, statt den Standard zu nehmen.
Die Tabelle wächst nur
Nichts räumt soft-gelöschte Zeilen weg, solange niemand die Aufgabe schreibt.
Jahre später besteht die Tabelle zu einem erheblichen Teil aus Zeilen, die
niemand sehen kann, Indizes sind größer als nötig, und jede Abfrage trägt ein
deleted_at is null mit, das der Planer berücksichtigen muss.
Wenn der Trait auf einem Modell sitzt, sollte es eine Aufbewahrungsregel geben und eine Aufgabe, die sie durchsetzt. „Wir behalten alles für immer" ist eine gültige Regel; keine Regel zu haben ist der Weg, auf dem eine Tabelle vierzig Millionen Zeilen erreicht, von denen sechs Millionen zählen.
Wann er wirklich richtig ist
Wo Rückgängigmachen eine Funktion ist, die Nutzer erwarten – ein Archiv, ein Papierkorb, eine versehentliche Löschung, die der Support umkehren können soll.
Wo der Datensatz von Historie referenziert wird, die lesbar bleiben muss: Eine Rechnung, die einen inzwischen entfernten Kunden nennt, sollte ihn weiterhin nennen.
Wo Löschen ein Arbeitsablauf ist statt eines Ereignisses, mit einem Freigabeschritt zwischen Markieren und Entfernen.
Das ist eine engere Menge als „jedes Modell", und dort landet der Trait üblicherweise. Der Standard sollte ein echtes Löschen sein, und der Trait dorthin kommen, wo jemand sagen kann, was Rückgängigmachen für diese Tabelle bedeutet – und wo jemand die Frage zur Unique-Constraint, die Frage zum Reporting und die Frage zur Aufbewahrung beantwortet hat, bevor die erste Zeile gelöscht wird, und nicht nach dem ersten Support-Ticket.
Unique-Constraints und Auswertungen sind Schemafragen. Beides nachträglich in eine Tabelle einzuziehen, die seit zwei Jahren weich löscht, ist Datenarbeit und dauert länger, als man denkt. An der Löschfrage hängt eine gesetzliche Frist, sie gehört also zu den anderen Entscheidungen, die sich nicht zurücknehmen lassen.
