JPA 異常後繼續事務
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()
方法中,我們嘗試使用單一刷新操作來保存物件清單。如果在此操作期間發生任何異常,我們將捕獲它並繼續單獨保存每個物件。讓我們透過以下方式實作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. 手動使用事務
當持久層遇到回滾的情況時,我們可以手動控制事務,確保即使出現異常,資料也能保存。
讓我們將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));
}
我們使用包含重複項的發票清單呼叫批次方法。但現在,我們可以看到三張發票中有兩張已成功保存。
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()
方法來單獨保存所有項目(如果可能)。最後,我們可以看到所有預期的項目都已儲存。
六,結論
事務是一種強大的機制,使我們能夠執行原子操作。回滾是失敗事務的預期行為。然而,在某些情況下,我們可能需要在失敗的情況下繼續我們的工作並確保我們的資料被保存。我們回顧了實現這一目標的各種方法。我們可以選擇最適合我們具體情況的一種。
像往常一樣,完整的源代碼可以在 GitHub 上找到。