1. 概述
JPA 的事务机制是确保原子性和数据完整性的强大工具,它要么提交所有更改,要么在异常发生时回滚所有更改。然而,在某些场景下,我们需要在遇到异常后继续执行事务,而不回滚数据更改。
本文将深入探讨出现这种情况的各种用例,并提供相应的解决方案。
2. 问题定位
事务中发生异常主要有两种情况,我们先来理解它们。
2.1. 服务层异常导致事务回滚
第一个可能遇到回滚的地方是服务层,外部异常可能影响数据库操作。
通过以下示例详细分析这个场景。首先定义 InvoiceEntity
作为数据模型:
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "serialNumber")})
public class InvoiceEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String serialNumber;
private String description;
//Getters and Setters
}
这里包含:
- 自动生成的内部 ID
- 需要全局唯一的序列号
- 描述信息
现在创建 InvoiceService
负责发票的事务操作:
@Service
public class InvoiceService {
@Autowired
private InvoiceRepository repository;
@Transactional
public void saveInvoice(InvoiceEntity invoice) {
repository.save(invoice);
sendNotification();
}
private void sendNotification() {
throw new NotificationSendingException("Notification sending is failed");
}
}
在 saveInvoice()
方法中,我们添加了事务性保存发票并发送通知的逻辑。不幸的是,通知发送过程中会抛出异常:
public class NotificationSendingException extends RuntimeException {
public NotificationSendingException(String text) {
super(text);
}
}
这是一个简单的 RuntimeException
。观察此场景下的行为:
@Autowired
private InvoiceService service;
@Test
void givenInvoiceService_whenExceptionOccursDuringNotificationSending_thenNoDataShouldBeSaved() {
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.setSerialNumber("#1");
invoiceEntity.setDescription("First invoice");
assertThrows(
NotificationSendingException.class,
() -> service.saveInvoice(invoiceEntity)
);
List<InvoiceEntity> entityList = repository.findAll();
Assertions.assertTrue(entityList.isEmpty());
}
调用 saveInvoice()
方法时遇到 NotificationSendingException
,所有数据库更改被回滚,符合预期。
2.2. 持久层异常导致事务回滚
另一个可能发生隐式回滚的情况在持久层。
我们可能认为捕获数据库异常后就能在同一事务中继续数据操作,但事实并非如此。在 InvoiceRepository
中创建 saveBatch()
方法复现问题:
@Repository
public class InvoiceRepository {
private final Logger logger = LoggerFactory.getLogger(
com.baeldung.continuetransactionafterexception.InvoiceRepository.class);
@PersistenceContext
private EntityManager entityManager;
@Transactional
public void saveBatch(List<InvoiceEntity> invoiceEntities) {
invoiceEntities.forEach(i -> entityManager.persist(i));
try {
entityManager.flush();
} catch (Exception e) {
logger.error("Exception occured during batch saving, save individually", e);
invoiceEntities.forEach(i -> {
try {
save(i);
} catch (Exception ex) {
logger.error("Problem saving individual entity {}", i.getSerialNumber(), ex);
}
});
}
}
}
在 saveBatch()
方法中,我们尝试通过单次 flush 操作保存对象列表。如果发生异常,则捕获并逐个保存对象。实现 save()
方法:
@Transactional
public void save(InvoiceEntity invoiceEntity) {
if (invoiceEntity.getId() == null) {
entityManager.persist(invoiceEntity);
} else {
entityManager.merge(invoiceEntity);
}
entityManager.flush();
logger.info("Entity is saved: {}", invoiceEntity.getSerialNumber());
}
通过捕获并记录异常避免触发事务回滚。调用该方法观察行为:
@Test
void givenInvoiceRepository_whenExceptionOccursDuringBatchSavingInternally_thenNoDataShouldBeSaved() {
List<InvoiceEntity> testEntities = new ArrayList<>();
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.setSerialNumber("#1");
invoiceEntity.setDescription("First invoice");
testEntities.add(invoiceEntity);
InvoiceEntity invoiceEntity2 = new InvoiceEntity();
invoiceEntity2.setSerialNumber("#1");
invoiceEntity.setDescription("First invoice (duplicated)");
testEntities.add(invoiceEntity2);
InvoiceEntity invoiceEntity3 = new InvoiceEntity();
invoiceEntity3.setSerialNumber("#2");
invoiceEntity.setDescription("Second invoice");
testEntities.add(invoiceEntity3);
UnexpectedRollbackException exception = assertThrows(UnexpectedRollbackException.class,
() -> repository.saveBatch(testEntities));
assertEquals("Transaction silently rolled back because it has been marked as rollback-only",
exception.getMessage());
List<InvoiceEntity> entityList = repository.findAll();
Assertions.assertTrue(entityList.isEmpty());
}
准备包含重复序列号的发票列表。尝试保存时遇到 UnexpectedRollbackException
,数据库未保存任何记录。 这是因为首次异常后事务被标记为仅回滚状态,阻止后续提交。
3. 使用 @Transactional 注解的 noRollbackFor 属性
对于 JPA 调用外发生的异常,可使用 @Transactional
注解的 noRollbackFor
属性保留数据库更改。
修改 InvoiceService
中的 saveInvoiceWithoutRollback()
方法:
@Transactional(noRollbackFor = NotificationSendingException.class)
public void saveInvoiceWithoutRollback(InvoiceEntity entity) {
repository.save(entity);
sendNotification();
}
调用该方法观察行为变化:
@Test
void givenInvoiceService_whenNotificationSendingExceptionOccurs_thenTheInvoiceBeSaved() {
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.setSerialNumber("#1");
invoiceEntity.setDescription("We want to save this invoice anyway");
assertThrows(
NotificationSendingException.class,
() -> service.saveInvoiceWithoutRollback(invoiceEntity)
);
List<InvoiceEntity> entityList = repository.findAll();
Assertions.assertTrue(entityList.contains(invoiceEntity));
}
如预期抛出 NotificationSendingException
,但发票成功保存到数据库。
4. 手动控制事务
持久层回滚时,可手动控制事务确保异常发生时数据仍被保存。
向 InvoiceRepository
注入 EntityManagerFactory
并创建 EntityManager
生成方法:
@Autowired
private EntityManagerFactory entityManagerFactory;
private EntityManager em() {
return entityManagerFactory.createEntityManager();
}
不使用共享 EntityManager
(无法手动操作事务)。实现 saveBatchUsingManualTransaction()
方法:
public void saveBatchUsingManualTransaction(List<InvoiceEntity> testEntities) {
EntityTransaction transaction = null;
try (EntityManager em = em()) {
transaction = em.getTransaction();
transaction.begin();
testEntities.forEach(em::persist);
try {
em.flush();
} catch (Exception e) {
logger.error("Duplicates detected, save individually", e);
transaction.rollback();
testEntities.forEach(t -> {
EntityTransaction newTransaction = em.getTransaction();
try {
newTransaction.begin();
saveUsingManualTransaction(t, em);
} catch (Exception ex) {
logger.error("Problem saving individual entity <{}>", t.getSerialNumber(), ex);
newTransaction.rollback();
} finally {
commitTransactionIfNeeded(newTransaction);
}
});
}
} finally {
commitTransactionIfNeeded(transaction);
}
}
流程:
- 开始事务并持久化所有项目
- 尝试提交事务
- 异常时回滚当前事务,使用独立事务逐个保存项目
saveUsingManualTransaction()
实现:
private void saveUsingManualTransaction(InvoiceEntity invoiceEntity, EntityManager em) {
if (invoiceEntity.getId() == null) {
em.persist(invoiceEntity);
} else {
em.merge(invoiceEntity);
}
em.flush();
logger.info("Entity is saved: {}", invoiceEntity.getSerialNumber());
}
commitTransactionIfNeeded()
实现提交逻辑:
private void commitTransactionIfNeeded(EntityTransaction newTransaction) {
if (newTransaction != null && newTransaction.isActive()) {
if (!newTransaction.getRollbackOnly()) {
newTransaction.commit();
}
}
}
测试新方法处理异常的能力:
@Test
void givenInvoiceRepository_whenExceptionOccursDuringBatchSavingInternally_thenDataShouldBeSavedInSeparateTransaction() {
List<InvoiceEntity> testEntities = new ArrayList<>();
InvoiceEntity invoiceEntity1 = new InvoiceEntity();
invoiceEntity1.setSerialNumber("#1");
invoiceEntity1.setDescription("First invoice");
testEntities.add(invoiceEntity1);
InvoiceEntity invoiceEntity2 = new InvoiceEntity();
invoiceEntity2.setSerialNumber("#1");
invoiceEntity1.setDescription("First invoice (duplicated)");
testEntities.add(invoiceEntity2);
InvoiceEntity invoiceEntity3 = new InvoiceEntity();
invoiceEntity3.setSerialNumber("#2");
invoiceEntity1.setDescription("Second invoice");
testEntities.add(invoiceEntity3);
repository.saveBatchUsingManualTransaction(testEntities);
List<InvoiceEntity> entityList = repository.findAll();
Assertions.assertTrue(entityList.contains(invoiceEntity1));
Assertions.assertTrue(entityList.contains(invoiceEntity3));
}
调用包含重复发票的批处理方法。现在可以看到三张发票中有两张成功保存。
5. 拆分事务
使用 @Transactional
注解方法可实现与前节相同的行为。 唯一限制是不能在同一 Bean 内调用所有方法(需在客户端代码中调用)。在 InvoiceRepository
中创建两个 @Transactional
方法:
@Transactional
public void saveBatchOnly(List<InvoiceEntity> testEntities) {
testEntities.forEach(entityManager::persist);
entityManager.flush();
}
仅实现批处理保存逻辑,复用前节的 save()
方法。使用方式:
@Test
void givenInvoiceRepository_whenExceptionOccursDuringBatchSaving_thenDataShouldBeSavedUsingSaveMethod() {
List<InvoiceEntity> testEntities = new ArrayList<>();
InvoiceEntity invoiceEntity1 = new InvoiceEntity();
invoiceEntity1.setSerialNumber("#1");
invoiceEntity1.setDescription("First invoice");
testEntities.add(invoiceEntity1);
InvoiceEntity invoiceEntity2 = new InvoiceEntity();
invoiceEntity2.setSerialNumber("#1");
invoiceEntity1.setDescription("First invoice (duplicated)");
testEntities.add(invoiceEntity2);
InvoiceEntity invoiceEntity3 = new InvoiceEntity();
invoiceEntity3.setSerialNumber("#2");
invoiceEntity1.setDescription("Second invoice");
testEntities.add(invoiceEntity3);
try {
repository.saveBatchOnly(testEntities);
} catch (Exception e) {
testEntities.forEach(t -> {
try {
repository.save(t);
} catch (Exception e2) {
System.err.println(e2.getMessage());
}
});
}
List<InvoiceEntity> entityList = repository.findAll();
Assertions.assertTrue(entityList.contains(invoiceEntity1));
Assertions.assertTrue(entityList.contains(invoiceEntity3));
}
使用 saveBatchOnly()
保存包含重复项的列表。异常发生时,在循环中使用 save()
方法逐个保存可行项目。 最终所有预期项目均被保存。
6. 总结
事务是执行原子操作的强大机制,回滚是失败事务的预期行为。但在某些场景下,我们需要在失败后继续工作并确保数据保存。本文探讨了多种实现方案,可根据具体场景选择最适合的方式。
完整源代码可在 GitHub 获取。