Wir nutzen eigentlich bei allen Services bzw. allen Hibernate-Entities immer die @Version-Annotation für Optimistic Locking.
Siehe dazu auch:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#locking-optimistic-mapping
Bei einem speziellen Service sind uns in letzter Zeit aber immer wieder Datensätze mit sehr hohen Werten bei der Version aufgefallen, die eigentlich nicht durch reguläre Updates (durch die Business Logik) erklärbar waren.
Die erste Vermutung ging in die Richtung eines Concurrency-Problems, allerdings konnte das auch relativ schnell ausgeschlossen werden.
Glücklicherweise konnten wir das Problem durch Experimentieren mit unterschiedlichen Testfällen relativ schnell auch lokal reproduzieren.
Im Speziellen haben wir festgestellt, dass hier vor einem Select-Query zu einzelnen Entities Updates durchgeführt wurden, obwohl diese eigentlich gar nicht geändert wurden.
Siehe dazu auch:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#flushing-auto
Eine weitere Analyse ergab, dass das Problem bei einer von uns definierten Java-Klasse lag, die in der Datenbank auf eine JSON-Column gemappt wurde.
Hier ein vereinfachtes Beispiel dazu:
@Entity(name = "MyPerson")
public static class MyPerson {
@Id
@GeneratedValue
Long id;
String name;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "my_json")
MyJson myJson;
@Version
int version;
public MyPerson() {
}
public MyPerson(String name) {
this.name = name;
}
}
public static class MyJson {
String text = "fubar";
}
Sieht im Grunde eigentlich in Ordnung aus. Wenn man damit aber einen ganz einfachen Test ausführt …
@Test
void savePerson(SessionFactoryScope scope) {
scope.inTransaction(session -> {
MyPerson person = new MyPerson("name");
person.myJson = new MyJson();
session.persist(person);
});
}
… sieht man, dass hier nach dem Insert ein zusätzliches Update durchgeführt wird:
Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Aber woran liegt das?
Das Problem ist schlicht und einfach, dass vergessen wurde in der Klasse MyJson die equals()- und hashCode()-Methoden zu überschreiben. Erweitert man das vorhergehende Beispiel wie folgt um eine equals()-Methode wird auch das Update-Query nicht mehr getriggert:
public static class MyJson {
String text = "fubar";
public boolean equals(Object other) {
return other instanceof MyJson myJson && text.equals(myJson.text);
}
}
Das Überschreiben der equals()-Methode garantiert die korrekte Funktionsweise des dirty checking Mechanismus von Hibernate, welcher sicherstellt, dass Änderungen in den Java-Entities nach Änderungen auch zur Datenbank synchronisiert werden.
Bei JSONs ist es ein wenig komplizierter, weil ein Objekt nach Serialisierung und anschließender Deserialisierung nicht mehr dem Original entspricht und damit auch Hibernate glaubt, dass sich an dem Objekt etwas geändert hat (obwohl sich die beiden Objekte nur durch die Object Identity, nicht aber im Inhalt unterscheiden). Aus diesem Grund ist es empfehlenswert bei eigenen Klassen, die als JSON in der Datenbank gespeichert werden sollen, darauf zu achten, dass auch die equals()- und hashCode()-Methode entsprechend korrekt sind.
Es kann aber noch schlimmer werden…
Hat man nämlich eine Batch-Verarbeitung implementiert, bei der man mehrere Objekte eines Typs zuvor selektiert hat, somit im Persistence Context hält und in einer Schleife zu jedem Objekt ein weiteres Select-Query ausführt, wird vor jedem Select für jede Entity ein Update-Query ausgeführt. Somit können also schnell n² Updates entstehen, selbst wenn man nichts ändern wollte.
Hier ein vereinfachtes Beispiel:
@Test
void saveThreePersons_batchProcessing(SessionFactoryScope scope) {
scope.inTransaction(session -> {
MyPerson first = new MyPerson("name");
first.myJson = new MyJson();
session.persist(first);
MyPerson second = new MyPerson("name");
second.myJson = new MyJson();
session.persist(second);
MyPerson third = new MyPerson("name");
third.myJson = new MyJson();
session.persist(third);
List<MyPerson> persons = session.createQuery("select p from MyPerson p", MyPerson.class).getResultList();
for (MyPerson person : persons) {
session.createQuery("select p from MyPerson p where p.id=:id", MyPerson.class).setParameter("id", person.id).getSingleResult();
}
});
}
In diesem Beispiel wird eine Vielzahl von Updates ausgeführt:
Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0 where mp1_0.id=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0 where mp1_0.id=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0 where mp1_0.id=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Im übrigen kann man in solchen Situationen auch den FlushMode einzelner Queries von AutoCommit auf Manual ändern.
Siehe dazu auch:
https://thorben-janssen.com/flushmode-in-jpa-and-hibernate/#how-to-configure-the-flushmode
Das sollte aber auch nur dann genutzt werden, wenn man sich wirklich sicher ist, dass sich die Entities zuvor wirklich nicht geändert haben, die man hier selektieren will.
Link zum vollständigen Test:
https://github.com/peter1123581321/hibernate-test-case-templates/blob/json-version-bug/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java
