Wonderful Version annotation in JPA
Introduction
So, let's begin! What does the Version annotation in JPA mean ?
In short, she is responsible for blocking in JPA. This annotation solves one of the problems that may arise as a result of parallel execution of transactions.
What problems may arise?
- Lost updates may occur in situations where two transactions running in parallel, trying to update the same data.
- Dirty reads occur when a transaction sees changes not yet made, made by another transaction. In such a case, a problem may arise due to the rollback of the second transaction, but the data have already been read first.
- Non-repeatable reads occur when the first transaction received data, and the second transaction made a change to them and successfully committed them, until the end of the first transaction. In other words, when, within the framework of a single transaction, the same request for receiving, for example, the entire table, returns different results.
- Phantom reading is a problem similar to non-repeatable reads, except that a different number of rows is returned.
Briefly about their decisions
- READ UNCOMMITED - solved using the Version annotation in JPA (this is what the article is about)
- READ COMMITED - allows you to read only committed changes
- REPEATABLE READ - a little more complicated here. Our transaction "does not see" changes to the data that it had previously read, and other transactions can not change the data that fell into our transaction.
- SERIALIZABLE - sequential execution of transactions
Each subsequent clause covers all previous ones; in other words, it can replace the solutions mentioned earlier. Thus SERIALIZABLE has the highest level of isolation, and READ UNCOMMITED is the lowest.
Version
Version solves the problem with lost updates . How exactly, now and see.
Before proceeding to the code, it is necessary to stipulate that there are two types of locks: optimistic and pessimistic . The difference is that the former focus on situations in which multiple transactions try to change one field at the same time, occur extremely rarely, while others focus on the reverse situation. In accordance with this, there is a difference in their execution logic.
The optimisticwhen locks are committed to the database, the value of the field marked as version is compared at the time of receiving the data and at the moment. If it has changed, then there is some other transaction ahead of ours and managed to change the data, then in this case our transaction throws an error, and you need to restart it.
When using optimistic locks, a higher level of concurrency is ensured when accessing the database, but in this case, you have to repeat transactions that did not have time to make changes before others.
In the pessimistic, the same lock is applied immediately before the intended modification of the data to all the lines that this modification presumably affects.
And when usingpessimistic locks are guaranteed no contradictions during the execution of the transaction, by placing the rest in standby mode (but time is wasted), as a result of a decrease in the level of competitiveness.
LockModeType or how to set a lock
Blocking can be set by calling the EntityManager look method.
entityManager.lock(myObject, LockModeType.OPTIMISTIC);
LockModeType sets the blocking strategy.
LockModeType is 6 types (2 of which are optimistic and 3 of which are pessimistic ):
- NONE - no lock
- OPTIMISTIC
- OPTIMISTIC_FORCE_INCREMENT
- PESSIMISTIC_READ
- PESSIMISTIC_WRITE
- PESSIMISTIC_FORCE_INCREMENT
Create our Entity
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@EntityListeners(OperationListenerForMyEntity.class)
@Entity
public class MyEntity{
@Version
private long version;
@Id
@GeneratedValue
@Getter
@Setter
private Integer id;
@Getter
@Setter
private String value;
@Override
public String toString() {
return "MyEntity{" +
"id=" + id +
", version=" + version +
", value='" + value + '\'' +
'}';
}
}
Create a class where all Callback methods will be implemented.
import javax.persistence.*;
public class OperationListenerForMyEntity {
@PostLoad
public void postLoad(MyEntity obj) {
System.out.println("Loaded operation: " + obj);
}
@PrePersist
public void prePersist(MyEntity obj) {
System.out.println("Pre-Persistiting operation: " + obj);
}
@PostPersist
public void postPersist(MyEntity obj) {
System.out.println("Post-Persist operation: " + obj);
}
@PreRemove
public void preRemove(MyEntity obj) {
System.out.println("Pre-Removing operation: " + obj);
}
@PostRemove
public void postRemove(MyEntity obj) {
System.out.println("Post-Remove operation: " + obj);
}
@PreUpdate
public void preUpdate(MyEntity obj) {
System.out.println("Pre-Updating operation: " + obj);
}
@PostUpdate
public void postUpdate(MyEntity obj) {
System.out.println("Post-Update operation: " + obj);
}
}
Main.java
import javax.persistence.*;
import java.util.concurrent.*;
// В этом классе создаем несколько потоков и смотрим, что будет происходить.
public class Main {
// Создаем фабрику, т.к. создание EntityManagerFactory дело дорогое, обычно делается это один раз.
private static EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("ru.easyjava.data.jpa.hibernate");
public static void main(String[] args) {
// Создаем 10 потоков(можно и больше, но в таком случае будет сложно разобраться).
ExecutorService es = Executors.newFixedThreadPool(10);
try {
// Метод persistFill() нужен для авто-заполнения таблицы.
persistFill();
for(int i=0; i<10; i++){
int finalI = i;
es.execute(() -> {
// Лучше сначала запустить без метода updateEntity(finalI) так, чтоб java создала сущность в базе и заполнила ее. Но так как java - очень умная, она сама запоминает последний сгенерированный id, даже если вы решили полностью очистить таблицу, id новой строки будет таким, как будто вы не чистили базу данных(может возникнуть ситуация, в которой вы запускаете метод persistFill(), а id в бд у вас начинаются с 500).
updateEntity(finalI);
});
}
es.shutdown();
try {
es.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
entityManagerFactory.close();
}
}
// Метод для получения объекта из базы и изменения его.
private static void updateEntity(int index) {
// Создаем EntityManager для того, чтобы можно было вызывать методы, управления жизненным циклом сущности.
EntityManager em = entityManagerFactory.createEntityManager();
MyEntity myEntity = null;
try {
em.getTransaction().begin();
// Получаем объект из базы данных по индексу 1.
myEntity = em.find(MyEntity.class, 1);
// Вызываем этот sout, чтобы определить каким по очереди был "вытянут" объект.
System.out.println("load = "+index);
// Эту строчку мы и будем изменять (а именно LockModeType.*).
em.lock(myEntity, LockModeType.OPTIMISTIC);
// Изменяем поле Value, таким образом, чтобы понимать транзакция из какого потока изменила его.
myEntity.setValue("WoW_" + index);
em.getTransaction().commit();
em.close();
System.out.println("--Greeter updated : " + myEntity +" __--__ "+ index);
}catch(RollbackException ex){
System.out.println("ГРУСТЬ, ПЕЧАЛЬ=" + myEntity);
}
}
public static void persistFill() {
MyEntity myEntity = new MyEntity();
myEntity.setValue("JPA");
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
em.persist(myEntity);
em.getTransaction().commit();
em.close();
}
}
First run with commented-out updateEntity method
Pre-Persistiting operation: MyEntity{id=null, version=0, value='JPA'}
Post-Persist operation: MyEntity{id=531, version=0, value='JPA'}
Все ожидаемо. Меняем id в методе find и идем дальше.
LockModeType.OPTIMISTIC
This is an optimistic lock, well, this is so logical. As I wrote above, the value of the version field is compared; if it is different, an error is thrown. Check it out.
Results:
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 3
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 2
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_2'}
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_3'}
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 9
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_9'}
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 1
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_1'}
Post-Update operation: MyEntity{id=531, version=1, value='WoW_1'}
--Greeter updated : MyEntity{id=531, version=1, value='WoW_1'} __--__ 1
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=0, value='WoW_2'}
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=0, value='WoW_3'}
Loaded operation: MyEntity{id=531, version=1, value='WoW_1'}
load = 4
Pre-Updating operation: MyEntity{id=531, version=1, value='WoW_4'}
Post-Update operation: MyEntity{id=531, version=2, value='WoW_4'}
--Greeter updated : MyEntity{id=531, version=2, value='WoW_4'} __--__ 4
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=0, value='WoW_9'}
Loaded operation: MyEntity{id=531, version=2, value='WoW_4'}
load = 0
Pre-Updating operation: MyEntity{id=531, version=2, value='WoW_0'}
Post-Update operation: MyEntity{id=531, version=3, value='WoW_0'}
--Greeter updated : MyEntity{id=531, version=3, value='WoW_0'} __--__ 0
Loaded operation: MyEntity{id=531, version=3, value='WoW_0'}
load = 6
Pre-Updating operation: MyEntity{id=531, version=3, value='WoW_6'}
Post-Update operation: MyEntity{id=531, version=4, value='WoW_6'}
Loaded operation: MyEntity{id=531, version=4, value='WoW_6'}
load = 5
Pre-Updating operation: MyEntity{id=531, version=4, value='WoW_5'}
Post-Update operation: MyEntity{id=531, version=5, value='WoW_5'}
--Greeter updated : MyEntity{id=531, version=4, value='WoW_6'} __--__ 6
--Greeter updated : MyEntity{id=531, version=5, value='WoW_5'} __--__ 5
Loaded operation: MyEntity{id=531, version=5, value='WoW_5'}
load = 7
Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_7'}
Post-Update operation: MyEntity{id=531, version=6, value='WoW_7'}
Loaded operation: MyEntity{id=531, version=5, value='WoW_5'}
load = 8
Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_8'}
--Greeter updated : MyEntity{id=531, version=6, value='WoW_7'} __--__ 7
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=5, value='WoW_8'}
Observations: As can be seen from the results, threads 3, 2, 9, and 1 were the first to start loading; for them, the Pre-Update callback methods were called. The first stream where the Post-Update method was called was 1, as can be seen from the results, the field marked with the Version annotation has already been changed (increased by 1). Accordingly, all remaining threads 2, 3, 9 threw an exception. And so on. The result of executing value = WoW_7, version = 6. Indeed, the last Post-Update was at stream 7 with version = 6.
LockModeType.OPTIMISTIC_FORCE_INCREMENT
Works according to the same algorithm as LockModeType.OPTIMISTIC with the exception that after commit the value of the Version field is forcibly increased by 1. As a result, finally the field after each commit will increase by 2 (an increase that can be seen in Post-Update + forced increase) . Question. What for? If, after a commit, we still want to “conjure” over the same data, and we do not need third-party transactions that can break between the first commit and the closing of our transaction.
Important!If you try to change the data to the same, then in this case the Pre-Update and Post-Update methods will not be called. All transactions may crash. For example, we simultaneously read data from several transactions, but since the calls to the pre and post (update) methods take time, the transaction that tries to change the data (to the same ones) will be executed immediately. This will lead to an error of the remaining transactions.
LockModeType.PESSIMISTIC_READ, LockModeType.PESSIMISTIC_WRITE and LockModeType.PESSIMISTIC_FORCE_INCREMENT
Since the work of the remaining types of locks looks similar, so I will write about all at once and consider the result only on PESSIMISTIC_READ.
LockModeType.PESSIMISTIC_READ - pessimistic read lock.
LockModeType.PESSIMISTIC_WRITE - pessimistic write lock (and read).
LockModeType.PESSIMISTIC_FORCE_INCREMENT - pessimistic write lock (and read) with a forced increase in the Version field.
As a result of such locks, a long wait for locking may occur, which in turn can lead to an error.
The result for LockModeType.PESSIMISTIC_READ (not fully represented):
load = 0
Pre-Updating operation: MyEntity{id=549, version=5, value='WoW_0'}
Post-Update operation: MyEntity{id=549, version=6, value='WoW_0'}
Loaded operation: MyEntity{id=549, version=6, value='WoW_0'}
load = 8
Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_8'}
Loaded operation: MyEntity{id=549, version=6, value='WoW_0'}
load = 4
Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_4'}
...
ERROR: ОШИБКА: обнаружена взаимоблокировка
Подробности: Процесс 22760 ожидает в режиме ExclusiveLock блокировку "кортеж (0,66) отношения 287733 базы данных 271341"; заблокирован процессом 20876.
Процесс 20876 ожидает в режиме ShareLock блокировку "транзакция 8812"; заблокирован процессом 22760.
As a result, threads 4 and 8 blocked each other, which led to an unsolvable conflict. Prior to this, stream 0 no one interfered with execution. The situation is similar with all threads up to 0.