0

Probably a bug here, but I'm not seeing it.

I want to cancel a transaction. Here are the parts of my JPA repository

@Repository
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long> {
    List<PaymentTransaction> findByPaymentStatus(PaymentStatus paymentStatus);

    @Transactional
    @Modifying
    @Query("UPDATE PaymentTransaction p " +
            "SET p.paymentStatus = 'Canceled' " +
            "WHERE p.siteId = :siteId " +
            "  AND p.transactionNumber = :transNum ")
    void cancelTransaction(@Param("siteId") String siteId, @Param("transNum") Long transactionNumber);

    PaymentTransaction findBySiteIdAndTransactionNumber(String siteId, Long transactionNumber);

and my unit test:

@RunWith(SpringRunner.class)
@DataJpaTest
class PaymentTransactionRepositoryTest {
    public static final String          CURRENCY_CODE = "KES";
    public static final PaymentStatus   PAYMENT_STATUS = PaymentStatus.Complete;
    public static final Long            PRODUCT_ID = 1170L;
    public static final FixedDecimal    QUANTITY = new FixedDecimal("5.0000");
    public static final String          REF_NUM = "23-7-655737-X-117";
    public static final String          SITE_ID = "ABCD1234";
    public static final FixedDecimal    TOTAL = new FixedDecimal(200, 2);
    public static final Long            TRANS_NUM = 42L;

    @Autowired
    private PaymentTransactionRepository paymentTransactionRepository;

    private PaymentTransaction paymentTransaction;

    public static PaymentTransaction getPaymentTransaction() {
        PaymentTransaction result = new PaymentTransaction();

        result.setSiteId(SITE_ID);
        result.setCurrencyCode(CURRENCY_CODE);
        result.setPaymentStatus(PAYMENT_STATUS);
        result.setProductId(PRODUCT_ID);
        result.setQuantity(QUANTITY);
        result.setReferenceNumber(REF_NUM);
        result.setTransactionNumber(TRANS_NUM);
        result.setTotal(TOTAL);

        return result;
    }

    @BeforeEach
    void setUp() {
        paymentTransaction = getPaymentTransaction();
    }

    @Test
    void testCancelTransaction() {
        paymentTransaction.setPaymentStatus(PaymentStatus.Created);
        paymentTransaction = paymentTransactionRepository.save(paymentTransaction);

        paymentTransactionRepository.cancelTransaction(SITE_ID, TRANS_NUM);

        paymentTransaction = paymentTransactionRepository.findBySiteIdAndTransactionNumber(SITE_ID, TRANS_NUM);
        assertEquals(PaymentStatus.Canceled, paymentTransaction.getPaymentStatus()); //test fails here
    }
}

Then when I run the unit test, I get

expected:<Canceled> but was:<Created>
Expected :Canceled
Actual   :Created

I'm sure I've missed something, but just can't see it.

Running tests against H2 database.

Here's the hibernate output:

Hibernate: call next value for hibernate_sequence
Hibernate: insert into payment_transaction (created, currency_code, customer_id, payment_status, product_id, quantity, ref_num, site_id, total, trans_num, updated, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: update payment_transaction set payment_status='Canceled' where site_id=? and trans_num=?
Hibernate: select paymenttra0_.id as id1_2_, paymenttra0_.created as created2_2_, paymenttra0_.currency_code as currency3_2_, paymenttra0_.customer_id as customer4_2_, paymenttra0_.payment_status as payment_5_2_, paymenttra0_.product_id as product_6_2_, paymenttra0_.quantity as quantity7_2_, paymenttra0_.ref_num as ref_num8_2_, paymenttra0_.site_id as site_id9_2_, paymenttra0_.total as total10_2_, paymenttra0_.trans_num as trans_n11_2_, paymenttra0_.updated as updated12_2_ from payment_transaction paymenttra0_ where paymenttra0_.site_id=? and paymenttra0_.trans_num=?
Thom
  • 14,013
  • 25
  • 105
  • 185
  • That might be a problem with your transaction setup, i.e. you might be running two transactions in parallel and thus the update doesn't succeed or the read doesn't see the changes due to transaction isolation. – Thomas Jan 08 '21 at 14:43
  • I have had to do this before in the repo ` @Modifying @Transactional S save(S myEntity);` – jr593 Jan 08 '21 at 14:43

1 Answers1

1

As far as I understand from the documentation, @Modifying only changes how the query is executed so that updates actually work. But it does not by default ensure that the persistence context is updated correctly.

In your case, the entity is already loaded into the persistence context (by calling paymentTransactionRepository.save) and then cancelTransaction updates the database (but not the entity in the persistence context!). But the following findBySiteIdAndTransactionNumber query fetches the entity from the persistence context which is outdated at that point.

To fix that, change the annotation to this:

@Modifying(clearAutomatically = true, flushAutomatically = true)

clearAutomatically is the important part here because it clears the persistence context and therefore ensures that the next query cannot fetch the outdated entity from it.

flushAutomatically would only be needed if you changed the PaymentTransaction entity prior to the cancelTransaction call. Without the flush, those changes would be lost (because the changed entity gets cleared from the context). It might actually required in your case because the paymentTransactionRepository.save might not be flushed yet otherwise.

Also: This all happens because both the creation, cancelling and reading of paymentTransaction happen inside the same transaction. That is because @DataJpaTest includes @Transactional which means the entire test method runs in a single transaction.

x4rf41
  • 5,184
  • 2
  • 22
  • 33