Soft-Delete mit Spring-Data und Hibernate Annotations
Intro
Wenn man eine Webapplikation betreibt die dem User in irgendeiner Weise Datenmanipulation erlaubt, kann es durchaus vorkommen, dass ebendieser User auf die Idee kommt einen Datensatz zu löschen. In der Regel, und ich denke es ist guter Stil, möchte man wohl nicht wirklich zulassen, dass der Datensatz gelöscht wird, sondern man möchte vielmehr die Daten nur als gelöscht markieren.
Setzt man auf Spring-Data, so möchte man allerdings nicht die Attribute einer Entity verändern um sie als gelöscht zu markieren, sondern man möchte vielmehr einfach die Delete-Methode des jeweiligen Repositories aufrufen. Wie wir dieses transparente Setup mit extrem wenigen Handgriffen hinbekommen ist Thema dieses Posts.
Setup
Wenn dir die Beschreibung hier nicht reicht, habe ich das Beispiel auch noch in ein kleines Github Projekt integriert. Du bist herzlichst eingeladen das Projekt auszuchecken oder ein Sternchen zu verteilen. ;)
Zunächst einmal beziehe ich mich hier auf ein Spring-Boot-Projekt mit einer einzigen Dependency, nämlich:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath/>
</parent>
<!-- ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- ... -->
Hinzufügen einer Entity mit deleted-Flag
Als nächstes brauchen wir eine Entity, die gespeichert werden soll. Also erstellen wir uns mal für unseren Beispielfall ein User Objekt:
@Entity
@Table(name = "t_user")
public class User implements Serializable {
@Id
private Integer id;
@Column(length = 100)
private String username;
@Column(length = 100)
private String password;
@Column(name = "deleted")
private boolean deleted = false;
// ... Getter and setter.
}
Deleted bekommt den default Wert false und soll später unser Indikator dafür sein, ob ein Datensatz gelöscht wurde. Ansonsten hat das User-Objekt so ungefähr die Properties, die man sich von einem User-Objekt so erwartet.
In meinem Github Beispiel habe ich die deleted-Flag in eine MappedSuperclass ausgelagert. Denn ich bin der Meinung, dass eigentlich alle Entities in meinem Datenbankmodell eine solche Flag brauchen. Ein schönes Beispiel für eine MappedSuperclass findest du in einem anderen Artikel von mir.
Hinzufügen der Soft-Delete-Annotationen
Um das Soft-Delete-Feature mit Spring-Data zu nutzen benötigen wir nur die folgenden zwei Hibernate Annotationen an unserer Entity: (Das ganze funktioniert auch mit anderen Persistence-APIs. Die Hauptsache, ist in diesem Fall, dass Hibernate als Object-Relational-Mapper genutzt wird.)
@Entity
@Table(name = "t_user")
@Where(clause = "deleted='false'")
@SQLDelete(sql = "UPDATE t_user SET deleted=true WHERE id=?")
public class User implements Serializable {
// ...
In Zeile 3 grenzen wir die Ergebnismenge automatisch über eine Where-Klausel ein. Bei jeder Query, die automatisch - über den ORM zusammengebaut wird - hängt Hibernate jetzt diese Where-Klausel an, sobald die Tabelle _tuser im Spiel ist. Damit wird dafür gesorgt, dass nur noch die Entities geladen werden, die nicht als deleted markiert sind. Falls wir doch einmal an die gelöschten Items wollen, müssen wir mit native-Queries arbeiten.
In Zeile 4 findet das Setzen der deleted-Flag statt. Bei jedem SQL-Delete, was an Hibernate gereicht wird, ignoriert Hibernate die eigentlich definierte Delete-Operation und führt stattdessen die SQL-Operation aus, die in in der Annotation SQLDelete definiert wird. Der Parameter ? ist dabei die ID des Entries.
Hinweis für Optimistic-Locking
Arbeitet man neben Soft-Deletes auch mit Optimistic-Locking - also einer @Version-Annotation, so führt der o.g. Code zu einer SQLException:
Caused by: java.sql.SQLException: Invalid argument in JDBC call: parameter index out of range: 2
Hibernate erwartet in der SQLDelete-Annotation in diesem Fall einen weiteren Parameter. Nämlich den Parameter, der das optimistic-locking übernimmt. In meinem Github Beispiel ist dies die Variable version, so dass die Annotation dann wie folgt aussieht:
@Entity
@Table(name = "t_user")
@Where(clause = "deleted='false'")
@SQLDelete(sql = "UPDATE t_user SET deleted = true WHERE id = ? and version = ?")
public class User extends BaseEntity {
Einen ausführlicheren Beitrag zu Optimistic-Locking findest du hier.
Fazit
Wir haben jetzt unsere gelöschten Datensätze weiterhin in der Datenbank, obwohl unser ORM diese bei jeder Query automatisch filtert. Damit haben wir mehr Datensicherheit, ohne Anpassung der delete- auf update-Statements.
Cheers!