Песимистично заключване в JPA

1. Общ преглед

Има много ситуации, когато искаме да извлечем данни от база данни. Понякога искаме да го заключим за по-нататъшна обработка, така че никой друг да не може да прекъсне действията ни.

Можем да измислим два механизма за контрол на паралелността, които ни позволяват да направим това: задаване на правилното ниво на изолация на транзакциите или задаване на заключване на данните, от които се нуждаем в момента.

Изолирането на транзакциите е дефинирано за връзки с база данни. Можем да го конфигурираме да запазва различната степен на заключване на данните.

Въпреки това, нивото на изолация е настроен след като бъде създадена връзка и това се отразява на всеки отчет в рамките на тази връзка. За щастие можем да използваме песимистично заключване, което използва механизми на базата данни за резервиране на по-подробен ексклузивен достъп до данните.

Можем да използваме песимистично заключване, за да гарантираме, че никоя друга транзакция не може да модифицира или изтрие резервирани данни.

Има два вида брави, които можем да запазим: изключителна брава и споделена ключалка. Можехме да четем, но не и да записваме данни, когато някой друг държи споделено заключване. За да модифицираме или изтрием запазените данни, трябва да имаме изключително заключване.

Можем да придобием ексклузивни брави, като използваме операторите „ SELECT… FOR UPDATE “.

2. Режими на заключване

Спецификацията на JPA определя три песимистични режима на заключване, които ще обсъдим:

  • PESSIMISTIC_READ - позволява ни да получим споделено заключване и да предотвратим актуализирането или изтриването на данните
  • PESSIMISTIC_WRITE - позволява ни да получим изключително заключване и да предотвратим четенето, актуализирането или изтриването на данните
  • PESSIMISTIC_FORCE_INCREMENT - работи като PESSIMISTIC_WRITE и допълнително увеличава атрибута на версия на версиран обект

Всички те са статични членове на класа LockModeType и позволяват транзакции за получаване на заключване на база данни. Всички те се запазват, докато транзакцията се ангажира или върне обратно.

Струва си да се отбележи, че можем да получим само по една ключалка наведнъж. Ако е невъзможно, се хвърля PersistenceException .

2.1. PESSIMISTIC_READ

Винаги, когато искаме само да четем данни и да не срещаме мръсни четения, бихме могли да използваме PESSIMISTIC_READ (споделено заключване). Въпреки това няма да можем да правим никакви актуализации или изтривания.

Понякога се случва, че използваната от нас база данни не поддържа заключване PESSIMISTIC_READ , така че е възможно вместо това да получим заключване PESSIMISTIC_WRITE .

2.2. PESSIMISTIC_WRITE

Всяка транзакция, която трябва да получи заключване на данни и да направи промени в тях, трябва да получи заключване PESSIMISTIC_WRITE . Според спецификацията JPA , задържането на заключване PESSIMISTIC_WRITE ще попречи на други транзакции да четат, актуализират или изтриват данните.

Моля, обърнете внимание, че някои системи за бази данни изпълняват многоверсионен контрол на паралелността, който позволява на читателите да извличат вече блокирани данни.

2.3. PESSIMISTIC_FORCE_INCREMENT

Това заключване работи подобно на PESSIMISTIC_WRITE , но е въведено за сътрудничество с версирани обекти - обекти, които имат атрибут, анотиран с @Version .

Всякакви актуализации на версионирани обекти могат да бъдат предшествани с получаване на заключване PESSIMISTIC_FORCE_INCREMENT . Придобиването на това заключване води до актуализиране на колоната на версията.

Доставчикът на постоянство трябва да определи дали поддържа PESSIMISTIC_FORCE_INCREMENT за неверсирани обекти или не. Ако не стане, изхвърля PersistanceException .

2.4. Изключения

Добре е да знаете кое изключение може да възникне по време на работа с песимистично заключване. Спецификацията на JPA предоставя различни видове изключения:

  • PessimisticLockException - показва, че получаването на заключване или преобразуването на споделена в изключителна ключалка не успее и води до откат на ниво транзакция
  • LockTimeoutException - показва, че получаването на заключване или преобразуването на споделена ключалка в ексклузивно изчакване и води до откат на ниво отчет
  • PersistanceException - показва, че е възникнал проблем с постоянството. PersistanceException и неговите подтипове, с изключение на NoResultException , NonUniqueResultException, LockTimeoutException и QueryTimeoutException, маркира активната транзакция, която трябва да бъде върната обратно.

3. Използване на песимистични ключалки

Има няколко възможни начина за конфигуриране на песимистично заключване на един запис или група записи. Нека да видим как да го направим в JPA.

3.1. намирам

Това е може би най-лесният начин. Достатъчно е да предадете обект LockModeType като параметър на метода за търсене :

entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);

3.2. Запитване

Освен това можем да използваме и обект Query и да извикаме setLockMode setter с режим на заключване като параметър:

