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);
    }
}

流程:

  1. 开始事务并持久化所有项目
  2. 尝试提交事务
  3. 异常时回滚当前事务,使用独立事务逐个保存项目

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 获取。


原始标题:Continue With Transaction After Exception in JPA | Baeldung