Spring Data JPA 出现异常后不回滚,继续处理事务

1、概览

JPA 中的事务机制是一个功能强大的工具,它通过提交所有更改或在出现异常时回滚更改来确保原子性和数据完整性。然而,在某些情况下,我们可能需要在遇到异常的情况下继续进行事务处理而不回滚数据更改。

2、出现异常后事务自动回滚

在事务中可能会出现两种主要的异常情况。

2.1、在 Service 层出现异常后回滚事务

我们可能遇到回滚(Rollback)的第一个地方是在 Service 层,外部异常可能会影响数据库更改。

让我们通过下面的示例更仔细地了解一下这种情况。首先,添加 InvoiceEntity,作为数据模型:

@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "serialNumber")})
public class InvoiceEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;     // 自增 ID
    private String serialNumber;    // 序列号
    private String description; // 说明
    // Getter / Setter
}

如上,包含了一个自增 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() 方法中,我们添加了事务性保存发票(invoice)和发送相关通知的逻辑。但是,在发送通知的过程中,会抛出异常:

public class NotificationSendingException extends RuntimeException {
    public NotificationSendingException(String text) {
        super(text);
    }
}

sendNotification 中没有具体实现任何功能,只抛出了一个 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());
}

调用 Service 中的 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() 方法中,尝试使用一次刷新操作来保存对象列表。如果在此操作过程中出现任何异常,则捕获异常并继续单独保存每个对象。

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

我们准备了一个 List<InvoiceEntity>,其中有两个对象违反了序列号字段的唯一性约束。在尝试保存发票列表时,遇到了 UnexpectedRollbackException 异常,数据库中没有保存任何项目。出现这种情况的原因是,在第一次异常发生后,事务被标记为 只能回滚,因此 无法在其中进行任何进一步的提交

3、使用 @Transactional 注解的 noRollbackFor 属性

对于在 JPA 调用之外发生异常的情况,可以使用 @Transactional 注解的 noRollbackFor 属性,以便在同一事务中发生预期异常时保留数据库更改。

修改 InvoiceService 类中的 saveInvoiceWithoutRollback() 方法:

// 通过 noRollbackFor 属性指定不会回滚的异常
@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 异常。不过,发票(invoice)已成功保存在数据库中。

4、手动控制事务

在持久层遇到回滚的情况下,可以手动控制事务,确保即使出现异常也能保存数据。

EntityManagerFactory 注入 InvoiceRepository,并定义一个方法来创建 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());
}

我们添加了与 save() 方法中相同的逻辑,但使用了方法参数中的 EntityManager

commitTransactionIfNeeded() 方法中实现了提交逻辑:

private void commitTransactionIfNeeded(EntityTransaction newTransaction) {
    if (newTransaction != null && newTransaction.isActive()) {
        if (!newTransaction.getRollbackOnly()) {
            newTransaction.commit();
        }
    }
}

最后,使用新的 Repository 方法,看看它是如何处理异常的:

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

使用包含重复发票的 List<InvoiceEntity> 调用了批处理方法。但现在我们可以看到,三张发票中的两张已成功保存。

5、分割事务

使用 @Transactional 注解方法,可以获得与上一节相同的行为。唯一的问题是,不能像手动使用事务时那样,在一个 Bean 中调用所有这些方法。但是,可以在 InvoiceRepository 中创建两个 @Transactional 注解方法,并从客户端代码中调用它们。

实现 saveBatchOnly() 方法:

@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、总结

事务是一种强大的机制,能让我们执行原子操作。回滚是失败事务的预期行为。但是,在某些情况下,我们可能需要在事务失败的情况下继续工作,并确保数据得到保存。本文介绍了实现这一目标的各种方法。你可以选择最适合具体情况的方法。


Ref:https://www.baeldung.com/spring-jpa-continue-txn-after-exception