Preface {#preface}
前两天在运行代码插入数据的时候总是报索引重复,数据死活插入不了。
情景再现 {#情景再现}
环境:Springboot,JPA。
数据库中有一张表,姑且称为table_a
,结构如下:
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Entity
@Table(name = TableA.TABLE_NAME, indexes = {
@Index(name = "idx_spuId", columnList = "spuId", unique = true)
})
public class TableA {
public static final String TABLE_NAME = "table_a";
/**
* 主键id
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = TABLE_NAME + "_generator")
private Integer id;
/**
* 商品spu_id
*/
@NotNull
@ColumnDefault("0")
private Long spuId = 0L;
/**
* 商品名称
*/
private String goodsName;
/**
* 商品信息更新次数
*/
@NotNull
@ColumnDefault("0")
private Integer updateNum = 0;
}
对应dao
内容如下:
public interface ITableADao extends JpaRepository<TableA, Integer> {
/**
* 根据 spuId 查询
* @param spuId spuId
* @return -
*/
TableA findFirstBySpuId(@NotNull Long spuId(@NotNull Long spuId);
}
有两个 Service 实现,分别称为ServiceAImpl
和ServiceBImpl
吧。
ServiceAImpl
中的add
方法首先判断要插入的商品是否已在数据库中了,存在则更新数据,不存在则插入数据。
@Service
@RequiredArgsConstructor
public class ServiceAImpl implements ServiceA {
private final ITableADao tableADao;
/**
* 插入商品信息
*
* @param form form
* @return -
*/
@Override
@Transactional(propagation= Propagation.REQUIRES_NEW)
public TableA add(TableA form) {
TableA tableA = tableADao.findFirstBySpuId(form.getSpuId());
if (tableA == null) {
// 插入数据
tableA = new TableA();
}
// 更新数据
// 模拟调用第三方接口查询数据
TableA result = Api.searchBySpuId(spuId);
BeanUtils.copyProperties(result, tableA);
tableADao.save(tableA);
return tableA;
}
}
在ServiceBImpl
中调用ServiceAImpl
的add
方法。
@Service
@RequiredArgsConstructor
public class ServiceBImpl implements IServiceB{
private final IServiceA serviceA;
private final ITableADao tableADao;
/**
* 处理商品信息
*
* @param spuId spuId
* @return -
*/
@Override
@Transactional
public boolean handleGoods(Long spuId) {
// 前面的一些代码逻辑,省略...
deal(spuId);
// 后面的一些代码逻辑,省略...
return true;
}
/**
* 处理商品信息
*
* @param spuId spuId
*/
private void deal(Long spuId) {
TableA tableA = serviceA.add(spuId);
// 设置商品的更新次数
tableA.setUpdateNum(tableA.getUpdateNum());
tableADao.save(tableA);
}
}
发生的异常情况是在调用handleGoods
方法传入数据库中不存在的spuId
时,在save
时会报错,提示重复的spuId
,无法插入数据。
// 设置商品的更新次数
tableA.setUpdateNum(tableA.getUpdateNum());
tableADao.save(tableA);
这就很奇怪了,明明插入的spuId
是数据库中不存在的,为什么会报spuId
重复呢?
经过一番简单的思考,发现是add
方法上的
@Transactional(propagation= Propagation.REQUIRES_NEW)
在作祟。add
方法中开启了一个新的事务,在插入新的数据时,就会导致在别的事务中查不到新插入的数据,因为此时数据还没有真正插入到数据库中。所以
// 设置商品的更新次数
tableA.setUpdateNum(tableA.getUpdateNum());
tableADao.save(tableA);
save
方法由于事务不同,查不到刚刚在ServiceAImpl
的add
方法添加的商品信息,也会执行insert
语句。最终在提交事务时add
方法中的save
和deal
方法中的save
都会执行insert
语句,就导致报spuId
重复的错误。
解决方法就是将add
方法上的
@Transactional(propagation= Propagation.REQUIRES_NEW)
改为
@Transactional(propagation= Propagation.REQUIRED)
这也是 Spring 默认的事务传播行为,表示没有就创建事务,有就加入事务。
至于之前为什么这么写,那是之前设计的问题了😂。
Spring 事务传播行为 {#spring-事务传播行为}
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。 例如:methodA 事务方法调用 methodB 事务方法时,methodB 是继续在调用者 methodA 的事务中运行呢,还是为自己开启一个新事务运行,这就是由 methodB 的事务传播行为决定的。
Spring 目前共有 7 种事务传播行为。
| 事务传播行为类型 | 说明 | |---------------------------|------------------------------------------------------------| | PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 | | PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | | PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 | | PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 | | PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | | PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | | PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |