BAEL-7297 - Continue with Transaction After Exception in JPA (#16233)

* BAEL-7297 - Continue with Transaction After Exception in JPA

* BAEL-7297 - Add linebreak

---------

Co-authored-by: ICKostiantyn.Ivanov <ICKostiantyn.Ivanov@amwell.com>
This commit is contained in:
sIvanovKonstantyn 2024-04-06 05:13:52 +02:00 committed by GitHub
parent 849c771ca5
commit db60b63c1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 531 additions and 0 deletions

View File

@ -108,6 +108,7 @@
<module>spring-hibernate-6</module>
<module>spring-jpa</module>
<module>spring-jpa-2</module>
<module>spring-jpa-3</module>
<module>spring-jdbc</module>
<module>spring-jdbc-2</module>
<!--<module>spring-jooq</module>-->

View File

@ -0,0 +1,13 @@
*.class
#folders#
/target
/neoDb*
/data
/src/main/webapp/WEB-INF/classes
*/META-INF/*
# Packaged files #
*.jar
*.war
*.ear

View File

@ -0,0 +1,4 @@
## Spring JPA (3)
### Relevant Articles:
- More articles: [[<-- prev]](/spring-jpa-2)

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-jpa-3</artifactId>
<version>0.1-SNAPSHOT</version>
<name>spring-jpa-3</name>
<packaging>war</packaging>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>persistence-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<!-- test scoped -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
<properties>
<!-- Spring -->
<spring-boot.version>3.2.4</spring-boot.version>
</properties>
</project>

View File

@ -0,0 +1,128 @@
package com.baeldung.continuetransactionafterexception;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.baeldung.continuetransactionafterexception.model.InvoiceEntity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
@Repository
public class InvoiceRepository {
private final Logger logger = LoggerFactory.getLogger(InvoiceRepository.class);
@PersistenceContext
private EntityManager entityManager;
@Autowired
private EntityManagerFactory entityManagerFactory;
@Transactional
public void saveBatch(List<InvoiceEntity> invoiceEntities) {
invoiceEntities.forEach(i -> entityManager.persist(i));
try {
entityManager.flush();
} catch (Exception e) {
logger.error("Duplicates detected, save individually", e);
invoiceEntities.forEach(i -> {
try {
save(i);
} catch (Exception ex) {
logger.error("Problem saving individual entity {}", i.getSerialNumber(), ex);
}
});
}
}
@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());
}
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);
}
}
private void commitTransactionIfNeeded(EntityTransaction newTransaction) {
if (newTransaction != null && newTransaction.isActive()) {
if (!newTransaction.getRollbackOnly()) {
newTransaction.commit();
}
}
}
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());
}
private EntityManager em() {
return entityManagerFactory.createEntityManager();
}
@Transactional
public void saveBatchOnly(List<InvoiceEntity> testEntities) {
testEntities.forEach(entityManager::persist);
entityManager.flush();
}
public List<InvoiceEntity> findAll() {
TypedQuery<InvoiceEntity> query = entityManager.createQuery("SELECT i From InvoiceEntity i", InvoiceEntity.class);
return query.getResultList();
}
@Transactional
public void deleteAll() {
Query query = entityManager.createQuery("DELETE FROM InvoiceEntity");
query.executeUpdate();
}
}

View File

@ -0,0 +1,27 @@
package com.baeldung.continuetransactionafterexception;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baeldung.continuetransactionafterexception.model.InvoiceEntity;
@Service
public class InvoiceService {
@Autowired
private InvoiceRepository repository;
@Transactional
public void saveInvoice(InvoiceEntity invoice) {
repository.save(invoice);
sendNotification();
}
@Transactional(noRollbackFor = NotificationSendingException.class)
public void saveInvoiceWithoutRollback(InvoiceEntity entity) {
repository.save(entity);
sendNotification();
}
private void sendNotification() {
throw new NotificationSendingException("Notification sending is failed");
}
}

View File

@ -0,0 +1,8 @@
package com.baeldung.continuetransactionafterexception;
public class NotificationSendingException extends RuntimeException {
public NotificationSendingException(String text) {
super(text);
}
}

View File

@ -0,0 +1,62 @@
package com.baeldung.continuetransactionafterexception.model;
import java.util.Objects;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "serialNumber")})
public class InvoiceEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String serialNumber;
private String description;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getSerialNumber() {
return serialNumber;
}
public void setSerialNumber(String serialNumber) {
this.serialNumber = serialNumber;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
InvoiceEntity that = (InvoiceEntity) o;
return Objects.equals(serialNumber, that.serialNumber);
}
@Override
public int hashCode() {
return Objects.hash(serialNumber);
}
}

View File

@ -0,0 +1,61 @@
package com.baeldung.com.baeldung.continuetransactionafterexception;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import jakarta.persistence.EntityManagerFactory;
@Configuration
@PropertySource("continuetransactionafterexception/test.properties")
public class ContinueWithTransactionAfterExceptionIntegrationConfiguration {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Value("${hibernate.hbm2ddl.auto}") String hbm2ddlType, @Value("${hibernate.dialect}") String dialect, @Value("${hibernate.show_sql}") boolean showSql) {
LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
result.setDataSource(dataSource());
result.setPackagesToScan("com.baeldung.continuetransactionafterexception.model");
result.setJpaVendorAdapter(jpaVendorAdapter());
Map<String, Object> jpaProperties = new HashMap<>();
jpaProperties.put("hibernate.hbm2ddl.auto", hbm2ddlType);
jpaProperties.put("hibernate.dialect", dialect);
jpaProperties.put("hibernate.show_sql", showSql);
result.setJpaPropertyMap(jpaProperties);
return result;
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
transactionManager.setNestedTransactionAllowed(true);
return transactionManager;
}
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
}

View File

@ -0,0 +1,165 @@
package com.baeldung.com.baeldung.continuetransactionafterexception;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.baeldung.continuetransactionafterexception.InvoiceRepository;
import com.baeldung.continuetransactionafterexception.InvoiceService;
import com.baeldung.continuetransactionafterexception.NotificationSendingException;
import com.baeldung.continuetransactionafterexception.model.InvoiceEntity;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
ContinueWithTransactionAfterExceptionIntegrationConfiguration.class,
InvoiceRepository.class, InvoiceService.class})
@DirtiesContext
@EnableTransactionManagement
class ContinueWithTransactionAfterExceptionIntegrationTest {
@Autowired
private InvoiceRepository repository;
@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());
}
@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());
}
@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));
}
@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));
}
@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));
}
@AfterEach
void clean() {
repository.deleteAll();
}
}

View File

@ -0,0 +1,7 @@
jdbc.driverClassName=org.h2.Driver
jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.show_sql=false
hibernate.hbm2ddl.auto=update

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="15 seconds" debug="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{ISO8601}]-[%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>