Zum Inhalt springen

Wie wird auditiert?

Kurzantwort: Über Hibernate Envers. Ein Modell mit @Audited bekommt eine Audit-Tabelle; jede Änderung erzeugt eine Revision. Die Revisionsdaten stehen in der Tabelle revinfo und tragen neben Nummer und Zeitstempel auch Benutzer-ID, Benutzername, IP-Adresse und User-Agent aus dem RequestContext. Jedes Persistenzziel führt seine eigene revinfo. Ein Rollback schreibt den alten Stand als neue Revision – die Historie wird nie umgeschrieben.


Ein Modell ist genau dann auditiert, wenn es selbst @Audited trägt. Die Annotation auf einer Basisklasse macht kein Modell auditiert. Ein Modell ohne @Audited bekommt keine Audit-Tabelle, erzeugt bei Änderungen keine Revision und wird von Envers beim Lesen seiner Historie abgelehnt (model-not-audited, HTTP 500).

Zusätzlich tragen die Modell-Basisklassen selbst @Audited – sonst würde Envers die Eigenschaften einer @MappedSuperclass gar nicht auditieren und _createdOn, _updatedOn und _userId fehlten in jeder Revision.

Im Modell wird das über @History(rollback = true|false) gesteuert; das audited-Flag landet in MetaClassInfo.isAudited().

Dasselbe Flag steuert auch die Dateiversionierung – siehe Dateien und Storage.

AuditRevisionEntity, Tabelle revinfo:

SpalteInhalt
idRevisionsnummer (@RevisionNumber, IDENTITY)
timestampZeitstempel (@RevisionTimestamp, Millisekunden)
user_idRequestContext.userId
usernameRequestContext.userName
ip_addressRequestContext.userIP (45 Zeichen, IPv6-tauglich)
user_agentRequestContext.userClientAgent (512 Zeichen)

AuditRevisionListener füllt die vier Kontextfelder. Fehlt der Request-Kontext, bleiben sie leer, ohne dass die Revision scheitert – ein Hintergrundlauf ohne Benutzer schreibt trotzdem Historie.

flowchart LR
    RC["RequestContext<br/>userId · userName · IP · User-Agent"] --> L[AuditRevisionListener]
    W["Schreiboperation"] --> E[Hibernate Envers]
    E -->|neue Revision| L
    L --> RI[("revinfo")]
    E --> AT[("&lt;modell&gt;_AUD")]

EntityClassFilterService registriert AuditRevisionEntity in jeder Persistenzeinheit – System- wie Mandantendatenbank. Jedes Ziel führt seine eigene revinfo.

Ohne diese Registrierung würde Envers still seine eigene DefaultRevisionEntity verwenden – der Audit-Trail verlöre seinen Urheber, ohne dass es auffiele.

POST /api/rest/<pfad>/{id}/history
{
"response": ["id", "name", "status"],
"parameter": { "page": 0, "limit": 20 }
}

Antwort:

{
"data": [
{
"revision": { "id": "…", "name": "Muster GmbH", "status": "ACTIVE" },
"revisionMeta": {
"ref": 17,
"ts": 1785574268973,
"ip": "10.0.0.5",
"useragent": "Mozilla/5.0 …",
"username": "d.mertins"
},
"revisionType": "MOD"
}
],
"meta": { "totalCount": 3 }
}
EigenschaftVerhalten
Reihenfolgeabsteigend nach Revisionsnummer (neueste zuerst)
Paginationüber parameter.page / parameter.limit
Feldernur die im Response Request angeforderten – Referenzen und Listen werden rekursiv mit eigenem Unter-Request kopiert
RechthistoryRolenur lesend, deshalb keine Schreibrolle nötig
ÄnderungsartrevisionType: ADD, MOD, DEL

Eine Löschung schneidet die Spur nicht ab: Die vollständige Revisionsliste bleibt lesbar, die Löschrevision nennt Urheber und Zeitpunkt, und der letzte Stand vor der Löschung steht in der vorhergehenden Revision.

POST /api/rest/<pfad>/{id}/rollback/{revision}

Ablauf im System-Layer:

sequenceDiagram
    participant C as Client
    participant S as System-Layer
    participant P as Persistenz
    participant F as Storage
    C->>S: rollback(id, revision, ReadPayload)
    S->>S: assertVisibleForWrite(id)
    S->>S: rollbackAccessAllowedByClass – Rollback-Rolle
    S->>S: aktuelle fileVersion merken
    S->>S: ROLLBACK-Hooks (before)
    S->>P: historyRollback(id, revision)
    P->>P: Zustand der Revision zurückschreiben<br/>als NEUE Revision
    S->>F: rollbackFileContent – Inhalt zurückkopieren
    S->>S: ROLLBACK-Hooks (after), flush
    S->>S: readObject(id, request)
    S-->>C: wiederhergestellter Stand

Regeln:

  • Wiederhergestellt werden die eigenen Zustandsdaten: skalare Attribute und eigene To-One-Referenzen.
  • Sammlungen werden nicht wiederhergestellt.
  • Ein gelöschtes Objekt wird nicht wiederbelebt.
  • Eine Revision, die nicht zu diesem Objekt gehört, wird abgelehnt.
  • Der wiederhergestellte Zustand wird als neue Revision geschrieben – die Historie bleibt vollständig und lückenlos.
  • Zusätzlich zur historyRole ist die rollbackRole erforderlich, weil der Rollback schreibt.

Vor dem Rollback merkt sich der System-Layer die aktuelle fileVersion. Danach trägt der Datensatz die Version der Zielrevision; der Storage bekommt die alte Kennung übergeben, bewahrt den aktuellen Inhalt darunter auf und kopiert die Zielversion zurück. Trägt die Revision keine fileVersion (weil sie älter als das Feld ist), bleibt der Inhalt unangetastet – ihn zu ersetzen wäre geraten.

Envers-FehlerCDMS-Fehler
NotAuditedExceptionSystemConfigurationException (model-not-audited)
RevisionDoesNotExistExceptionEntityNotFoundException

Envers-Typen verlassen das Persistenzmodul nicht.

ip_address und user_agent sind personenbezogene Daten. Ihre Speicherung ist projektseitig gegen die jeweiligen Lösch- und Aufbewahrungsanforderungen zu prüfen.

Nicht verwechseln:

Mechanismuswas er liefert
_createdOn / _updatedOnZeitpunkte am aktuellen Datensatz
_userIdBesitzer eines benutzereigenen Modells
Envers-Revisionenvollständige Änderungshistorie mit Urheber, Zeit, IP und Client