📜 ⬆️ ⬇️

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?


  1. Lost updates may occur in situations where two transactions running in parallel, trying to update the same data.
  2. 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.
  3. 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.
  4. Phantom reading is a problem similar to non-repeatable reads, except that a different number of rows is returned.

Briefly about their decisions


  1. READ UNCOMMITED - solved using the Version annotation in JPA (this is what the article is about)
  2. READ COMMITED - allows you to read only committed changes
  3. 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.
  4. 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.

In optimistic locks, when committing 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 using pessimistic locks, no contradictions are guaranteed when executing a transaction, by placing others in the standby mode (but this takes time), 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 ):

  1. NONE - no lock
  2. OPTIMISTIC
  3. OPTIMISTIC_FORCE_INCREMENT
  4. PESSIMISTIC_READ
  5. PESSIMISTIC_WRITE
  6. 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 thread 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.

Source: https://habr.com/ru/post/434836/


All Articles