事务在使用Spring Boot和Data JPA时不会在已检查的异常上进行回滚。

huangapple go评论73阅读模式
英文:

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&lt;open&gt;)]
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(&quot;transId should be provided in Reconciliation if there is no immediate&quot; +
                    &quot; response for a particular reqId!&quot;);

        // 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&lt;String, Object&gt; queryParams = new HashMap&lt;String, Object&gt;();
        queryParams.put(&quot;transId&quot;, transId);
        queryParams.put(&quot;requestType&quot;, requestType);

        logger.debug(&quot;{}&quot;, queryParams);

        final HttpResponse&lt;String&gt; result = Unirest.get(makeAckUrl()).queryString(queryParams).asString();

        logger.debug(&quot;Output of ack with queryParams {} is {}&quot;, 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&lt;Reconciliation, Long&gt; {
    List&lt;Reconciliation&gt; 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!

huangapple
  • 本文由 发表于 2020年8月20日 04:40:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/63494686.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定