CodamAIDocs
Themafertig

Feld-Hooks

Wiederverwendbare Logik für einen einzelnen Wert: verschlüsselt speichern, ausrechnen, vereinheitlichen. Wie ein Feld-Hook aussieht, wie du ihn einem Feld zuordnest und wann er läuft.

Ausprägungen
vor dem Schreiben (beforeDatabaseChange)nach dem Lesen (afterDatabaseRead)umwandelnder Hookableitender Hook (runsOnEveryWrite)mehrere Hooks an einem FeldPATCH, PUT mit Feldrolle, ROLLBACKnicht durchsuchbar (keepsValueSearchable)

Worum es geht

Ein Feld-Hook ist Logik für einen einzelnen Wert. Er gehört nicht zu einem Modell, sondern zu einer Art von Wert. Deshalb kannst du denselben Feld-Hook an viele Felder in vielen Modellen hängen. Typische Fälle:

  • einen Text verschlüsselt speichern und entschlüsselt ausliefern,
  • einen Wert ausrechnen, etwa summe = preis × menge,
  • einen Wert vereinheitlichen, etwa eine Telefonnummer in eine feste Schreibweise bringen.

Der Modell-Hook sieht das ganze Objekt und gehört zu genau einem Modell. Der Feld-Hook bekommt den Wert und gibt den Wert zurück, der stattdessen gilt.

Einen Feld-Hook schreiben

Ein Feld-Hook implementiert HookFieldInterface<T>. T ist der Java-Typ des Feldes, für einen Text also String:

@Component
public class EncryptHook implements HookFieldInterface<String> {

  @Override
  public String beforeDatabaseChange(String value, FieldHookContext context) {
    return value == null ? null : cipher.encrypt(value);
  }

  @Override
  public String afterDatabaseRead(String value, FieldHookContext context) {
    return value == null ? null : cipher.decrypt(value);
  }
}

Beide Methoden geben einen Wert zurück. Den setzt CDMS in das Feld. Willst du nichts ändern, gibst du value unverändert zurück.

Der zweite Parameter, der Kontext (FieldHookContext), sagt dir, wo du bist:

Methodeliefert
item()das Objekt mit allen anderen Feldern
modelMeta()die Beschreibung des Modells
field(), fieldName()die Beschreibung und den Namen des Feldes
method()die Operation: CREATE, UPDATE, PATCH oder READ

Ist die Klasse eine Spring-Bean, etwa mit @Component, nimmt CDMS diese Bean. Sonst legt CDMS die Klasse selbst über Spring an. In beiden Fällen bekommt sie ihre Abhängigkeiten per @Autowired oder @Value, zum Beispiel den Schlüssel.

Den Feld-Hook einem Feld zuordnen

Die Zuordnung steht im Modell, nicht im Code. Im Hub öffnest du das Feld und setzt im Reiter Regeln die Regel Feld-Hook. Als Wert trägst du den vollständigen Klassennamen ein:

com.example.shop.hooks.EncryptHook

Mehrere Hooks trennst du durch Komma. Sie laufen in dieser Reihenfolge:

com.example.shop.hooks.CompressHook, com.example.shop.hooks.EncryptHook
  1. 1
    Modellierer
    setzt am Feld die Regel Feld-Hook (@hook) mit dem Klassennamen
  2. 2
    Generator
    prüft den Namen und schreibt die Klasse in die Beschreibung des Feldes
    Ein Name ohne Paket, etwa EncryptHook, bricht die Generierung ab. Eine Klasse, die es im Projekt nicht gibt, bricht das Kompilieren ab.
  3. 3
    CDMS
    legt beim Start jeden Feld-Hook an und prüft, ob sein Typ zum Feld passt
    Passt er nicht, startet die Anwendung nicht. Die Meldung nennt Modell, Feld und Hook: field-hook-type-mismatch|Kunde.telefon -> …EncryptHook.
  4. 4
    Feld-Hook
    läuft ab jetzt bei jedem Schreiben und Lesen dieses Feldes

Einen Feld-Hook gibt es nur an einfachen Feldern, also Text-, Zahlen-, Datums- und Enum-Feldern. Eine Beziehung bekommt keinen Feld-Hook. Dafür ist der Modell-Hook da.

Der Weg eines Wertes

sequenceDiagram
    participant C as Client
    participant D as CDMS
    participant H as Feld-Hook
    participant DB as Datenbank
    C->>D: POST code = "geheim"
    D->>D: Before-Hooks des Modells
    D->>H: beforeDatabaseChange("geheim")
    H-->>D: "x7Kq…"
    D->>DB: speichert "x7Kq…"
    D->>DB: liest das Objekt für die Antwort
    D->>H: afterDatabaseRead("x7Kq…")
    H-->>D: "geheim"
    D->>D: Lese-Hook des Modells
    D-->>C: 200, code = "geheim"

