Mengunci pesimis dalam JPA

1. Gambaran keseluruhan

Terdapat banyak situasi ketika kita ingin mengambil data dari pangkalan data. Kadang-kadang kita mahu menguncinya untuk proses selanjutnya sehingga tidak ada orang lain yang dapat mengganggu tindakan kita.

Kita dapat memikirkan dua mekanisme kawalan serentak yang memungkinkan kita melakukannya: menetapkan tahap pengasingan transaksi yang betul atau menetapkan kunci pada data yang kita perlukan pada masa ini.

Pengasingan transaksi ditentukan untuk sambungan pangkalan data. Kami dapat mengkonfigurasinya untuk mengekalkan tahap penguncian data yang berbeza.

Walau bagaimanapun, tahap pengasingan ditetapkan setelah sambungan dibuat dan mempengaruhi setiap pernyataan dalam hubungan tersebut. Nasib baik, kita dapat menggunakan penguncian pesimis yang menggunakan mekanisme pangkalan data untuk menempah akses eksklusif yang lebih terperinci ke data.

Kami dapat menggunakan kunci pesimis untuk memastikan bahawa tidak ada transaksi lain yang dapat mengubah atau menghapus data yang disimpan.

Terdapat dua jenis kunci yang dapat kita simpan: kunci eksklusif dan kunci bersama. Kami dapat membaca tetapi tidak menulis dalam data apabila orang lain memegang kunci bersama. Untuk mengubah atau menghapus data yang dikhaskan, kita perlu mempunyai kunci eksklusif.

Kami dapat memperoleh kunci eksklusif menggunakan pernyataan ' SELECT ... FOR UPDATE '.

2. Mod Kunci

Spesifikasi JPA menentukan tiga mod kunci pesimis yang akan kita bincangkan:

  • PESSIMISTIC_READ - membolehkan kami mendapatkan kunci bersama dan mengelakkan data daripada dikemas kini atau dihapuskan
  • PESSIMISTIC_WRITE - membolehkan kita mendapatkan kunci eksklusif dan mengelakkan data dibaca, dikemas kini atau dihapuskan
  • PESSIMISTIC_FORCE_INCREMENT - berfungsi seperti PESSIMISTIC_WRITE dan ia juga menambah atribut versi entiti versi

Kesemuanya adalah ahli statik dari kelas LockModeType dan membenarkan transaksi mendapatkan kunci pangkalan data. Semuanya disimpan sehingga transaksi dilakukan atau dipulangkan.

Perlu diperhatikan bahawa kita hanya dapat memperoleh satu kunci pada satu masa. Sekiranya mustahil, PersistenceException dilemparkan.

2.1. PESSIMISTIC_READ

Bila-bila masa kita hanya mahu membaca data dan tidak mengalami pembacaan kotor, kita dapat menggunakan PESSIMISTIC_READ (kunci bersama). Kami tidak akan dapat membuat kemas kini atau penghapusan sekalipun.

Kadang kala pangkalan data yang kita gunakan tidak menyokong kunci PESSIMISTIC_READ , jadi mungkin kita memperoleh kunci PESSIMISTIC_WRITE sebagai gantinya.

2.2. PESSIMISTIC_WRITE

Segala urus niaga yang perlu mendapatkan kunci data dan membuat perubahan harus memperoleh kunci PESSIMISTIC_WRITE . Menurut spesifikasi JPA , menahan kunci PESSIMISTIC_WRITE akan menghalang transaksi lain membaca, mengemas kini atau menghapus data.

Harap maklum bahawa beberapa sistem pangkalan data menerapkan kawalan serentak multi-versi yang membolehkan pembaca mengambil data yang telah disekat.

2.3. PESSIMISTIC_FORCE_INCREMENT

Kunci ini berfungsi sama dengan PESSIMISTIC_WRITE , tetapi diperkenalkan untuk bekerjasama dengan entiti versi - entiti yang mempunyai atribut yang diberi penjelasan dengan @Version .

Sebarang kemas kini entiti versi dapat didahului dengan mendapatkan kunci PESSIMISTIC_FORCE_INCREMENT . Memperoleh kunci itu menghasilkan kemas kini lajur versi.

Terserah kepada penyedia kegigihan untuk menentukan sama ada ia menyokong PESSIMISTIC_FORCE_INCREMENT untuk entiti yang tidak berubah atau tidak. Sekiranya tidak, ia akan membuang PersistanceException .

2.4. Pengecualian

Adalah baik untuk mengetahui pengecualian mana yang mungkin berlaku semasa bekerja dengan kunci pesimis. Spesifikasi JPA memberikan pelbagai jenis pengecualian:

  • PessimisticLockException - menunjukkan bahawa mendapatkan kunci atau menukar kunci yang dikongsi menjadi kunci eksklusif gagal dan mengakibatkan pengembalian peringkat transaksi
  • LockTimeoutException - menunjukkan bahawa mendapatkan kunci atau menukar kunci bersama kepada waktu tutup eksklusif dan menghasilkan pengembalian peringkat pernyataan
  • PersistanceException - menunjukkan bahawa masalah kegigihan berlaku. PersistanceException dan subtipenya , kecuali NoResultException , NonUniqueResultException, LockTimeoutException , dan QueryTimeoutException, menandakan transaksi aktif yang akan dikembalikan.

3. Menggunakan Kunci Pesimis

Terdapat beberapa cara yang mungkin untuk mengkonfigurasi kunci pesimis pada satu rekod atau sekumpulan rekod. Mari lihat bagaimana melakukannya di JPA.

3.1. Cari

Ini mungkin kaedah yang paling mudah. Cukup untuk meneruskan objek LockModeType sebagai parameter ke kaedah cari :

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

3.2. Pertanyaan

Selain itu, kita dapat menggunakan objek Query juga dan memanggil setLockMode setter dengan mode kunci sebagai parameter:

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

3.3. Penguncian Eksplisit

Anda juga dapat mengunci hasil yang diperoleh secara manual dengan kaedah cari:

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

3.4. Segarkan

Sekiranya kita ingin menimpa keadaan entiti dengan kaedah penyegaran , kita juga boleh menetapkan kunci:

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

3.5. DinamakanQuery

Anotasi @NamedQuery membolehkan kami menetapkan mod kunci juga:

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

4. Skop Kunci

Parameter kunci skop menentukan bagaimana menangani hubungan penguncian entiti terkunci. Ada kemungkinan untuk mendapatkan kunci hanya pada satu entiti yang ditentukan dalam pertanyaan atau juga menyekat hubungannya.

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.

Sebaliknya, kita harus ingat bahawa memahami kunci pangkalan data sama pentingnya dengan memahami mekanisme sistem pangkalan data yang mendasari. Penting juga untuk diingat bahawa tingkah laku kunci pesimis bergantung pada penyedia ketekunan yang kita bekerjasama.

Terakhir, kod sumber tutorial ini terdapat di GitHub untuk hibernate dan untuk EclipseLink.