Hibernate Envers bietet die Möglichkeit zur Versionierung von Hibernate Entitäten über separate Audit-Tabellen. Das funktioniert auch als JPA-Provider. Wo man im klassischen Ansatz INSERT/UPDATE/DELETE Trigger und Stored-Procedures einsetzt, um Auditierung auf Tabellen-Ebene durchzuführen, ist Envers im Java-Spring Umfeld eine sehr interessante Alternative.
Hierzu stellt Envers eine Reihe von Annotationen zur Verfügung, mit denen die Auditierung deklarativ vorgenommen werden kann. Das funktioniert auf Klassenebe , auf Feldebene und sogar für Reletionsbeziehungen. Die Dokumentation ist gut verständlich und übersichtlich. Eine Entität Approval
könnte beispielsweise wie folgt mit einer Audit-Annotation deklariert werden:
@Entity @Table(name = "approvals") @Data
@Audited
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Approval {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@EqualsAndHashCode.Include
@Column(unique = true, updatable = false, nullable = false)
private String approvalNumber;
@Enumerated(EnumType.STRING) @NotNull
private ApprovalStatusType status;
private String title;
private String description;
@OneToOne(cascade = CascadeType.ALL)
private Person cancelInitiator;
}
Das ist schon alles! Jetzt ist nur noch dafür zu sorgen, dass die Audit-Tabelle approval_aud
mit den erforderlichen Spalten vorhanden ist, und Hibernate protokolliert dort dann automatisch alle Änderungen.
FEHLER: Relation »hibernate_sequence« existiert nicht
In dem ganz konkreten Fall funktioniert das mit H2 als Datenbank ganz wunderbar und ohne Probleme. Mit dem Ausrollen auf unsere Stage-Umgebung sehen wir dann allerdings die Exception:
org.postgresql.util.PSQLException: FEHLER: Relation »hibernate_sequence« existiert nicht
Position: 17
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2532)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2267)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:312)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:448)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:369)
at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:153)
at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:103)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
Wie kommt das? Nun, Envers erzeugt pro Vorgang zwei Dinge:
- Eine eindeutige Revisionsnummer in der übergeordneten Tabelle REVINFO.
- Einen Eintrag in der Entity-spezifischen Audit-Tabelle bei Hinterlegung der vorgenannten Revisionsnummer.
Für REVINFO benutzt Envers intern eine eigene Entity:
@MappedSuperclass
public class DefaultRevisionEntity implements Serializable {
private static final long serialVersionUID = 8530213963961662300L;
@Id
@GeneratedValue
@RevisionNumber
private int id;
@RevisionTimestamp
private long timestamp;
...
Die MappedSupereclass
definiert keine explizite Strategie, so dass der Default greift. Auf unserer Stage-Umgebung ist eine Postgres-Datenbank installiert, und Hibernate wendet in diesem Fall die GenerationType.SEQUENCE Strategie an. Ist die hierfür erforderliche Sequenz in der Datenbank nicht vorhanden, dann kommt es zu o.g. Exception. Wird die Datenbank – wie auf dem Dev-Laptops üblich – automatisch über Hibernate generiert, so fällt auch das nicht unmittelbar auf. Ab Stage rollen wir Schema-Änderungen jedoch explizit über Flyway aus, so dass in diesem Fall die Sequenz tatsächlich nicht vorhanden ist.
Eigene Revision Enity
Als Lösung wird die DefaultRevisionEntity
durch eine CustomRevisionEntity
ersetzt. Hierbei ist wichtig, dass die Entity die @RevisionEntity
Annotation bekommt und die beiden Revisionfelder die Annotationen @RevisionNumber
bzw. @RevisionTimestamp
. Das Ergebnis sieht dann so aus:
@Entity
@Table(name = "REVINFO")
@RevisionEntity
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class CustomRevisionEntity implements Serializable {
...
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@RevisionNumber
@EqualsAndHashCode.Include
@Column(name = "REV")
private Long id;
@RevisionTimestamp
@EqualsAndHashCode.Include
@Column(name = "REVTSTMP")
private long timestamp;
}
Mit dieser Version läuft die Applikation auch unter Postgres.
Revision Entity erweitern
In einer Spring-Security Web-Application kann die Entity jetzt zusätzlich noch um Infos des initiierenden Benutzers ergänzt werden. Hierfür stellt Hibernate Envers einen Listener-Ansatz zur Verfügung, der ebenfalls über die Annotation registriert werden kann.
@RevisionEntity(CustomRevisionEntityListener.class)
Der Listener implementiert das RevisionListener
Interface mit einer Methode, die mit jeder neuen Revisionen aufgerufen wird. Hat man dort einen SecurityContext
zur Hand, so kann der angemeldete User sehr einfach mit in die Revionstabelle vermerkt werden:
@Slf4j
@Component
public class CustomRevisionEntityListener implements RevisionListener {
@Override
public void newRevision(final Object revisionEntity) {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Optional.ofNullable(authentication)
.map(Authentication::getPrincipal)
.filter(Objects::nonNull)
.filter(principle -> principle instanceof MyUserDetails)
.map(MyUserDetails.class::cast)
.ifPresent(userDetails -> ((CustomRevisionEntity)revisionEntity).setUsername(userDetails.getUsername()));
}
}