在 Spring Boot with Data JPA 中事务不会回滚检查异常

Transactional doesn't roll back on checked exception in Spring Boot with Data JPA

我有一个 ProcessRecon 用例 class,只有一个名为 execute 的方法。它使用 paymentRepository.saveRecon 保存实体 Reconciliation 并使用 paymentRepository.sendReconAck.

调用 Web 服务作为确认的一部分

现在这个外部网络服务有可能失败,在这种情况下我想回滚更改,即保存的实体。由于我使用的是 Unirest,它会抛出 UnirestException,这是一个已检查的异常。

控制台上没有错误,但这可能会有所帮助 [已更新]

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

我目前看到的是,即使有 UnirestException,实体也会被推送到数据库。但是我希望没有数据被保存到数据库中。

我正在使用 Spring Boot 2.3.3 和 MySQL 5.7。这是我的代码。

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);
}

要使注释工作,您必须使用接口而不是 类 进行依赖注入。

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 {
        //method implementation...
    }
}

用法

@Autowired
ProcessRecon processRecon;

public void executeServiceMethod(Reconciliation reconciliation) {
    processRecon.execute(reconciliation)
}

这样您就获得了 ProcessReconImpl 的代理,并具有注释提供的附加功能。

我假设表的默认引擎是 InnoDB 但令我惊讶的是,这些表是使用 MyISAM 引擎创建的'支持交易

我按照建议 here

使用以下 属性 解决了问题
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

而不是

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

这是唯一需要更改的地方。谢谢!