a dev's blog

Some thoughts about thoughts.

Optimistic Locking mit JPA

2016-03-11 Development Java

Intro

Gerade wenn eine Anwendung komplexer wird und es möglich ist die gleichen Daten an verschiedenen Stellen der Anwendung zu modifizieren oder aber mehrere Anwender auf der gleichen Datenbasis operieren, kommen wir wohl oder übel zu dem Punkt, dass Werte von Datensätzen durch andere konkurrierende Schreibzugriffe überschrieben werden können.

Hier ein anschauliches Beispiel zur Problemstellung: Zwei User fordern von einer Applikation gleichzeitig das gleiche Objekt an. Beide User ändern dieses Objekt jetzt und schicken es zurück zur Applikation. Ohne einer expliziten Strategie würde zunächst das erste Objekt, das eintrifft, gespeichert werden, dann würde es durch das zweite überschrieben werden und die Eingaben des ersten Users wären für uns verloren. Und das ist Mist… Daher lohnt es sich eine Strategie zu besitzen, wie man mit dem Thema umgeht. Und eine solche Strategie ist optimistic-locking, die im Standard JPA 2.0 enthalten ist. Es gibt noch mehr in JPA 2.0 enthaltene Locking Strategien, die für mich allerdings noch nicht praxisrelevant geworden sind, jedoch sicherlich ihre Daseins- Berechtigung besitzen.

Runnable Example

Von der Theorie her brauchst du kein explizites Setup, da die Beschreibung JPA konform ist. Da ich aber gerne mit lauffähigen Beispielen arbeite habe ich ein kleines Github-Projekt angelegt, was über einen JUnit-Test verfügt, der das in diesem Post besprochene Beispiel testet.

How it works

Bei der JPA 2.0 Implementierung des Optimistic-Locking wird in dem zu speichernden Objekt eine Attribut erwartet, welchen als Versionsnummer dienst. Dein JPA-Provider prüft vor jedem Speichervorgang, ob die Versionsnummer des Objekts, was du ihm zum speichern gibst, äquivalent zu der Versionsnummer ist die in der Datenbank steht. Falls das nicht der Fall ist wird mit einer Exception ausgestiegen. Ist es jedoch der Fall und das Objekt kann gespeichert werden, so wird von deinem JPA-Provider die Versionsnummer vor diesem Speichervorgang inkrementiert, so dass eine neue Versionsnummer zu dem Datensatz in der Datenbank steht.

How to add a version

Eine Versionsnummer hinzuzufügen ist kein großer Akt. Bestenfalls hast du eine eigene Mapped-Superclass die für alle deine Entities gilt, die du um die entsprechenden Zeilen erweiterst. Wie du zu einer Mapped-Superclass kommst habe ich in meinem Blog-Post Eine schöne Mapped-Superclass mit Spring-Data und JPA beschrieben. Wenn du keine Mapped-Superclass bei dir im Projekt hast, kannst du als Ausgangsbasis eine beliebige Entity aus deinem Projekt nehmen. Als nächstes wird die Versionsnummer zu der Entity hinzugefügt.

@Entity
@Table(name = "t_movie")
public class Movie implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  @Version
  private Integer version;

  // ...
}

Hier findet sich die Versionsnummer in den Zeilen 9 und 10.

An der Stelle ist es noch wichtig zu wissen welchen Datentyp man mit der Versionsnummer annotiert. Diese lässt nämlich neben Zahlentypen short, int long auch java.sql.Timestamp zu. Es ist zwar unwahrscheinlich, jedoch nicht ausgeschlossen, dass Objekte zum selben Zeitpunkt in die Datenbank geschrieben werden können. Und je schneller unsere CPUs werden, umso höher ist die Wahrscheinlichkeit, dass etwas klemmen könnte. Daher meine persönliche Empfehlung: Je nach Domäne den richtigen Zahlentyp raussuchen und als Versionsnummer verwenden.

Hinweise für Unit-Testing mit Optimistic-Locking

Transactional

Wählt man die Spring-Annotation @Transactional über seinem Unit-Test, so wird bei konkurrierenden Schreibzugriffen keine Exception (in dem Fall ObjectOptimisticLockingFailureException geworfen) und der Datensatz wird kommentarlos überschrieben!

Attached Entities

In der Regel wird heute mit Attached Entities gearbeitet. Wenn man also direkt auf den Objekten arbeitet, die aus der Datenbank kommen kann es sein, dass keine Exception geworfen wird, auch wenn dies nach der Idee des Optimistic-Lockings eigentlich hätte passieren müssen. Daher lohnt es sich meiner Meinung nach User-Operationen in jedem Fall auf Detached Entities durchzuführen oder die Entities selbst zuvor in DTOs zu transformieren, die Veränderung durchzuführen und wieder zurück zu transformieren (wie bspw. in meinem Unit-Test des Beispiel-Projekts).

Fazit

Zwei Zeilen Code + Import und wir haben ein Optimistic-Locking in unserer JPA-Applikation, ein sicheres Verhalten und vor allen Dingen eine Strategie von der wir wissen wie sie funktioniert.

Cheers!