Closed alex-sira closed 1 day ago
The constraint violation causes the transaction in which isValidName
is running to be marked as rollback-only. When the transaction manager tries to commit the transaction it notes that it's rollback-only and throws an UnexpectedRollbackException
. Your code that's calling isValidName
isn't in a try-catch block so this exception reaches the transaction handling for createName
. The exception's unexpected so that transaction is rolled back as well.
You can avoid the problem by calling isValidName
defensively, for example:
@Transactional
public void createName(int schedulatioNumber) throws InterruptedException {
MyNumberEntity myNumberEntity = new MyNumberEntity();
myNumberEntity.setSchedulationNumber(schedulatioNumber);
myNumberEntityService.save(myNumberEntity);
boolean isValid = false;
int attempt = 0;
do {
attempt++;
String name = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
try {
isValid = validationNameService.isValidName(name, schedulatioNumber);
}
catch (Exception ex) {
// Continue
}
if (isValid) {
break;
}
Thread.sleep(20000);
} while (attempt < 10);
}
If you have any further questions, please follow up on Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.
The constraint violation causes the transaction in which
isValidName
is running to be marked as rollback-only. When the transaction manager tries to commit the transaction it notes that it's rollback-only and throws anUnexpectedRollbackException
. Your code that's callingisValidName
isn't in a try-catch block so this exception reaches the transaction handling forcreateName
. The exception's unexpected so that transaction is rolled back as well.You can avoid the problem by calling
isValidName
defensively, for example:@Transactional public void createName(int schedulatioNumber) throws InterruptedException { MyNumberEntity myNumberEntity = new MyNumberEntity(); myNumberEntity.setSchedulationNumber(schedulatioNumber); myNumberEntityService.save(myNumberEntity); boolean isValid = false; int attempt = 0; do { attempt++; String name = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")); try { isValid = validationNameService.isValidName(name, schedulatioNumber); } catch (Exception ex) { // Continue } if (isValid) { break; } Thread.sleep(20000); } while (attempt < 10); }
If you have any further questions, please follow up on Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.
Thank you @wilkinsona for your response, but I still think this is a bug in the transaction management of JPA.
Yes, I already tried your solution because I understood that the UnexpectedRollbackException
is thrown by the proxy after the end of the method isValidName and in this way it works correctly.
The issue is that the transaction in the isValidName must not be marked as rollback, because the transaction starts in this method, with the REQUIRED_NEW, and the exception that has to rollback it is not propagated out of the method with the @Transactional
annotation because I have catched it inside the method isValidName and suppressed it
because I have catched it inside the method isValidName and suppressed it
That doesn't matter. When a ConstraintViolationException
is thrown, Hibernate will mark the transaction as rollback-only. This happens before your code has a chance to catch the exception.
@alex-sira If you make the following changes:
@Service
@Slf4j
@RequiredArgsConstructor
public class ValidationNameService {
private final MyNameEntityService myNameEntityService;
public boolean isValidName(String name, int schedulationNumber) {
boolean isValid = true;
try {
myNameEntityService.save(name, schedulationNumber);
} catch (Exception e) {
log.warn("Name: [{}] cannot be used because it already exists", name);
isValid = false;
}
return isValid;
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class MyNameEntityService {
private final MyNameEntityRepository myNameEntityRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public MyNameEntity save(String name, int schedulationNumber) {
log.info("Saving MyNameEntity with name: [{}]", name);
MyNameEntity myNameEntity = new MyNameEntity();
myNameEntity.setName(name);
myNameEntity.setSchedulationNumber(schedulationNumber);
myNameEntityRepository.save(myNameEntity);
log.info("Saved MyNameEntity with id: [{}]", myNameEntity.getId());
return myNameEntity;
}
}
UnexpectedRollbackException
will not be thrown anymore.
Bug with the transaction management in spring-data-jpa.
I'm using spring-boot-starter-parent 3.3.5. The bug is: when an exception is thrown in a method inside a transaction, is not possible to stop it with a catch clause, but it will be propogate and will resolve in an UnexpectedRollbackException out of this transaction.
I'll try to explain it better: this is the stack-trace of my Exception:
I created an application to reproduce it:
https://github.com/alex-sira/transaction-propagation-test
Explanation of the issue
While in the transaction opened by the upper method
the lower methods should save two entities:
The issue is that the exception thrown by the database in the method
is a DataIntegrityViolation that must be catch in the try-catch block, but it is propagated in the upper method:
causing the rollback of the transaction with the exception:
These are my classes:
Entities and Repositories:
Scheduler
Upper transactional service:
First service that will save the entity without constraint
Service that should try the save operation until it works:
This is the pom.xml: