Zum Inhalt springen

Hooks – eigene Businesslogik einhängen

Kurzantwort: Hooks sind der vorgesehene Erweiterungspunkt. Ein Hook ist eine Spring-Bean, die HookServiceInterface<T> für eine Entity implementiert und @Order trägt. CDMS ruft sie vor und nach dem Datenbankzugriff auf – und zwar vor der Validierung, damit ein Hook Pflichtfelder noch füllen kann.


EbeneInterfaceMethoden
KlasseHookServiceInterface<T extends ModelInterface>beforeDatabaseChange, afterDatabaseChange, fieldBeforeDatabaseChange, fieldOnDatabaseRead
FeldHookFieldInterfacedieselben vier, aber je Feld registriert über @Hook({...})

Der Methodenparameter CmsMethods sagt, welche Operation läuft: CREATE, READ, UPDATE, PATCH, DELETE, QUERY, ROLLBACK.

@Order(100)
@Service
public class ModelFieldHook implements HookServiceInterface<ModelFieldEntity> {
@Override
public void beforeDatabaseChange(ModelFieldEntity item, CmsMethods method) {
if (method == CmsMethods.CREATE || method == CmsMethods.UPDATE) {
// Gegenseite einer Beziehung synchron halten, Feldnamen eindeutig erzwingen
}
}
@Override
public void afterDatabaseChange(ModelFieldEntity item, CmsMethods method) { }
@Override
public void fieldBeforeDatabaseChange(ModelFieldEntity item, String field, CmsMethods method) { }
@Override
public void fieldOnDatabaseRead(ModelFieldEntity item, String field, CmsMethods method) { }
}

@Order ist Pflicht – die Hook-Verwaltung sortiert danach und greift beim Sortieren direkt auf die Annotation zu.

sequenceDiagram
    participant S as System-Layer
    participant R as Rekursion
    participant H as Hooks
    participant V as Validierung
    participant P as Persistenz

    S->>R: recursiveCreate / Update / Patch
    R->>R: Felder setzen, Beziehungen auflösen,<br/>Regelverstöße sammeln
    R->>H: Hooks registrieren (before/after je Objekt)
    S->>H: runAllBefore – alle Before-Hooks
    Note over H: darf Felder setzen, die der Client nicht kennt
    S->>V: assertValid – Verstöße erneut prüfen
    V-->>S: nur noch echte Verstöße → 422
    S->>P: persist / merge
    S->>H: runAllAfter – alle After-Hooks
    S->>P: flush

Die Reihenfolge Hooks vor Validierung ist bewusst: Ein Before-Hook darf ein Feld ableiten, das der Client weder kennt noch schickt, und damit eine Pflichtregel erfüllen. Anschließend wird jeder gesammelte Verstoß erneut gegen die Entity geprüft – ein Hook, der einen fehlenden Wert durch einen unzulässigen ersetzt, wird von der Regel gefasst, die er jetzt bricht.

Die Rekursion registriert Hooks (addHooksBeforeDatabase / addHooksAfterDatabase) pro besuchtem Objekt im request-scoped HookRequestContext. Ausgeführt werden sie gesammelt über runAllBefore(...) bzw. runAllAfter(...) im AbstractSystemLayer.

Vorteil: Jedes Objekt der Rekursion – auch jedes kaskadierte Kind – registriert seine eigenen Hooks über seinen eigenen System-Layer. Ein einzelner Auslöser deckt damit den ganzen Baum ab.

Die Zuordnung Entity → Hook-Beans wird beim ersten Zugriff aufgelöst und statisch gecacht (Klassen- und Feldebene getrennt).

recursiveDelete registriert die DELETE-Hooks für das Objekt selbst und für jedes kaskadierte Kind, jeweils über dessen eigenen System-Layer. Ausgeführt werden sie über dieselben Dispatcher wie beim Schreiben.

Die Persistenz ruft am Ende von readObject(...) genau einen Hook-Punkt: callHooksAfterDatabase(entity, CmsMethods.READ). Alle übrigen Punkte setzt der System-Layer.

WurfBedeutung
HookValidationExceptionfachliche Ablehnung → 422
HookExecutionExceptiontechnischer Fehler → 500

Beide sind CodamAI-Exceptions und lösen über den zentralen Exception-Mapper das Rollback des gesamten Requests aus.

  1. Generierter Code wird nicht angefasst. Fachlogik gehört in Hooks und eigene Klassen unter src/main/java.
  2. Owning side beachten. Wer in einem Hook Beziehungen ändert, muss beide Seiten pflegen – siehe Beziehungen.
  3. Keine Filter umgehen. Wer im Hook direkt auf die Persistenz zugreift, umgeht Owner- und Attributfilter.
  4. Hooks sind synchron und Teil der Transaktion. Langlaufendes gehört nicht hinein.
  5. Keine Businesslogik in die Persistenz. Das Persistenzmodul definiert selbst keine Hooks – es ruft nur.

Ein ModelFieldHook implementiert HookServiceInterface<ModelFieldEntity>: Er hält bei Beziehungsfeldern die Gegenseite synchron, erzwingt eindeutige Feldnamen und räumt beim Löschen die Spiegelbeziehung ab. Nach demselben Muster arbeiten Hooks für Enumerationen, Enum-Werte, Ordner und Primitivfelder.