Spring中JPA在异常后三种方法继续事务


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

在本文中,我们将深入研究出现这种情况的各种用例。此外,我们将探索此类情况的潜在解决方案。

确定问题
交易中可能出现异常的情况主要有两种。让我们从了解它们开始。

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.持久层异常后回滚事务
我们可能面临隐式回滚的另一种情况是在持久层中。

我们可以假设,如果我们从数据库捕获异常,我们就能够在同一事务中继续我们的数据操作逻辑。但事实并非如此。让我们在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());
}

我们准备了一份发票列表,其中两张违反了序列号字段的唯一约束。当尝试保存此发票列表时,我们遇到UnexpectedRollbackException,并且数据库中没有保存任何项目。发生这种情况是因为,在第一个异常之后,我们的事务被标记为仅回滚,从而防止在其中发生任何进一步的提交。


办法1:使用@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。但是,发票已成​​功保存在数据库中。

办法2:手动使用事务
当持久层遇到回滚的情况时,我们可以手动控制事务,保证即使出现异常,数据也能保存。

让我们将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()方法中相同的逻辑,但我们从方法参数中使用了实体管理器。在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));
}

我们使用包含重复项的发票列表调用批处理方法。但现在,我们可以看到三张发票中有两张已成功保存。

办法3:分割事务
我们可以使用@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()方法来单独保存所有项目(如果可能)。最后,我们可以看到所有预期的项目都已保存。


结论
事务是一种强大的机制,使我们能够执行原子操作。回滚是失败事务的预期行为。然而,在某些情况下,我们可能需要在失败的情况下继续我们的工作并确保我们的数据被保存。我们回顾了实现这一目标的各种方法。我们可以选择最适合我们具体情况的一种。