1. „Wer hat geändert“ im Audit ohne Security
Wenn createdAt und updatedAt die Frage „wann“ beantworten, dann beantworten createdBy und lastModifiedBy die Frage „von wem“ (oder, um für unseren Projektstand ehrlicher zu sein – „welcher Quelle“). In der echten Produktion wird dies oft zu einer der wichtigsten Methoden, seltsame Daten zu untersuchen: wer hat den Preis geändert, wer hat die Bestellung geschlossen, wer hat die Bestände auf Null gesetzt. Und ja, das ist sogar nützlich, bevor es echte Benutzer und Autorisierung gibt – „Autor“ ist für uns vorerst kein Mensch, sondern eine bedingte Änderungsquelle.
Dabei lebt der Basis-mini-shop bereits gut mit createdAt und updatedAt. Die Autor-Felder sind eine zusätzliche Schicht: nützlich, aber für die aktuelle Arbeitskonfiguration des Projekts nicht zwingend erforderlich.
Stellen Sie sich eine Situation vor: Sie sehen in der Datenbank, dass ein Produkt plötzlich einen Preis von 0 hat. Anhand von updatedAt kennen Sie den Zeitpunkt, aber nicht den Kontext. Das Feld lastModifiedBy = "system" macht Sie auch nicht zu Sherlock Holmes, hilft aber, „das ist eine automatische Änderung“ von „das hat irgendein Admin-Szenario geändert“ zu unterscheiden (wenn Sie verschiedene Werte vergeben). Und vor allem: Sie hören auf, diese Dinge manuell in jedem Service zu schreiben – sonst wird aus dem Auditing „Copy-Paste von now() und currentUser()“.
Wichtig: Verwechseln Sie nicht die Bedeutung: createdBy und lastModifiedBy sind technische Felder, ähnlich in der Natur wie createdAt/updatedAt. Sie sind keine Geschäftsdaten des Kunden und schon gar kein Ersatz für customerEmail in der Bestellung. customerEmail gibt an, über wen die Bestellung ist, während createdBy angibt, von wem sie im System erstellt wurde. Wie in früheren Vorlesungen halten wir die Grenze ein: Geschäftsfelder sind für den Sinn der Domäne zuständig, Audit-Felder für die Herkunft und Datenpflege.
Um es gar nicht erst im Kopf zu vermischen, ist es praktisch, einmal in einer Minitabelle darauf zu schauen:
| Frage | Beispielfeld | Was wird gespeichert | Was es nicht ist |
|---|---|---|---|
| Wann erstellt? | createdAt | Zeitpunkt des Einfügens des Datensatzes | „wann zum ersten Mal in den Speicher geladen“ |
| Wann geändert? | updatedAt | Zeitpunkt der letzten Änderung | „wann gelesen“ |
| Wer hat erstellt? | createdBy | Erstellungsquelle | customerEmail, „Eigentümer des Datensatzes“ |
| Wer hat geändert? | lastModifiedBy | Quelle der letzten Bearbeitung | „wer liest gerade“ |
2. Annotationen @CreatedBy und @LastModifiedBy
Die Idee der Author-Felder in Spring Data JPA wirkt sehr „springisch“: Sie markieren ein Feld mit einer Annotation, und die Infrastruktur füllt es zum richtigen Zeitpunkt im Lebenszyklus der Entität aus. Aber damit dies nicht zu Magie wird („Annotation gesetzt – und ich bete“), müssen wir die Semantik genau verstehen. @CreatedBy wird bei der Erstellung der Entität ausgefüllt (normalerweise vor dem INSERT) und sollte logischerweise unveränderlich sein, während @LastModifiedBy bei Änderungen der Entität aktualisiert wird (normalerweise vor dem UPDATE). Und das alles funktioniert nur im Rahmen des aktivierten Auditing und des entity listeners.
Die Annotationen stammen aus Spring Data, nicht aus JPA:
- org.springframework.data.annotation.CreatedBy
- org.springframework.data.annotation.LastModifiedBy
Die Verarbeitung erfolgt über den AuditingEntityListener, den wir bereits in früheren Vorlesungen eingebunden haben.
Aus praktischer Sicht werden die Felder meist als Strings definiert. In unserem Stadium ist das besonders praktisch: Wir haben keine Benutzer, Rollen oder Logins (und wir wollen nicht plötzlich die Hälfte von Spring Security nur für eine einzige Zeile aufbauen). Daher ist der pragmatischste Typ für den Kurs String: zum Beispiel "system", "manual", "migration", "catalog-service".
Unten sehen Sie einen minimalen Ausschnitt von Entitätsfeldern, der die beiden Rollen und ein sauberes Mapping zeigt. Beachten Sie: createdBy wird sinnvollerweise als updatable = false gesetzt, da der „Erstellungsautor“ bei jeder Preisänderung nicht überschrieben werden sollte.
import jakarta.persistence.Column;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
// Wird einmal bei der Erstellung der Entität ausgefüllt (normalerweise vor INSERT)
@CreatedBy
@Column(name = "created_by", updatable = false, length = 50) // updatable=false: "Erstellungsautor" soll bei UPDATE nicht überschrieben werden
private String createdBy;
// Wird bei jeder Änderung der Entität aktualisiert (normalerweise vor UPDATE)
@LastModifiedBy
@Column(name = "last_modified_by", length = 50)
private String lastModifiedBy;
Ein wichtiger Punkt, der Nerven spart: Diese Felder können null bleiben, und das ist in Ordnung. Wenn AuditorAware Optional.empty() zurückgibt, wird Spring keinen „Autor für Sie erfinden“. Daher macht man solche Spalten auf Datenbankebene oft nullable, besonders bei der schrittweisen Einführung des Auditing in ein bereits bestehendes Projekt (und wir sind in einem Projekt, das nicht mehr auf einer „leeren Datenbank“ basiert).
3. Autorquelle: Vertrag AuditorAware<T>
An dieser Stelle haben Anfänger normalerweise die Frage: „Okay, die Annotation sagt, was gefüllt werden soll, aber woher kommt der Wert?“. Spring Data JPA ist kein Gedankenleser, es errät weder Ihre Gedanken noch den Benutzer anhand des Gesichtsausdrucks der JVM. Dafür gibt es das Interface AuditorAware<T>. Es ist ein einfacher Vertrag: Wenn der Autor gesetzt werden muss, ruft Spring Ihren Code auf und fragt: „Wer ist der aktuelle Autor?“. Sie antworten mit Optional<T> – entweder ein Wert oder „ich weiß nicht“.
Das Interface sieht so aus (sinngemäß vereinfacht):
- es gibt die Methode getCurrentAuditor()
- sie gibt Optional<T> zurück
- T ist Ihr Autortyp (bei uns wird es String sein)
Der entscheidende Punkt ist, dass AuditorAware so leicht und vorhersagbar wie möglich sein sollte. Es ist nicht der Ort für Datenbankzugriffe oder komplexe Berechnungen. In einer normalen Anwendung holt AuditorAware den Autor aus dem Ausführungskontext (z. B. aus dem Sicherheitskontext). In unserem Projekt gibt es noch kein echtes Benutzermodell, daher wählen wir eine ehrliche und verständliche Quelle: „Alle Änderungen werden vom System durchgeführt“ oder „die Quelle wird aus einer einfachen Konfiguration bezogen“.
Und noch ein Punkt: Optional ist hier nicht „um zu verkomplizieren“, sondern um das Verhalten explizit zu machen. Wenn der Autor unbekannt ist, ist es besser, createdBy = null zu lassen, als eine zufällige Zeichenfolge hineinzuschreiben und sich dann ein Jahr lang zu wundern, warum die Hälfte der Datensätze in der Datenbank vom Benutzer "undefined" erstellt wurde (ja, das ist kein Witz, das ist ein echter Klassiker).
4. Minimaler AuditorAware<String> für shop-data-jpa
In der Praxis besteht das Einbinden des Author-Auditing aus zwei Schritten. Erstens: @EnableJpaAuditing mitteilen, dass es einen Bean gibt, der den aktuellen Autor liefern kann. Zweitens: diesen Bean so einfach und stabil wie möglich implementieren. Und hier ist es wichtig, es nicht zu übertreiben: Wir befinden uns in einem Data-Layer-Kurs, nicht beim Bau eines „IAM-Systems mit 15 Microservices“.
Auditing mit auditorAwareRef aktivieren
Wenn Sie in der vorherigen Vorlesung Auditing so aktiviert haben:
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing // Aktiviert die Auditing-Infrastruktur (createdAt/updatedAt usw.)
public class JpaAuditConfig {
}
dann fügen wir für die Autorfelder einen Verweis auf den Bean über auditorAwareRef hinzu:
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
// auditorAwareRef verweist auf den Bean, der die Frage "wer ist der aktuelle Autor?" beantwortet
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaAuditConfig {
}
Hier ist der Name "auditorAware" kein „magisches Wort des Universums“, sondern ein Verweis auf einen konkreten Spring-Bean mit diesem Namen.
Die minimalste Variante: Immer "system" zurückgeben
Für ein Übungsprojekt ist das ein ehrlicher und funktionierender Start. Es liefert korrekt gefüllte Felder, erfordert keine Kontexte, bricht nicht bei Multithreading und verlangt nicht zu erklären, wer der „Benutzer“ in einem System ohne Benutzer ist.
import java.util.Optional;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
@Component("auditorAware") // wichtig: der Name muss mit auditorAwareRef übereinstimmen
public class FixedAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Das einfachste Szenario für den Kurs: Alle Änderungen erfolgen "im Namen des Systems"
return Optional.of("system");
}
}
Ja, das sieht fast zu einfach aus. Aber das ist der Sinn: Zuerst lernen wir, den Mechanismus korrekt einzubinden, anstatt einen „perfekten Autorfußabdruck im Sand“ zu bauen.
Quelle aus der Konfiguration
Manchmal ist es praktisch, in der Dev-Umgebung zu sehen, dass Daten „von Hand nachgebessert“ oder „über ein Übungs-Seed erstellt“ wurden. Dann kann man den Wert aus einer Property lesen. Es geht immer noch nicht um Benutzer, sondern um die Quelle.
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
@Component("auditorAware") // Der Bean-Name ist wieder wichtig wegen auditorAwareRef
public class PropertyAuditorAware implements AuditorAware<String> {
private final String source;
public PropertyAuditorAware(@Value("${shop.audit.source:system}") String source) {
// Quelle aus application.yml holen, falls nicht gesetzt "system" verwenden
this.source = source;
}
@Override
public Optional<String> getCurrentAuditor() {
// Gibt eine stabile, menschenlesbare "Quelle" der Änderungen zurück
return Optional.of(source);
}
}
Wenn Sie in application-dev.yml shop.audit.source: manual setzen, werden alle Einträge den Autor "manual" haben. Das ist nicht supergenau, aber schon praktisch: Sie beginnen, die Datenherkunft zu unterscheiden, ohne das Benutzerthema anzurühren.
5. Wo die Autorfelder speichern
Sobald createdBy und lastModifiedBy vorhanden sind, werden Sie schnell in Versuchung geraten: „Oh! Packen wir sie in unsere BaseAuditEntity und vergessen sie“. Manchmal ist das wirklich in Ordnung. Aber denken Sie an die Idee des Tages: Nicht alle Entitäten brauchen denselben Satz Felder. Wenn Sie eine große Basisklasse „für alle Lebenslagen“ erstellen, erhalten Sie mehr Rauschen als Nutzen und beginnen, Auditing-Felder dorthin zu ziehen, wo sie niemand liest. Für die aktuelle Baseline von mini-shop ist das nicht nötig: Die Basiskonfiguration des Projekts kommt mit dem Änderungszeitpunkt aus, und die Autorfelder bleiben eine punktuelle zusätzliche Option.
Der ruhigste Ansatz für unser Projekt sieht so aus: BaseAuditEntity bleibt schlank und ist nur für die Zeit zuständig (createdAt, updatedAt) sowie die Einbindung des Listeners, und die Autorfelder fügen Sie nur in den Entitäten hinzu, in denen dies wirklich zur Datenuntersuchung beiträgt. Zum Beispiel kann dies für CustomerOrder nützlich sein (Bestellungen werden oft in verschiedenen Szenarien erstellt/geändert), während es für manche seltenen Nachschlagewerke bereits fraglich ist.
Unten ein Beispiel, wie man Autorfelder direkt in eine Entität einfügt, die bereits die Basis-Auditing-Klasse erbt. Da der Listener bereits eingebunden ist (z. B. auf der Basisklasse), werden die Felder automatisch gefüllt.
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
@Entity
public class CustomerOrder extends BaseAuditEntity {
@Id
private Long id;
@CreatedBy
@Column(updatable = false, length = 50) // Wird bei Erstellung einmal geschrieben, danach nicht mehr geändert
private String createdBy;
@LastModifiedBy
@Column(length = 50) // Wird bei jedem UPDATE aktualisiert
private String lastModifiedBy;
}
Sollte sich herausstellen, dass die Autorfelder in fast allen Entitäten benötigt werden, kann man sie in die Basisklasse verschieben. Der grundlegende Audit-Vertrag von mini-shop hängt jedoch nicht davon ab: Zuerst reicht es, die Mechanik an 1–2 Entitäten zu sehen und zu verstehen, ob es einen echten Nutzen bringt.
6. Wann createdBy und lastModifiedBy gefüllt werden
Von außen sieht Auditing so aus, als ob die Werte „von selbst erscheinen“. Aber wir sind bereits alt genug (zumindest nach Maßstäben des persistence context), um an spontane Selbstentstehung von Daten zu glauben. Tatsächlich passiert alles zu bestimmten Zeitpunkten: beim persist/Erstellen der Entität und beim Aktualisieren der Entität. Dabei muss die Aktualisierung nicht als explizites save() aussehen: Wenn die Entität managed ist und Sie ihr Feld innerhalb einer Transaktion ändern, greift das dirty checking, dann erzeugt Hibernate ein UPDATE, und davor setzt das Auditing updatedAt/lastModifiedBy.
Es ist praktisch, dies als kleine Ereignisabfolge darzustellen:
sequenceDiagram
participant S as "Service @Transactional"
participant PC as "Persistence Context"
participant A as "AuditingEntityListener"
participant DB as "PostgreSQL"
S->>PC: lade Entity (managed)
S->>PC: ändere Feld (dirty)
S->>PC: Transaktion endet (commit)
PC->>A: "before flush/update: fülle updatedAt/lastModifiedBy"
PC->>DB: "UPDATE ... updated_at=?, last_modified_by=? WHERE id=?"
Aus praktischer Sicht ergeben sich zwei wichtige Schlussfolgerungen.
Erste Schlussfolgerung: Wenn Sie erwartet haben, dass lastModifiedBy „sofort nach dem Setter“ geändert wird, werden Sie enttäuscht sein. Es wird aktualisiert, wenn die tatsächliche Synchronisierung mit der Datenbank stattfindet – meistens beim flush vor dem Commit. Dies entspricht vollständig unserem vorherigen Thema über flush und „SQL geht nicht immer sofort in die Datenbank“.
Zweite Schlussfolgerung: Author-Auditing harmoniert hervorragend mit unserem Service-Stil, bei dem wir eine „saubere Geschäftsänderung“ ohne überflüssige save() nach jeder Änderung durchführen. Zum Beispiel wird der folgende Code korrekt sowohl das Geschäftsfeld als auch updatedAt und lastModifiedBy ändern:
import java.math.BigDecimal;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void changePrice(Long productId, BigDecimal newPrice) {
// 1) Managed-Entität laden
Product p = productRepository.findById(productId).orElseThrow();
// 2) Geschäftsfeld ändern
p.setPrice(newPrice);
// 3) Explizites save() ist nicht nötig: dirty checking + Auditing greifen beim flush/commit
}
Und das SQL (sinngemäß vereinfacht) würde etwa so aussehen:
-- Hibernate aktualisiert sowohl die Geschäftsspalte als auch die Audit-Spalten in einem einzigen UPDATE
update product
set price = ?, updated_at = ?, last_modified_by = ?
where id = ?;
Das heißt, das Auditing fügt „Dienstspalten“ in die gleichen INSERT/UPDATE ein, die ohnehin stattfinden.
7. Typische Fehler im Author-Auditing
Author-Auditing sieht einfach aus, aber die Fehler sind ziemlich tückisch: Alles kompiliert, die Anwendung startet, aber in der Datenbank sind plötzlich leere Felder oder seltsame Werte. Unten die häufigsten Stolpersteine, auf die selbst sorgfältige Entwickler treten (und wir als zukünftige sorgfältige Entwickler sollten nur einmal darauf treten, und zwar ausschließlich zu Übungszwecken).
Fehler Nr. 1: Erwarten, dass createdBy bereits durch die Annotation am Feld gefüllt wird.
Häufig setzt man @CreatedBy und @LastModifiedBy, vergisst aber, dass Auditing eine Infrastruktur ist. Wenn @EnableJpaAuditing nicht aktiviert oder AuditingEntityListener nicht angebunden ist (an der Entität oder an @MappedSuperclass), führt Spring Data die Audit-Mechanik einfach nicht aus. In der Folge bleiben die Felder null, und Sie schauen sie mit dem Gesichtsausdruck „aber ich habe doch die Annotation gesetzt“ an.
Fehler Nr. 2: auditorAwareRef angeben, aber keinen Bean mit diesem Namen erstellen.
Der Verweis auditorAwareRef = "auditorAware" ist eine feste Bindung an den Bean-Namen. Wenn Sie @Component ohne Namen verwenden und der Bean daher anders heißt, findet das Auditing Ihren AuditorAware nicht. Je nach Konfiguration erhalten Sie entweder einen Fehler beim Start oder ein stillschweigend nicht funktionierendes Author-Auditing. Der zuverlässigste Weg in einem Übungsprojekt ist, den Bean explizit zu benennen: @Component("auditorAware").
Fehler Nr. 3: createdBy mit Geschäftsdaten verwechseln, z. B. mit customerEmail.
Dies ist ein sehr „menschlicher“ Fehler: Da customerEmail bereits existiert, möchte man es auch in createdBy schreiben, weil „die Bestellung ja für den Kunden ist“. Aber die Logik ist eine andere. customerEmail sind Daten über den Kunden. createdBy sind Daten über die Änderungsquelle im System. In einer echten Anwendung kann die Bestellung von einem Operator, einer Integration, einem automatischen Prozess oder dem Kunden selbst erstellt werden. Diese Rollen zu vermischen bedeutet, den Sinn der Daten zu brechen und dann noch zu versuchen, Berichte zu reparieren, die plötzlich zählen, „wer die Bestellungen erstellt hat“.
Fehler Nr. 4: Versuchen, @Version durch Autorfelder oder Zeit zu ersetzen.
Manchmal denkt man: „Na, wenn ich updatedAt kenne, bin ich vor Konflikten geschützt.“ Nein. updatedAt und lastModifiedBy sind Metadaten. Sie schützen nicht vor lost update. Der Schutz erfolgt durch @Version und optimistisches Locking, weil dort der Konflikt auf der Ebene von WHERE version = ? im UPDATE geprüft wird. Also sollte man nicht „ein Versionsfeld sparen“ um den Preis von verlorenen Aktualisierungen und sehr traurigen Beständen im Lager.
Fehler Nr. 5: Den Autor als „zufälligen“ oder instabilen Wert setzen.
Solange es keinen Sicherheitskontext gibt, ist es sehr verlockend, irgendwelche „temporären“ Werte in den AuditorAware einzufügen: aktuellen Timestamp, random UUID, Thread-Name, Mondphase. Am Ende wird createdBy zu einer Sammlung zufälliger Zeichenfolgen, die niemand interpretieren kann. Wenn Sie keine stabile Semantik für den Autor liefern können, ist es besser, ehrlich Optional.empty() zurückzugeben und das Feld leer zu lassen, als es mit Müll zu füllen, der sowohl Ihnen als auch der Datenbank im Weg steht.
GO TO FULL VERSION