Doctrine: Entitäten richtig löschen
Für jedes meiner bisherigen Symfony Projekte ist Doctrine die vorherrschende ORM. Sämtliche Datenbank-Kommunikation wird über dieses Tool abgewickelt. Wie vermutlich auch in jeder anderen ORM, ist auch in Doctrine das richtige Löschen von Entitäten ein interessantes Thema. Und neulich hat sich folgendes zugetragen:
In einem Projekt werden nach bestimmten Aktionen im System Meldungen für die Administratoren erzeugt. Diese Meldungen werden in der Datenbank abgelegt und auf dem Dashboard chronologisch angezeigt. Da es sich „nur“ um Benachrichtigungen handelt, haben diese keine direkte Priorität, also wird sichergestellt, dass die Aktion selbst funktioniert, bevor die Meldungen über einen EventListener generiert werden. Das sieht etwa so aus:
$copy = clone $myObject; $em->remove($myObject); $em->flush(); $eventDispatcher->dispatch(MyEvent::DELETE, new MyEvent($copy)); return new Response('Objekt wurde entfernt!');
Im EventListener findet nichts weiter statt, als die Erzeugung einer anderen Entität, die dann mit $em->persist(..)
und $em->flush()
in die Datenbank geschrieben wird.
Nach dem Löschvorgang offenbarte ein Blick in die Datenbank, dass das Objekt immer noch da ist. Zusätzlich wurde eine neue Meldung mit dem Hinweis auf eine erfolgreich gelöschte Entität erzeugt… Was war nun passiert? Der Doctrine Profiler schrieb dazu folgendes:
Das Objekt, welches zuerst erfolgreich gelöscht wurde, wird am Ende einfach wieder angelegt. Dafür existiert aber – nach wie vor – keine einzige Anweisung im Quellcode, das Ding soll schließlich verschwinden! Das macht Doctrine einfach um uns zu ärgern.
Detach for further flushes
Nach vielem Suchen, Googlen und Ausprobieren fand man die Lösung im richtigen leeren des Objekt-Caches von Doctrine. Der ORM speichert alle Objekte in Pools für damit einhergehende Aktionen (Create, Update, Delete, Relate). Diese Pools bleiben bis zum Ende der Ausführung einer Seite, können also mehrfach bei Flushen genutzt werden.
Mit $em->clear(MyObject::class)
können alle Pools für eine bestimmte Entität geleert werden, sodass diese Objekte beim nächsten $em->flush()
nicht mehr existieren. Dies hat allerdings auch einen Haken, wenn man z.B. die Nutzer leert: Hat man Änderungen am aktuell eingeloggten Nutzer zu speichern, werden diese ebenfalls ignoriert.
Praktischer ist daher das explizite Löschen von genau einer Entität aus den Pools, nämlich der, die gerade zum löschen vorgesehen wurde. Auch das ist zum Glück möglich: $em->detach($myObject)
lässt genau dieses eine Element aus dem Cache verschwinden. Das sollte dann so aussehen:
$copy = clone $myObject; $em->remove($myObject); $em->detach($myObject); $em->flush(); $eventDispatcher->dispatch(MyEvent::DELETE, new MyEvent($copy)); return new Response('Objekt wurde entfernt!');
Hier gibt es jedoch einen Wermutstropfen – die Kaskadieren auf die Relationen. Die wenigsten Entitäten haben keine Relationen auf andere Entitäten, die von demselben Lösch-Prozess betroffen sind oder gar ignoriert werden müssen. Für das gemeinsame Update oder Löschen bietet Doctrine das @ORM\ManyToOne(..., cascade={"persist", "remove"})
an.
Für das Ignorieren von Entitäten beim Flushen kommt nun noch das @ORM\ManyToOne(..., cascade={"detach"})
hinzu. Es sollte auf jeden Fall dort genutzt werden, wo auch ein „remove“ zum Einsatz kommt.
Fazit
Eigentlich nutzt man ein ORM, um sich das Leben leichter zu machen. Mit dieser Änderung kommt mir das Handling mittlerweile etwas schwerfällig vor…
Ich kann mir leider auch überhaupt nicht erklären, wieso dieses Problem plötzlich auftritt und bin froh, dass ich es gefunden habe. Es hatte auch schon ein Jahr lang ohne diesen „Workaround“ funktioniert. Auch bei „persist“ hat es nie Abweichungen oder Dopplungen gegeben, das Problem trat erst jetzt und nur für Löschvorgänge auf.
Wenn mir jemand erklären kann, warum das so ist, wie es nun ist, schreibt es bitte in die Kommentare. Danke!