Query query = entityManager.createQuery("from Student where studentId = :studentId"); query.setParameter("studentId", studentId); query.setLockMode(LockModeType.PESSIMISTIC_WRITE); query.getResultList()

3.3. Изрично заключване

Възможно е също така да заключите ръчно резултатите, извлечени чрез метода за търсене:

Student resultStudent = entityManager.find(Student.class, studentId); entityManager.lock(resultStudent, LockModeType.PESSIMISTIC_WRITE);

3.4. Обнови

Ако искаме да презапишем състоянието на обекта чрез метода за опресняване , можем също да зададем заключване:

Student resultStudent = entityManager.find(Student.class, studentId); entityManager.refresh(resultStudent, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

3.5. NamedQuery

Анотацията @NamedQuery ни позволява да зададем и режим на заключване:

@NamedQuery(name="lockStudent", query="SELECT s FROM Student s WHERE s.id LIKE :studentId", lockMode = PESSIMISTIC_READ)

4. Обхват на заключване

Параметърът за обхват на заключване определя как да се справя със заключващите взаимоотношения на заключения обект. Възможно е да се получи заключване само на един обект, дефиниран в заявка, или допълнително да се блокират неговите връзки.

To configure the scope we can use PessimisticLockScope enum. It contains two values: NORMAL and EXTENDED.

We can set the scope by passing a parameter ‘javax.persistance.lock.scope‘ with PessimisticLockScope value as an argument to the proper method of EntityManager, Query, TypedQuery or NamedQuery:

Map properties = new HashMap(); map.put("javax.persistence.lock.scope", PessimisticLockScope.EXTENDED); entityManager.find( Student.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties); 

4.1. PessimisticLockScope.NORMAL

We should know that the PessimisticLockScope.NORMAL is the default scope. With this locking scope, we lock the entity itself. When used with joined inheritance it also locks the ancestors.

Let's look at the sample code with two entities:

@Entity @Inheritance(strategy = InheritanceType.JOINED) public class Person { @Id private Long id; private String name; private String lastName; // getters and setters } @Entity public class Employee extends Person { private BigDecimal salary; // getters and setters }

When we want to obtain a lock on the Employee, we can observe the SQL query which spans over those two entities:

SELECT t0.ID, t0.DTYPE, t0.LASTNAME, t0.NAME, t1.ID, t1.SALARY FROM PERSON t0, EMPLOYEE t1 WHERE ((t0.ID = ?) AND ((t1.ID = t0.ID) AND (t0.DTYPE = ?))) FOR UPDATE

4.2. PessimisticLockScope.EXTENDED

The EXTENDED scope covers the same functionality as NORMAL. In addition, it's able to block related entities in a join table.

Simply put, it works with entities annotated with @ElementCollection or @OneToOne, @OneToMany etc. with @JoinTable.

Let's look at the sample code with the @ElementCollection annotation:

@Entity public class Customer { @Id private Long customerId; private String name; private String lastName; @ElementCollection @CollectionTable(name = "customer_address") private List addressList; // getters and setters } @Embeddable public class Address { private String country; private String city; // getters and setters }

Let's analyze some queries when searching for the Customer entity:

SELECT CUSTOMERID, LASTNAME, NAME FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE SELECT CITY, COUNTRY, Customer_CUSTOMERID FROM customer_address WHERE (Customer_CUSTOMERID = ?) FOR UPDATE

We can see that there are two ‘FOR UPDATE‘ queries which lock a row in the customer table as well as a row in the join table.

Another interesting fact we should be aware of is that not all persistence providers support lock scopes.

5. Setting Lock Timeout

Besides setting lock scopes, we can adjust another lock parameter – timeout. The timeout value is the number of milliseconds that we want to wait for obtaining a lock until the LockTimeoutException occurs.

We can change the value of timeout similarly to lock scopes, by using property ‘javax.persistence.lock.timeout' with the proper number of milliseconds.

It's also possible to specify ‘no wait' locking by changing timeout value to zero. However, we should keep in mind that there are database drivers which don't support setting a timeout value this way.

Map properties = new HashMap(); map.put("javax.persistence.lock.timeout", 1000L); entityManager.find( Student.class, 1L, LockModeType.PESSIMISTIC_READ, properties);

6. Conclusion

When setting the proper isolation level is not enough to cope with concurrent transactions, JPA gives us pessimistic locking. It enables us to isolate and orchestrate different transactions so they don't access the same resource at the same time.

To achieve that we can choose between discussed types of locks and consequently modify such parameters as their scopes or timeouts.

От друга страна, трябва да помним, че разбирането на заключванията на бази данни е също толкова важно, колкото и разбирането на механизмите на основните системи от бази данни. Също така е важно да се има предвид, че поведението на песимистичните брави зависи от доставчика на упоритост, с който работим.

И накрая, изходният код на този урок е достъпен в GitHub за хибернация и за EclipseLink.