javaspringdatabasehibernateaop

Why I get error transaction is in progress when my listener phase AFTER_COMMIT


Why if I set the transaction propagation= Required. I will get error org.springframework.dao.InvalidDataAccessApiUsageException: no transaction is in progress. I cant understand that because my Listner has phase TransactionPhase.AFTER_COMMIT and the transaction should be completed and create new transaction But if I set @Transactional(Transactional.TxType.REQUIRES_NEW) it doesn't throw an error. How it works

@Aspect
@Component
@RequiredArgsConstructor
public class AspectOffsettingRepository {

  private final ApplicationEventPublisher publisher;
  private static final String STATUS_FOR_MESSAGE = "REGISTERED";

  @AfterReturning(
      pointcut = "execution(* service.repository.impl.OffsettingJpaRepository.save(..))",
      returning = "result"
  )
  public void afterSave(OffsettingEntity result) {
    handleAfterSave(result);
  }

  @AfterReturning(
      pointcut = "execution(* service.repository.impl.OffsettingJpaRepository.saveAndFlush(..))",
      returning = "result"
  )
  public void afterSaveAndFlush(OffsettingEntity result) {
    handleAfterSave(result);
  }

  private void handleAfterSave(OffsettingEntity entity) {
    if (entity.getStatus() != null && STATUS_FOR_MESSAGE.equals(entity.getStatus())) {
      publisher.publishEvent(new OffsettingRegisteredEvent(
          entity.getOffsetId(),
          entity.getCreatedBy(),
          entity.getFirstPartnerId().toString()
      ));
    }
  }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void postSave(OffsettingRegisteredEvent event) {
    offsettingOutboxService.createMessageToRo(event);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class OffsettingOutboxService {

  private final OffsettingDebtDao offsettingDebtDao;
  private final OffsettingDao offsettingDao;
  private final DebtService debtService;
  private final OutboxContentBuilder outboxBuilder;
  private final OutboxService outboxService;
  private final MdmGateway mdmGateway;

  private static final String OFFSETTING_STATUS = "GLA_CONFIRMATION";

  @Transactional(Transactional.TxType.REQUIRES_NEW)
  public void createMessageToRo(OffsettingRegisteredEvent event) {
    List<PairOffsettingDebtIds> pairsDebtId = findPairsDebtId(event.offsetId());
    EmployeeInfo employeeInfo = mdmGateway.findEmployeeInfo(event.createdBy());
    Map<UUID, DebtModel> debtMap = findDebts(pairsDebtId);
    Map<String, EmployeeInfo> employeeInfoCache = new HashMap<>();

    boolean isRegisterFilled = false;

    for (PairOffsettingDebtIds pair : pairsDebtId) {
      DebtModel firstDebt = debtMap.get(pair.getFirstDebtId());
      DebtModel secondDebt = debtMap.get(pair.getSecondDebtId());

      if (firstDebt == null || secondDebt == null) {
        continue;
      }

      EmployeeInfo firstEmployeeInfo = employeeInfoCache.computeIfAbsent(
          firstDebt.getCreatedBy(), mdmGateway::findEmployeeInfo);
      EmployeeInfo secondEmployeeInfo = employeeInfoCache.computeIfAbsent(
          secondDebt.getCreatedBy(), mdmGateway::findEmployeeInfo);

      boolean fillRegisterForThisPair = !isRegisterFilled && Boolean.TRUE.equals(secondDebt.getIsRegisterCreator());

      OutboxRegisterOfOperationContent registerOfOperationContent = createRegisterOfOperationContent(
          OutboxRegisterOfOperationContentDto.builder()
              .debtFirstSide(firstDebt)
              .debtSecondSide(secondDebt)
              .serviceOriginatorDepartment(event.partnerId())
              .userName(employeeInfo.fio())
              .debtFirstSideEmployeeInfo(firstEmployeeInfo)
              .debtSecondSideEmployeeInfo(secondEmployeeInfo)
              .debtFirstSideBeginDate(getRelationBeginDate(firstDebt.getRegister().getAccountingPartnerId().toString()))
              .debtSecondSideBeginDate(getRelationBeginDate(secondDebt.getRegister().getAccountingPartnerId().toString()))
              .isRegisterFill(fillRegisterForThisPair)
              .build()
      );

outboxService.save(outboxBuilder.buildOutbox(registerOfOperationContent, fillRegisterForThisPair));

      if (fillRegisterForThisPair) {
        isRegisterFilled = true;
      }
    }

    OffsettingEntity offsettingEntity = offsettingDao.findByOffsetId(event.offsetId());
    offsettingEntity.setStatus(OFFSETTING_STATUS);

    offsettingDao.save(offsettingEntity);
  }

Solution

  • This seems to be explained in the docs for @TransactionalEventListener:

    WARNING: if the TransactionPhase is set to AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION, the transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still "participate" in the original transaction, but changes will not be committed to the transactional resource. See TransactionSynchronization.afterCompletion(int) for details.

    That explains your observations. Put another way: an AFTER_COMMIT listener executes (initially) in the context of the transaction that just committed (which is, admittedly, a bit weird), but because that transaction has already committed, you can't actually do much of anything in it that requires a transaction. But if you enter a method annotated @Transactional(Transactional.TxType.REQUIRES_NEW) then, as that annotation specifies, whatever runs within gets its own, new transaction.

    The referenced docs of TransactionSynchronization.afterCompletion(int) recommend a related approach:

    NOTE: The transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still "participate" in the original transaction, allowing to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.