英文:
Transactional doesn't roll back on checked exception in Spring Boot with Data JPA
问题
我有一个名为 ProcessRecon
的用例类,其中有一个名为 execute
的单个方法。它使用 paymentRepository.saveRecon
保存实体 Reconciliation
,并在确认的一部分中调用网络服务,使用 paymentRepository.sendReconAck
。
现在有可能这个外部网络服务会失败,在这种情况下,我想回滚所做的更改,即保存的实体。因为我在使用 Unirest,它会抛出一个叫做 UnirestException 的已检查异常。
在控制台上没有错误,但这可能会有所帮助 [已更新]。
2020-08-20 17:21:42,035 DEBUG [http-nio-7012-exec-6] org.springframework.transaction.support.AbstractPlatformTransactionManager: 使用名称 [com.eyantra.payment.features.payment.domain.usecases.ProcessRecon.execute] 创建具有名称的新事务:PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-com.mashape.unirest.http.exceptions.UnirestException
...
2020-08-20 17:21:44,041 DEBUG [http-nio-7012-exec-2] org.springframework.transaction.support.AbstractPlatformTransactionManager: 启动事务回滚
2020-08-20 17:21:44,044 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: 在 EntityManager 上回滚 JPA 事务 [SessionImpl(621663440<open>)]
2020-08-20 17:21:44,059 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: 在事务之后不关闭预绑定的 JPA EntityManager
2020-08-20 17:22:40,020 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor: 在 OpenEntityManagerInViewInterceptor 中关闭 JPA EntityManager
目前我看到的情况是,即使出现 UnirestException
,实体也会被推送到数据库。但我希望在数据库中不保存任何数据。
我正在使用 Spring Boot 2.3.3 与 MySQL 5.7。以下是我编写的代码。
ProcessRecon.java
@Usecase // 此自定义注解源自 @Service
public class ProcessRecon {
private final PaymentRepository paymentRepository;
@Autowired
public ProcessRecon(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
@Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
PaymentDetails paymentDetails = paymentRepository.getByReqId(reconciliation.getReqId());
if (paymentDetails == null)
throw new EntityNotFoundException(ExceptionMessages.PAYMENT_DETAILS_NOT_FOUND);
reconciliation.setPaymentDetails(paymentDetails);
Long transId = null;
if (paymentDetails.getImmediateResponse() != null)
transId = paymentDetails.getImmediateResponse().getTransId();
if (transId != null)
reconciliation.setTransId(transId);
if (reconciliation.getTransId() == null)
throw new ValidationException("如果没有特定 reqId 的立即响应,则应在对帐中提供 transId!");
// 这将会被保存
Reconciliation savedRecon = paymentRepository.saveRecon(reconciliation);
paymentDetails.setReconciliation(savedRecon);
// 如果出错,回滚
paymentRepository.sendReconAck(reconciliation);
return savedRecon;
}
}
PaymentRepositoryImpl.java
@CleanRepository
public class PaymentRepositoryImpl implements PaymentRepository {
@Override
public String sendReconAck(final Reconciliation recon) throws UnirestException {
// 确认 OP
return sendAck(recon.getRequestType(), recon.getTransId());
}
String sendAck(final String requestType, final Long transId) throws UnirestException {
// TODO: 检查 restTemplate 是否可以处理字符(requestType)
final Map<String, Object> queryParams = new HashMap<String, Object>();
queryParams.put("transId", transId);
queryParams.put("requestType", requestType);
logger.debug("{}", queryParams);
final HttpResponse<String> result = Unirest.get(makeAckUrl()).queryString(queryParams).asString();
logger.debug("ack 的输出,带有查询参数 {} 是 {}", queryParams, result.getBody());
return result.getBody();
}
@Override
public Reconciliation saveRecon(final Reconciliation recon) {
try {
return reconDS.save(recon);
}
catch (DataIntegrityViolationException ex) {
throw new EntityExistsException(ExceptionMessages.CONSTRAINT_VIOLATION);
}
}
}
ReconciliationDatasource.java
@Datasource // 扩展自 @Repository
public interface ReconciliationDatasource extends JpaRepository<Reconciliation, Long> {
List<Reconciliation> findByPaymentDetails_User_Id(Long userId);
}
英文:
I have a ProcessRecon
usecase class with a single method named execute
. It saves an entity Reconciliation
using paymentRepository.saveRecon
and calls a web service as part of acknowledgement using paymentRepository.sendReconAck
.
Now there's a chance that this external web service might fail in which case I want to rollback the changes i.e. the saved entity. Since I am using Unirest, it throws UnirestException which is a checked exception.
There are no errors on the console but this will probably be helpful [UPDATED].
2020-08-20 17:21:42,035 DEBUG [http-nio-7012-exec-6] org.springframework.transaction.support.AbstractPlatformTransactionManager: Creating new transaction with name [com.eyantra.payment.features.payment.domain.usecases.ProcessRecon.execute]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-com.mashape.unirest.http.exceptions.UnirestException
...
2020-08-20 17:21:44,041 DEBUG [http-nio-7012-exec-2] org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction rollback
2020-08-20 17:21:44,044 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: Rolling back JPA transaction on EntityManager [SessionImpl(621663440<open>)]
2020-08-20 17:21:44,059 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: Not closing pre-bound JPA EntityManager after transaction
2020-08-20 17:22:40,020 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor: Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
What I see at the moment is that entity gets pushed to database even if there's a UnirestException
. But I expect no data be saved to database.
I am using Spring Boot 2.3.3 with MySQL 5.7. This is the code I have for it.
ProcessRecon.java
@Usecase // this custom annotation is derived from @service
public class ProcessRecon {
private final PaymentRepository paymentRepository;
@Autowired
public ProcessRecon(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
@Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
PaymentDetails paymentDetails = paymentRepository.getByReqId(reconciliation.getReqId());
if (paymentDetails == null)
throw new EntityNotFoundException(ExceptionMessages.PAYMENT_DETAILS_NOT_FOUND);
reconciliation.setPaymentDetails(paymentDetails);
Long transId = null;
if (paymentDetails.getImmediateResponse() != null)
transId = paymentDetails.getImmediateResponse().getTransId();
if (transId != null)
reconciliation.setTransId(transId);
if (reconciliation.getTransId() == null)
throw new ValidationException("transId should be provided in Reconciliation if there is no immediate" +
" response for a particular reqId!");
// THIS GETS SAVED
Reconciliation savedRecon = paymentRepository.saveRecon(reconciliation);
paymentDetails.setReconciliation(savedRecon);
// IF THROWS SOME ERROR, ROLLBACK
paymentRepository.sendReconAck(reconciliation);
return savedRecon;
}
}
PaymentRepositoryImpl.java
@CleanRepository
public class PaymentRepositoryImpl implements PaymentRepository {
@Override
public String sendReconAck(final Reconciliation recon) throws UnirestException {
// Acknowledge OP
return sendAck(recon.getRequestType(), recon.getTransId());
}
String sendAck(final String requestType, final Long transId) throws UnirestException {
// TODO: Check if restTemplate can work with characters (requestType)
final Map<String, Object> queryParams = new HashMap<String, Object>();
queryParams.put("transId", transId);
queryParams.put("requestType", requestType);
logger.debug("{}", queryParams);
final HttpResponse<String> result = Unirest.get(makeAckUrl()).queryString(queryParams).asString();
logger.debug("Output of ack with queryParams {} is {}", queryParams, result.getBody());
return result.getBody();
}
@Override
public Reconciliation saveRecon(final Reconciliation recon) {
try {
return reconDS.save(recon);
}
catch (DataIntegrityViolationException ex) {
throw new EntityExistsException(ExceptionMessages.CONSTRAINT_VIOLATION);
}
}
}
ReconciliationDatasource.java
@Datasource // extends from @Repository
public interface ReconciliationDatasource extends JpaRepository<Reconciliation, Long> {
List<Reconciliation> findByPaymentDetails_User_Id(Long userId);
}
答案1
得分: 0
为使注解起作用,您必须在依赖注入中使用接口而不是类。
interface ProcessRecon {
Reconciliation execute(final Reconciliation reconciliation)
throws UnirestException;
}
然后:
@Usecase
public class ProcessReconImpl implements ProcessRecon {
private final PaymentRepository paymentRepository;
@Autowired
public ProcessReconImpl(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
@Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
// 方法实现...
}
}
用法:
@Autowired
ProcessRecon processRecon;
public void executeServiceMethod(Reconciliation reconciliation) {
processRecon.execute(reconciliation);
}
这样,您将获得具有由注解提供的附加功能的 ProcessReconImpl
的代理。
英文:
To make annotations work you have to use interfaces instead of classes for dependency injection.
interface ProcessRecon {
Reconciliation execute(final Reconciliation reconciliation)
throws UnirestException;
}
Then
@Usecase
public class ProcessReconImpl implements ProcessRecon {
private final PaymentRepository paymentRepository;
@Autowired
public ProcessReconImpl(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
@Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
//method implementation...
}
}
Usage
@Autowired
ProcessRecon processRecon;
public void executeServiceMethod(Reconciliation reconciliation) {
processRecon.execute(reconciliation)
}
This way you have got proxy of ProcessReconImpl
with provided by annotations additional functionality.
答案2
得分: 0
我假设表格的默认引擎会是InnoDB,但令我惊讶的是,这些表格实际上是使用MyISAM引擎创建的,而该引擎不支持事务。
我通过使用以下建议的属性解决了这个问题(链接在这里):
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
而不是
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
这是唯一需要的更改。谢谢!
英文:
I assumed the default engine for the tables would be InnoDB but to my surpise, the tables were created using MyISAM engine which doesn't support transactions.
I resolved the problem by using the below property as suggested here
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
instead of
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
That was the only change required. Thanks!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论