Schreibsemantik – PUT, PATCH, DELETE
Kurzantwort: PUT ist eine Aussage über die ganze Ressource, PATCH über die
genannten Felder. Was in einem PUT fehlt, wird geleert bzw. entkoppelt. Was in
einem PATCH fehlt, bleibt unangetastet – ein ausdrückliches null leert dagegen.
Einen save-Endpunkt gibt es nicht.
Die Tabellen
Abschnitt betitelt „Die Tabellen“PUT /{model}/update/{id} – ersetzen
Abschnitt betitelt „PUT /{model}/update/{id} – ersetzen“| Feldart | fehlt | ausdrücklich null | Wert |
|---|---|---|---|
| Skalar | auf null gesetzt | auf null gesetzt | gesetzt |
| Referenz | entkoppelt; bei RecursiveType.DELETE gelöscht | wie „fehlt” | gesetzt bzw. rekursiv geschrieben |
| Liste | alle Mitglieder entfernt | wie „fehlt” | Zielzustand: nicht enthaltene Mitglieder entfernt |
PUT unterscheidet „fehlt” und null nicht. Der Payload ist ein typisiertes
Objekt; nach der Deserialisierung sind beide Fälle dasselbe null.
PATCH /{model}/update/{id} – nur das Genannte
Abschnitt betitelt „PATCH /{model}/update/{id} – nur das Genannte“| Feldart | fehlt | ausdrücklich null | Wert |
|---|---|---|---|
| Skalar | unverändert | geleert | gesetzt |
| Referenz | unverändert | entkoppelt; bei RecursiveType.DELETE gelöscht | siehe „Verschachtelte Objekte” |
| Liste | unverändert | alle Mitglieder entfernt | Zielzustand; [] leert vollständig |
PATCH transportiert eine HashMap und kennt den Tri-State fehlend / null /
Wert deshalb wirklich.
⚠️ Die häufigste Falle
Abschnitt betitelt „⚠️ Die häufigste Falle“// FALSCH – leert alles, was nicht gesetzt wurdeEmployeeUpdatePayload p = new EmployeeUpdatePayload();p.setId(id);p.setFirstname("Test"); // lastname, department … bleiben nullapi.patch(objectMapper.writeValueAsString(new WritePayload<>(p)));Jackson serialisiert das Objekt vollständig, also mit "lastname": null für
jedes nicht gesetzte Feld – und das leert die Felder.
// RICHTIG – nur nennen, was sich ändern soll{ "data": { "id": "…", "firstname": "Test" }, "response": ["+"] }Wer ein Formularmodell oder ein generiertes Payload-Objekt an PATCH schickt, löscht alles, was er nicht ausdrücklich gesetzt hat.
Verschachtelte Objekte
Abschnitt betitelt „Verschachtelte Objekte“Was mit einem Eintrag in einer Referenz oder Liste passiert, hängt an der id
und an den Recursive-Flags der Beziehung:
| Eintrag | Flag der Beziehung | Verhalten |
|---|---|---|
mit id | UPDATE gesetzt | wird rekursiv mitgeschrieben – bei PUT als Replace, bei PATCH als Patch |
mit id | UPDATE nicht gesetzt | nur referenziert, Inhalt unangetastet |
ohne id | CREATE gesetzt | wird angelegt |
ohne id | CREATE nicht gesetzt | Strict Mode: 400 recursive-create-not-allowed|<feld>; sonst wird der Eintrag still verworfen |
Diese Regeln gelten auf jeder Ebene. Der Operationskontext wechselt dabei pro Ebene: Ein PATCH auf ein Elternobjekt kann den Create eines Kindes enthalten – für das Kind gelten dann Create-Regeln, für den Elternteil Update-Regeln, im selben Request.
Back-Reference: Beim rekursiven Schreiben bleibt das Feld, das auf das auslösende Elternobjekt zurückzeigt, unangetastet. Die Zugehörigkeit verwaltet ausschließlich der Parent – sonst würde ein Teilobjekt die gerade hergestellte Verknüpfung wieder auflösen.
Entkoppeln oder Löschen
Abschnitt betitelt „Entkoppeln oder Löschen“flowchart TB
A["Referenz soll geleert werden"] --> B{"RecursiveType.DELETE<br/>auf der Beziehung?"}
B -->|nein| C["Objekt überlebt<br/>Rück-Referenz wird bereinigt<br/>updateObject"]
B -->|ja| D["abhängiges Kind<br/>recursiveDelete + deleteObject<br/>späteres Lesen: 404"]
Das gilt für PUT und PATCH gleichermaßen – beide nutzen denselben Codepfad.
Löschungen durchlaufen dabei die DELETE-Hooks.
DELETE /{model}/delete/{id} löscht das Objekt und – über
RecursiveType.DELETE – seine abhängigen Kinder, rekursiv. Für jedes betroffene
Objekt feuern die DELETE-Hooks über dessen eigenen System-Layer. Ist das Objekt
ein Datei-Modell, wird auch der Inhalt im Storage gelöscht, inklusive aller
aufbewahrten Versionen.
Nicht-abhängige Beziehungen werden nur entkoppelt: die Rück-Referenz wird geleert und das Gegenüber gespeichert.
Singletons
Abschnitt betitelt „Singletons“Dieselben Regeln, nur ohne ID:
/…/update statt /…/update/{id}, der Endpunkt löst die ID selbst auf und
setzt sie vor dem Patch in die Map ein.
Es gibt keinen save-Endpunkt
Abschnitt betitelt „Es gibt keinen save-Endpunkt“Die ältere Konzeptdokumentation beschreibt Save-Endpunkte (Create-oder-Update,
verzweigt über data.id). Der Generator erzeugt sie nicht. Vorhanden sind
create, read, update (PUT), update (PATCH), delete, query sowie
optional file, history und rollback.
Wer Create-oder-Update braucht, entscheidet clientseitig anhand der id.
Das ist auch der Grund, warum es kein WritePayload<SavePayload> gibt: Ein
WritePayload<CreatePayload> lässt sich nicht auf WritePayload<UpdatePayload>
casten, weil es unterschiedliche Klassen sind.
Validierung beim Schreiben
Abschnitt betitelt „Validierung beim Schreiben“Jedes geschriebene Feld wird gegen die Regeln der Operation geprüft, nicht
des Endpunkts: CREATE gegen die Create-Regeln, UPDATE und PATCH gegen die
Update-Regeln. Ein PATCH validiert nur, was er auch schreibt.
Alle Verstöße eines Requests werden gesammelt und in einer 422-Antwort mit Feldpfad gemeldet. Siehe Validierung.
Kein Optimistic Locking
Abschnitt betitelt „Kein Optimistic Locking“Im generierten Code gibt es kein @Version-Feld. Wer für ein PUT den
Ist-Zustand liest, ändert und zurückschreibt, kann fremde Änderungen
dazwischen überschreiben. Wo das kritisch ist, hilft PATCH (schreibt nur die
genannten Felder) oder eine fachliche Prüfung im Hook.