Why is my entity not evicted from my second-level cache?

4.1k views Asked by At

I’m using Hibernate 4.3.11.Final with Spring 3.2.11.RELEASE. I’m confused as to why my cache eviction isn’t working. I have this set up in my DAO …

@Override
@Caching(evict = { @CacheEvict("main") })
public Organization save(Organization organization)
{
    return (Organization) super.save(organization);
}

@Override
@Cacheable(value = "main")
public Organization findById(String id)
{
    return super.find(id);
}

and here’s my Spring config …

<cache:annotation-driven key-generator="cacheKeyGenerator" />

<bean id="cacheKeyGenerator" class="org.mainco.subco.myproject.util.CacheKeyGenerator" />

<bean id="cacheManager"
    class="org.springframework.cache.ehcache.EhCacheCacheManager"
    p:cacheManager-ref="ehcache"/>

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
    p:configLocation="classpath:ehcache.xml"
    p:shared="true" />

<util:map id="jpaPropertyMap">
    <entry key="hibernate.show_sql" value="true" />
    <entry key="hibernate.dialect" value="org.mainco.subco.myproject.jpa.subcoMysql5Dialect" />
    <entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory" />
    <entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider" />
    <entry key="hibernate.cache.use_second_level_cache" value="true" />
    <entry key="hibernate.cache.use_query_cache" value="false" />
    <entry key="hibernate.generate_statistics" value="true" />
    <entry key="javax.persistence.sharedCache.mode" value="ENABLE_SELECTIVE" />
</util:map>

<bean id="sharedEntityManager"
    class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

Yet in the below test, my entity is not getting evicted from the cache, which I know because the line with “hit count #3:” prints out “3” whereas the line with "hit count #2:” prints out “2”.

private net.sf.ehcache.Cache m_cache

@Autowired 
private net.sf.ehcache.CacheManager ehCacheManager;

@Before
public void setup()
{
    m_cache = ehCacheManager.getCache("main");
    m_transactionTemplate = new TransactionTemplate(m_transactionManager);
}   // setup

...
@Test
public void testCacheEviction()
{
    final String orgId = m_testProps.getProperty("test.org.id");

    // Load the entity into the second-level cache
    m_transactionTemplate.execute((TransactionCallback<Void>) transactionStatus -> {            
        m_orgSvc.findById(orgId);
        return null;
    });

    final long hitCount = m_cache.getStatistics().getCacheHits();
    System.out.println("hit count #1:" + hitCount);
    m_transactionTemplate.execute((TransactionCallback<Void>) transactionStatus -> {            
        final Organization org = m_orgSvc.findById(orgId);
        System.out.println("hit count:" + m_cache.getStatistics().getCacheHits());
        org.setName("newName");
        m_orgSvc.save(org);
        return null;
    });

    // Reload the entity.  This should not incur a hit on the cache.
    m_transactionTemplate.execute((TransactionCallback<Void>) transactionStatus -> {
        System.out.println("hit count #2:" + m_cache.getStatistics().getCacheHits());
        final Organization newOrg = m_orgSvc.findById(orgId);
        System.out.println("hit count #3:" + m_cache.getStatistics().getCacheHits());
        return null;
    });

What is the right configuration to allow me to evict an entity from my second-level cache?

Edit: The CacheKeyGenerator class I referenced in my application context is defined below

public class CacheKeyGenerator implements KeyGenerator 
{

    @Override
    public Object generate(final Object target, final Method method, 
      final Object... params) {

        final List<Object> key = new ArrayList<Object>();
        key.add(method.getDeclaringClass().getName());
        key.add(method.getName());

        for (final Object o : params) {
            key.add(o);
        }
        return key;
    }  
}

As such I don’t have to define a “key” for each @Cacheable annotation which I prefer (less code). However, I don’t know how this applies to CacheEviction. I thought the @CacheEvict annotation would use the same key-generation scheme.

3

There are 3 answers

6
manish On

You are missing the cache keys for both @Cacheable and @CacheEvict. Because of that, the two operations are using different cache keys and hence the entity is not evicted.

From the JavaDocs for @Cacheable.key:

Spring Expression Language (SpEL) expression for computing the key dynamically. Default is "", meaning all method parameters are considered as a key, unless a custom {@link #keyGenerator} has been configured.

So, @Cacheable(value = "main") public Organization findById(String id) means that the returned object (of type Organization) will be cached with the key id.

Similarly, @Caching(evict = { @CacheEvict("main") }) public Organization save(Organization organization) means that the string representation of organization will be considered as the cache key.


The solution is to make the following changes:

@Cacheable(value = "main", key ="#id)

@CacheEvict(value = "main", key = "#organization.id")

This will force the two cache operations to use the same key.

6
Thanga On

I have re-written the CodeKeyGenerator as below. This will make a key based on the parameter you send. If it is a string (In case of id), It will use it as it is. If it is a Organization object, It gets the id from that object and use it for the key. This way you don't need to rewrite your code in all places. (Only change is you need to replace your CacheKeyGenerator with the below code. )

public class CacheKeyGenerator implements KeyGenerator 
{
    @Override
    public Object generate(final Object target, final Method method, 
      final Object... params) {
    StringBuilder sb = new StringBuilder();
    sb.append(o.getClass().getName());
    sb.append(method.getName());

    if (params[0].getClass().getName() == "Organization" ) {
      sb.append(((Organization) params[0]).id);
    }
    else if (params[0].getClass().getName() == "java.lang.String" ) {
      sb.append(params[0].toString());
    }
    return sb.toString();
    }  
}

5
David Siro On

What you're trying to evict is not a Hibernate's second-level cache, but rather a Spring Cache, which is completely different caching layer.

As per Hibernate's docs, second-level cache is a cluster or JVM-level (SessionFactory-level) cache on a class-by-class and collection-by-collection basis.

That means it is managed solely by a Hibernate and annotations such as @Cacheable or @CacheEvict have no effect on it.

It's not particularly clear how you get the m_cache instance in your test, but provided it is really a Hibernate's second level cache, it won't be evicted by using the annotations you used.

You'll have to evict it programatically, e.g.:

sessionFactory.evict(Organization.class)

Anyway, as long as you do all your data access within single JVM and through the Hibernate, you should not worry about cache eviction, it is handled by the framework itself transparently.

For more about the eviction possibilities, look at the Hibernate documentation, chapter 20.3. Managing the caches.