Nach dem Schreiben läuft der Feld-Hook nicht. Die Antwort auf einen Schreibvorgang entsteht beim Lesen, und dort läuft afterDatabaseRead.

Wann der Hook vor dem Schreiben läuft

Vor dem Schreiben läuft ein Feld-Hook nur für Felder, die der Vorgang schreibt. So wird ein gespeicherter Wert nie ein zweites Mal verschlüsselt.

VorgangLäuft beforeDatabaseChange?
Anlegen (POST)ja, für jedes Feld mit Hook
Ersetzen (PUT)ja, für jedes Feld mit Hook
Ändern (PATCH)nur für die Felder, die der PATCH sendet
PUT oder PATCH, den eine Feldrolle abweist und der gespeicherte Wert bleibtnein, der gespeicherte Wert bleibt, wie er ist
ein Before-Hook des Modells ändert das Feldja, auch dieser Wert läuft durch den Feld-Hook
Löschennein
Zurücksetzen auf einen alten Standnein, der alte Stand wird so übernommen, wie er gespeichert war

Der Feld-Hook läuft nach den Before-Hooks des Modells und vor der Prüfung der Regeln. Ein Hook, der einen Wert ausrechnet, kann damit auch ein Pflichtfeld füllen. Hat ein Feld mehrere Hooks, laufen sie in der Reihenfolge aus dem Modell. Beim Lesen laufen sie in umgekehrter Reihenfolge: Was zuletzt angewendet wurde, wird zuerst zurückgenommen.

Ein Wert, der sich aus anderen ergibt

Ein Hook, der einen Wert ausrechnet, muss auch laufen, wenn nur eine seiner Quellen geändert wird. Ein PATCH mit nur preis sendet summe nicht mit. Deshalb meldet so ein Hook sich für jeden Schreibvorgang an:

@Component
public class TotalHook implements HookFieldInterface<Integer> {

  @Override
  public Integer beforeDatabaseChange(Integer value, FieldHookContext context) {
    OrderEntity order = (OrderEntity) context.item();
    if (order.getPrice() == null || order.getQuantity() == null)
      return null;
    return order.getPrice() * order.getQuantity();
  }

  @Override
  public Integer afterDatabaseRead(Integer value, FieldHookContext context) {
    return value;
  }

  @Override
  public boolean runsOnEveryWrite() {
    return true;
  }
}

Mit runsOnEveryWrite() läuft der Hook bei jedem Anlegen, Ersetzen und Ändern des Objekts. Er bekommt dann auch den gespeicherten Wert und ignoriert ihn. Beim Löschen und beim Zurücksetzen läuft er nicht.

Feldrollen und Feld-Hooks

Eine Feldrolle verlangt ihre Rolle nur für eine Änderung. Schickst du einen Datensatz so zurück, wie du ihn gelesen hast, brauchst du keine. CDMS vergleicht dafür den gesendeten Wert mit dem gespeicherten, und zwar so, wie ein Leser ihn sieht, also nach afterDatabaseRead. Ein verschlüsseltes Feld, das du unverändert zurückschickst, gilt deshalb nicht als geändert.

Entscheidungstabelle

Modell-Hook oder Feld-Hook?
Was du tun willstHook
einen einzelnen Wert umwandeln, etwa verschlüsselnFeld-Hook
einen Wert aus anderen Feldern desselben Objekts ausrechnenFeld-Hook mit runsOnEveryWrite()
dieselbe Logik an vielen Feldern in vielen ModellenFeld-Hook
eine Regel prüfen, die mehrere Felder zusammen betrifft, und ablehnenModell-Hook
Beziehungen pflegen oder andere Datensätze schreibenModell-Hook
etwas an einer Beziehung tunModell-Hook, Feld-Hooks gibt es nur an einfachen Feldern

Fallen

Wie es weitergeht

Quellen im Code und in der Wissensdatenbank
  • commons – global/commons/interfaces/HookFieldInterface, FieldHookContext; meta/MetaFieldInfo (hooks)
  • CDMS/cdms-system-layer – HookManagementSystem (callHooksBeforeDatabase, callHooksAfterDatabase, readValue, checkFits, createFieldHook, afterSingletonsInstantiated); session/HookRequestContext (addFieldWritten); AbstractLayer (markWritten, guardedValue)
  • CDMS/cdms-generator – CdmsYamlLoader (Regel hook, rules.hooks), DtoMetaProcessor.fieldHooks
  • CDMS/frontend – shared/utils/cdmsFieldRules (HOOK_RULE)
  • CDMS/cdms-integrationtest – AbstractFieldHookTest, Modell secret, fieldhook/MarkHook, ZipHook, TotalHook
  • CDMS/cdms-system-layer – docs/adr/adr-017-feld-hooks-und-hook-verwaltung-ohne-profil.md
  • documentation/60-erweiterung/01-hooks.md
Suchen