2
votes

I am using spring cache abstraction using Ehcache as a cache provider. I am trying to attach cache operations to spring JPA transactions, but not able to do so.

Even though transaction fails/rollback cache put happens.

Configuration,

        @Bean
    public EhCacheManagerFactoryBean cacheManagerUsingSpringApi() {
        EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();

        // provide xml file for ehcache configuration/
        ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("spring-cache-abs-ehcache.xml"));

        return ehCacheManagerFactoryBean;
    }

    @Bean
    public org.springframework.cache.CacheManager ehCacheCacheManager() {
        final EhCacheCacheManager ehCacheCacheManager = new EhCacheCacheManager(cacheManagerUsingSpringApi().getObject());
        ehCacheCacheManager.setTransactionAware(true); // Setting  transaction aware
        return ehCacheCacheManager;
    }

spring-cache-abs-ehcache.xml,

    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="true"
         monitoring="autodetect"
         dynamicConfig="true">

    <cache name="EmployeeCache"
           maxEntriesLocalHeap="10000"
           eternal="false"
           timeToIdleSeconds="300" timeToLiveSeconds="600"
           memoryStoreEvictionPolicy="LFU"
           transactionalMode="off">
        <persistence strategy="localTempSwap" />
    </cache>
</ehcache>

EmployeeRepository,

    public interface EmployeeRepository extends JpaRepository<Employee, Long>, CustomEmployeeRepository {
    }

Transactional Method,

    @Repository
public class EmployeeRepositoryImpl implements CustomEmployeeRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Autowired
    private CacheManager cacheManager;

    // THIS METHOD SHOULD NOT PUT INTO CACHE WITH NEW NAME
    @Override
    @Transactional
    @Cacheable(cacheNames = "EmployeeCache", key = "#a0.id")
    public Employee customUpdate(Employee employee) {
        employee.setFirstName(UUID.randomUUID().toString());
        entityManager.merge(employee);

        // rolling back transaction 
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

        return employee;
    }
}

Test case(caller),

    @Test
public void testCustomUpdate() {
    // GIVEN
    Employee employee = new Employee();
    employee.setFirstName(UUID.randomUUID().toString());
    employee.setLastName(UUID.randomUUID().toString());
    final Employee savedEmployee = employeeRepository.save(employee);

    // WHEN
    final Employee updatedEmployee = employeeRepository.customUpdate(savedEmployee);

    // THEN
    final Cache employeeCache = cacheManager.getCache("EmployeeCache");
    final Cache.ValueWrapper object = employeeCache.get(updatedEmployee.getId());
    assertNull(object);
}

Test Should succeed i.e spring should not PUT data into cache in method employeeRepository.customUpdate if transaction was rollback in that method. But, spring puts data into cache even if transactions fails. NOTE: Weird part is, if entry already exists in cache , then @CachePut does not updates entry in cache if transaction fails.

So, if I annotate employeeRepository.save with @CachePut(cacheNames = "EmployeeCache", key = "#result.id") then cache is not updated in update call.

What is missing here?

1

1 Answers

2
votes

Option 1: Use advice ordering,

Refer to: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-ordering

The below declaration will cause the transactional advice to be executed first and then the cacheable advice.

<tx:annotation-driven order="0"/>

<cache:annotation-driven cache-manager="ehCacheManager" order="1"/>

Option 2: Annotating @Cacheable and @Transactional on different methods

You will see the code working if you call the @Cacheable annotated method from another method which is annotated with @Transactional. Both annotations i.e. @Cacheable and @Transaction on the same method won't help your cause unless you have advice ordering implemented. Please see below a crude implementation of your logic.

    @Override
    @Transactional
    public Employee customUpdate(Employee employee) {
        Employee mergedEmployee = updateHelper(employee);

        // rolling back transaction 
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

        return mergedEmployee;
    }

    @Cacheable(cacheNames = "EmployeeCache", key = "#a0.id")
    public Employee updateHelper(Employee employee)) {
        employee.setFirstName(UUID.randomUUID().toString());
        //On a merge(...) call on the entityManager, the returned item is the managed 
        //instance
        return entityManager.merge(employee); 
    }