Zum Inhalt springen

Beziehungen und Rekursion

Kurzantwort: Jede Beziehung hat einen Typ (ONETOONE, ONETOMANY, MANYTOONE, MANYTOMANY), ein Rückreferenz-Feld und eine Liste erlaubter rekursiver Operationen (CREATE, READ, UPDATE, DELETE). CDMS pflegt beim Schreiben immer beide Seiten und entscheidet anhand der Flags, ob ein verschachteltes Objekt angelegt, geändert, nur referenziert, entkoppelt oder gelöscht wird.


@Reference(relation = RelationshipType.ONETOMANY,
backReferenceFieldName = "customer",
recursive = { RecursiveType.CREATE, RecursiveType.UPDATE, RecursiveType.DELETE })
private Set<OrderEntity> orders;
AngabeBedeutung
relationKardinalität und Besitzverhältnis
backReferenceFieldNameFeld auf der Gegenseite, das zurückzeigt
recursiveerlaubte rekursive Operationen über diese Beziehung
FlagWirkung
CREATEEin verschachteltes Objekt ohne id wird angelegt
UPDATEEin verschachteltes Objekt mit id wird mitgeschrieben
READDie Beziehung darf gelesen werden
DELETEDas Gegenüber ist ein abhängiges Kind: Entkoppeln bedeutet Löschen

Ohne CREATE wird ein Eintrag ohne id im Strict Mode mit 400 (recursive-create-not-allowed|<feld>) abgelehnt, sonst still verworfen. Ohne UPDATE wird ein Eintrag mit id nur referenziert, sein Inhalt bleibt unangetastet.

flowchart TB
    A["verschachteltes Objekt im Payload"] --> B{"id vorhanden?"}
    B -->|nein| C{"RecursiveType.CREATE?"}
    C -->|ja| D["recursiveCreate<br/>+ Rückreferenz setzen"]
    C -->|"nein, Strict Mode"| E["400 recursive-create-not-allowed"]
    C -->|"nein, tolerant"| F["Eintrag verwerfen"]
    B -->|ja| G{"RecursiveType.UPDATE?"}
    G -->|ja| H["recursiveUpdate / recursivePatch<br/>der bestehenden Zeile"]
    G -->|nein| I["nur referenzieren"]

setReference(...) im System-Layer setzt je nach Beziehungstyp beide Enden:

TypAktion
ONETOONEFeld setzen und Rückreferenz auf der Gegenseite setzen
ONETOMANYElement in die Sammlung aufnehmen und Rückreferenz setzen
MANYTOONEFeld setzen und in die Sammlung der Gegenseite aufnehmen
MANYTOMANYbeide Sammlungen pflegen

Beim Entkoppeln räumt removeBackReference(...) symmetrisch wieder auf. Fehlende Sammlungen werden dabei als LinkedHashSet angelegt – dadurch bleibt die Reihenfolge stabil und die Mengen-Semantik erhalten.

Der Grund für den Aufwand: Hibernate persistiert nur, was über die besitzende Seite und Dirty Checking erkannt wird. Ein im Debugger korrekt aussehendes Objekt genügt nicht.

Schreibt CDMS rekursiv in ein Kind, wird das Feld, das auf den auslösenden Elternteil zeigt, übersprungen (excludeField). Die Zugehörigkeit verwaltet allein der Parent – andernfalls würde das Kind die gerade hergestellte Verknüpfung wieder auflösen.

Eine gesendete Liste beschreibt den Zielzustand der Beziehung. Alles, was nicht in ihr steht, wird über reduceToTargetState(...) entfernt:

  • ohne DELETE-Flag: Mitglied bleibt bestehen, Rückreferenz wird geleert, das Mitglied wird gespeichert
  • mit DELETE-Flag: Mitglied wird rekursiv gelöscht

Bei PUT bedeutet eine fehlende Liste „leer”. Bei PATCH bedeutet eine fehlende Liste „unangetastet”, ein null oder [] dagegen „leeren”. Siehe Schreibsemantik.

Für Beziehungen mit eigenen Attributen wird eine explizite Join-Entity bevorzugt (z. B. User, Group, User2Group). Vorteile: zusätzliche Felder, eigene Auditierung, klare Persistenzkontrolle.

Zu beachten:

  • die Join-Entity muss auf beiden Seiten korrekt referenziert sein
  • die besitzende Seite bestimmt den Fremdschlüssel
  • Cascade-Regeln bewusst setzen
  • bei NOT-NULL-Fremdschlüsseln muss die Referenz der besitzenden Seite vor dem Flush stehen
public void addGroup(GroupEntity group) {
User2GroupEntity link = new User2GroupEntity();
link.setUser(this);
link.setGroup(group);
this.groups.add(link);
}

Beziehungen werden nur geladen, wenn der Response Request sie anfordert:

  • Einzelreferenz: recursiveRead über den System-Layer des Zieltyps
  • Listenreferenz: eigene Abfrage mit automatisch ergänztem Filter <backReference>.id = <id des Elternobjekts>, plus dem im parameter angegebenen Filter, Sortierung und Pagination
  • Beziehungen auf abstrakte Typen laufen über die Untertyp-Auflösung, siehe Vererbung

In der Datenbank werden Beziehungen mit LEFT JOIN angebunden, damit Objekte ohne Beziehung nicht aus dem Ergebnis fallen. Ein impliziter Zugriff über einen Pfad wie root.get("model").get("id") erzeugt dagegen einen Inner Join und würde genau das tun – deshalb wird explizit gejoint.

Jedes DTO trägt ein Feld _reference. Beim Betreten der Rekursion setzt CDMS dort eine UUID; ein bereits besuchtes Objekt wird nicht erneut verarbeitet. Ohne diesen Schutz würde ein bidirektionaler Graph endlos laufen.