Java for Web学习笔记(一三十)映射(6)@Version

@Version

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相关文章

你可能感兴趣的:(读书笔记,JAVA,愷风(Wei)之Java,for,Web学习笔记)