JPA可以将entity的某个属性(只允许一个)标记为@javax.persistence.Version。这个属性可以是整形和java.sql.Timestamp。
如果是整形,Version从0开始,每次update,每次自动+1;如果是Timestamp,就是每次更新为now()。如果数据没有实际变化,version不会变更。
CREATE TABLE My_Entity(
Id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Version INT UNSIGNED NOT NULL ,
Data VARCHAR(128) NOT NULL
)
@Entity(name = "My_Entity")
public class MyEntity {
private long id;
private int version;
private String data;
//将version属性标记为@Version
@Version
public int getVersion() { ... }
/* 为了避免不正确的version设定,其值全部由JPA自动处理,对setter方法采用protected或者package-private。
* 【package-private】 即没有修饰词,如本例。
* package-private访问权限:it is visible only within its own package.
* 本类(Y)Package(Y)Subclass(N)World(N)
* package-private和protected的区别在于: protected对于Subclass是可以访问的,而package-private不可以。
*/
void setVersion(int version) { ... }
}
当insert数据时,version=0,以后每次修改,version=version+1。如果使用了update,但是data不变,则不会变更version。
如果数据库表格中有LastUpdate之类的列,应该将相应的属性标记为@Version。注意一个Entity中最多只能有一个属性可以标记@Version。
private Timestamp lastUpdate;
@Version
public Timestamp getLastUpdate() {
return lastUpdate;
}
void setLastUpdate(Timestamp lastUpdate) {
this.lastUpdate = lastUpdate;
}
@Transactional
public void versionTest(){
// 1)读信息
MyEntity entity = myEntityRepository.findOne(1L);
if(entity == null)
entity = new MyEntity();
log.info(gson.toJson(entity));
// 2)设置新的信息
String data = "Hello, ".concat(LocalDateTime.now().toString());
entity.setData(data);
myEntityRepository.save(entity);
// 3)再读的信息
entity = myEntityRepository.findOne(1L);
log.info(gson.toJson(entity));
}
相关的log如下:
Hibernate: select myentity0_.id as id1_5_0_, ...
15:04:34.683 [http-nio-8080-exec-6] [INFO ] - {"id":1,"version":0,"data":"Hello, 2018-07-31T15:01:50.441"}
15:04:34.689 [http-nio-8080-exec-6] [INFO ] - {"id":1,"version":0,"data":"Hello, 2018-07-31T15:04:34.683"}
Hibernate: update My_Entity set data=?, version=? where id=? and version=?
我们查询数据库,实际version已经变更为1。
MariaDB [AdvancedMappings]> select * from My_Entity;
+----+---------+--------------------------------+
| Id | Version | Data |
+----+---------+--------------------------------+
| 1 | 1 | Hello, 2018-07-31T15:04:34.683 |
+----+---------+--------------------------------+
仔细看看log,跟踪hibernate的sql操作,发现没有再次去读。无论如何设置isolation的级别,包括READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE,都是如此。这是因为在transaction中,不会重复去读取同一id的数据。在普通的情况下,save(entity)操作中的entity被JPA认为是就是新的内容,不再读取。然而@Version的属性是自动更新的,并没有体现在save(entity)的entity中。因此,不要在save()后,将@Version的属性的值来作为业务逻辑的依据(当然一般也不会出现这种情况,常规的处理是读-》分析-》写-结束)。
@Version可以用于自动设置表格中lastUpdate的列,此外还可以对数据同时操作进行保护。下面是我们的测试代码。
@Transactional(isolation=Isolation.XXX) // org.springframework.transaction.annotation.Transactional
public void versionTest(long id){
MyEntity entity = myEntityRepository.findOne(id);
if(entity == null)
entity = new MyEntity();
"Hello, " + id + " at " + LocalDateTime.now().toString();
entity.setData(data);
log.info("want to set : {}", gson.toJson(entity));
try {
Thread.sleep(4000L);
} catch (InterruptedException e) {
}
myEntityRepository.save(entity);
}
我们通过sleep 4秒,可以让两个transactional同时执行。我们对比没有@Version且不同的isolation设置下,代码执行的情况,以及有@Version的情况。
我们可以在不使用SERIALIZABLE的情况下,得到保护,在处理旧数据时,抛出org.springframework.orm.ObjectOptimisticLockingFailureException的异常。Optimisticlocking允许多个线程读同一个entity,但只允许其中一个线程update该entity。
@Version:无,isolation:READ_UNCOMMIT、READ_COMMIT、REPEATABLE_READ
两个基于同时执行的transaction均运行成功,表格的数据为第二个写的结果。log如下
Hibernate: select myentity0_.id as id1_5_0_, ...
16:15:53.534 [http-nio-8080-exec-52] [INFO ] - {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:04.685"}
16:15:53.535 [http-nio-8080-exec-52] [INFO ] - want to set : {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:53.535"}
Hibernate: select myentity0_.id as id1_5_0_, ...
16:15:55.700 [http-nio-8080-exec-37] [INFO ] - {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:04.685"}
16:15:55.701 [http-nio-8080-exec-37] [INFO ] - want to set : {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:55.701"}
Hibernate: update My_Entity set data=?, version=? where id=?
Hibernate: update My_Entity set data=?, version=? where id=?
@Version:有,isolation:READ_UNCOMMIT、READ_COMMIT、REPEATABLE_READ
第一个线程执行成功。第二个线程因为version版本不正确,执行失败,抛出ObjectOptimisticLockingFailureException异常。表格数据为第一个线程的结果。
Hibernate: select myentity0_.id as id1_5_0_, myentity0_.data as data2_5_0_, ...
16:48:09.282 [http-nio-8080-exec-60] [INFO ] - {"id":1,"version":8,"data":"Hello, 1 at 2018-07-31T16:45:12.641"}
16:48:09.283 [http-nio-8080-exec-60] [INFO ] - want to set : {"id":1,"version":8,"data":"Hello, 1 at 2018-07-31T16:48:09.283"}
Hibernate: select myentity0_.id as id1_5_0_, myentity0_.data as data2_5_0_, ...
16:48:11.214 [http-nio-8080-exec-66] [INFO ] - {"id":1,"version":8,"data":"Hello, 1 at 2018-07-31T16:45:12.641"}
16:48:11.215 [http-nio-8080-exec-66] [INFO ] - want to set : {"id":1,"version":8,"data":"Hello, 1 at 2018-07-31T16:48:11.215"}
Hibernate: update My_Entity set data=?, version=? where id=? and version=?
Hibernate: update My_Entity set data=?, version=? where id=? and version=?
16:48:15.221 [http-nio-8080-exec-66] [ERROR] Hibernate ExceptionMapperStandardImpl - HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]
16:48:15.223 [http-nio-8080-exec-66] [INFO ] Hibernate AbstractBatchImpl - HHH000010: On release of batch it still contained JDBC statements
16:48:15.238 [http-nio-8080-exec-66] [ERROR] MainController:32 home() - Error found : org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:320) ~[spring-orm-4.3.17.RELEASE.jar:4.3.17.RELEASE]
... ...
Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:67) ~[hibernate-core-5.2.13.Final.jar:5.2.13.Final]
... ...
@Version:无/有,isolation:SERIALIZABLE
第一个线程执行成功。第二个线程因为dead lock,执行失败,抛出CannotAcquireLockException异常。表格数据为第一个线程的结果。
Hibernate: select myentity0_.id as id1_5_0_, myentity0_.data as data2_5_0_, ...
16:15:53.534 [http-nio-8080-exec-52] [INFO ] - {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:04.685"}
16:15:53.535 [http-nio-8080-exec-52] [INFO ] - want to set : {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:53.535"}
Hibernate: select myentity0_.id as id1_5_0_, ...
16:15:55.700 [http-nio-8080-exec-37] [INFO ] - {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:04.685"}
16:15:55.701 [http-nio-8080-exec-37] [INFO ] - want to set : {"id":1,"version":7,"data":"Hello, 1 at 2018-07-31T16:15:55.701"}
Hibernate: update My_Entity set data=?, version=? where id=?
Hibernate: update My_Entity set data=?, version=? where id=?
16:15:59.765 [http-nio-8080-exec-37] [WARN ] Hibernate SqlExceptionHelper - SQL Error: 1213, SQLState: 40001
16:15:59.765 [http-nio-8080-exec-37] [ERROR] Hibernate SqlExceptionHelper - Deadlock found when trying to get lock; try restarting transaction
16:15:59.768 [http-nio-8080-exec-37] [INFO ] Hibernate AbstractBatchImpl - HHH000010: On release of batch it still contained JDBC statements
16:15:59.779 [http-nio-8080-exec-37] [ERROR] Hibernate ExceptionMapperStandardImpl - HHH000346: Error during managed flush [org.hibernate.exception.LockAcquisitionException: could not execute statement]
16:15:59.793 [http-nio-8080-exec-37] [ERROR] - Error found : org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement
org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:269) ~[spring-orm-4.3.17.RELEASE.jar:4.3.17.RELEASE]
相关链接:我的Professional Java for Web Applications相关文章