Spring中JPA在異常後三種方法繼續事務

banq發表於2024-04-13


JPA 中的事務機制是一個強大的工具,它透過提交所有更改或在發生異常時回滾它們來確保原子性和資料完整性。然而,在某些情況下,遇到異常後需要繼續事務而不回滾資料更改。

在本文中,我們將深入研究出現這種情況的各種用例。此外,我們將探索此類情況的潛在解決方案。

確定問題
交易中可能出現異常的情況主要有兩種。讓我們從瞭解它們開始。

1.服務層異常後回滾事務
我們首先可能遇到回滾的地方是在服務層,外部異常可能會影響資料庫的更改。

讓我們使用以下示例更仔細地檢查此場景。首先,讓我們新增 InvoiceEntity ,它將用作我們的資料模型:

@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = <font>"serialNumber")})
public class InvoiceEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String serialNumber;
    private String description;
   
//Getters and Setters<i>
}

在這裡,我們有一個自動生成的內部 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(<font>"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(<font>"#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(<font>"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(<font>"Entity is saved: {}", invoiceEntity.getSerialNumber());
}

我們透過捕獲並記錄異常來處理每個異常,以避免觸發事務回滾。讓我們呼叫它並看看它是如何工作的:

@Test
void givenInvoiceRepository_whenExceptionOccursDuringBatchSavingInternally_thenNoDataShouldBeSaved() {
    List<InvoiceEntity> testEntities = new ArrayList<>();
    InvoiceEntity invoiceEntity = new InvoiceEntity();
    invoiceEntity.setSerialNumber(<font>"#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(<font>"#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(<font>"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(<font>"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(<font>"#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(<font>"#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()方法來單獨儲存所有專案(如果可能)。最後,我們可以看到所有預期的專案都已儲存。


結論
事務是一種強大的機制,使我們能夠執行原子操作。回滾是失敗事務的預期行為。然而,在某些情況下,我們可能需要在失敗的情況下繼續我們的工作並確保我們的資料被儲存。我們回顧了實現這一目標的各種方法。我們可以選擇最適合我們具體情況的一種。
 

相關文章