一、javax.persistence.Version
在数据库并发操作时,为了保证数据的正确性,经常要对数据加锁,加锁有两种方式:悲观锁、乐观锁
悲观锁:把所需要的数据全部加锁,不允许其他事务对数据做修改
update xxx where xxxx for update
乐观锁:对数据进行版本校验,如果版本不一致,则操作数据失败
update xxx,version+1 where xxxx and version=x
在jpa中,@Version注解,可以实现乐观锁功能
实体类Account,version属性上加@Version注解
package com.xhx.springboot.entity;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
/**
* @author xuhaixing
* @date 2018/4/28 10:29
*/
@Entity
public class Account {
@Id
private int id;
private String name;
private Double money;
@Version
private int version;
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
}
在更新数据时,需要用jpa自己实现的save方法
S save(S var1);
Iterable saveAll(Iterable var1);
如果是自己写的update方法,下面这样,是不生效的
@Repository
public interface AccountDao extends JpaRepository {
@Modifying
@Query("update Account set name=:name, money=:money where id=:id")
int updateAccount(@Param("id") int id,@Param("name") String name, @Param("money") double money);
}
数据库数据如下:
我们更新id是10的数据,数据库中版本是0,我们设置版本1
Account account = new Account();
account.setId(10);
account.setName("eeee");
account.setMoney(7999.0);
account.setVersion(1);
accountController.update(account);
报如下错误:
org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.xhx.springboot.entity.Account] with identifier [10]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.xhx.springboot.entity.Account#10]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:298)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
把版本号改成0,再更新,数据库中执行如下语句,更新成功
再看数据库,版本号+1了
自己实现可以这样:
@Modifying
@Query("update Account set name=:name, money=:money,version=:version+1 where id=:id and version=:version")
int updateAccountByVersion(@Param("id") int id,@Param("name") String name, @Param("money") double money,@Param("version") int version);
service中加判断,抛异常,这样就自己通过数据库实现了乐观锁
@Transactional(rollbackFor = Exception.class)
public int updateAccountByVersion(Account account){
int i =accountDao.updateAccountByVersion(account.getId(),account.getName(),account.getMoney(),account.getVersion());
if(i==0){
throw new ObjectOptimisticLockingFailureException("更新account失败",new Exception());
}
return i;
}
二、tk.mybatis.mapper.annotation.Version
想要使用乐观锁,只需要在实体中,给乐观锁字段增加 @tk.mybatis.mapper.annotation.Version 注解。
例如:
public class User {
private Long id;
private String name;
//...
@Version
private Integer version;
//setter and getter
}
@Version 注解有一个 nextVersion 属性,默认值为默认的实现,默认实现如下:
package tk.mybatis.mapper.version;
import java.sql.Timestamp;
/**
* @author liuzh
* @since 3.5.0
*/
public class DefaultNextVersion implements NextVersion {
@Override
public Object nextVersion(Object current) throws VersionException {
if (current == null) {
throw new VersionException("当前版本号为空!");
}
if (current instanceof Integer) {
return (Integer) current + 1;
} else if (current instanceof Long) {
return (Long) current + 1L;
} else if (current instanceof Timestamp) {
return new Timestamp(System.currentTimeMillis());
} else {
throw new VersionException("默认的 NextVersion 只支持 Integer, Long" +
" 和 java.sql.Timestamp 类型的版本号,如果有需要请自行扩展!");
}
}
}
默认实现支持 Integer, Long 和 java.sql.Timestamp ,如果默认实现不能满足自己的需要,可以实现自己的方法,在配置注解时指定自己的实现即可。
支持的方法
delete
deleteByPrimaryKey
updateByPrimaryKey
updateByPrimaryKeySelective
updateByExample
updateByExampleSelective
这些方法在执行时会更新乐观锁字段的值或者使用乐观锁的值作为查询条件。
需要注意的地方
在使用乐观锁时,由于通用 Mapper 是内置的实现,不是通过 拦截器 方式实现的,因此当执行上面支持的方法时,如果版本不一致,那么执行结果影响的行数可能就是 0。这种情况下也不会报错!
所以在 Java6,7中使用时,你需要自己在调用方法后进行判断是否执行成功。
在 Java8+ 中,可以通过默认方法来增加能够自动报错(抛异常)的方法,例如:
public interface MyMapper<T> extends Mapper<T> {
default int deleteWithVersion(T t){
int result = delete(t);
if(result == 0){
throw new RuntimeException("删除失败!");
}
return result;
}
default int updateByPrimaryKeyWithVersion(Object t){
int result = updateByPrimaryKey(t);
if(result == 0){
throw new RuntimeException("更新失败!");
}
return result;
}
//...
}