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

3

10

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.

Jammie answered 25/2, 2016 at 23:5 Comment(0)
J
4

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

Janessa answered 3/3, 2016 at 11:39 Comment(6)
So I understand your answer, you're saying I can eitehr add a "key" to every Cacheable annotation in my application (not an option since I want an auto-generated key) or I can change the signature of every method in my application with Cacheable (e.g. change save(organizaiton org) to save(param1, param2, ...)? Am I understanding you correctly?Jammie
It is not an option to re-code every method signature in my application that uses a Cacheable annotation -- there are hundreds. How would I write a @CacheEvict annotation given my current method signature and key-generator?Jammie
Finally I found a way to rewrite your code CodeKeyGenerator. Please check the answer which I updated in the Updated solution Starts and ends sectionJanessa
Ok, thanks. Perhaps this is obvious, but given your CacheKeyGenerator, what would be correspponding @CacheEvict annotation be? Also will I have to change this CacheKeyGenerator for other classes in which I also want to apply CacheEvict to methods?Jammie
You can use the same cache evict annotation what you already use. As you mentioned this cachekeygenerator in the spring config, it will be a change in only one placeJanessa
I'm concerned that because we're hard-coding "Organization," this code would not work in other situations, but notwithstanding, I tried your code above and it doesn't work anyway (second-level hit still records a cache in the JUnit code I posted).Jammie
N
3

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.

Nickelsen answered 1/3, 2016 at 6:6 Comment(6)
Becuase I include this in my application context (from my question), '<bean id="cacheKeyGenerator" class="..."', I don't need to define a "key" in my @Cacheable annotation. I included that class in my edit. Given that, I don't know what key to put in CacheEvict. I tried your solution anyway, but it didn't evict the entity from teh cache.Jammie
Your key generator generates different keys for the two methods, which again leads to key mismatch and hence no eviction. Turn on debug logs to verify this. You will see Spring caching logs that will show you the logical error in how you are generating the key.Nickelsen
I have created a sample application to demonstrate to you that my answer works. You can download it and run it as mvn clean test to see all the tests passing. There is a test in there for checking cache status according to your calls. I will suggest you to take my sample and add your code to it, without your custom key generator first. Things should work if you do not make any changes to the working sample. Then plug in your key generator to see where the error is.Nickelsen
When you say "Your key generator generates different keys for the two methods," I know, that's the point. I don't want to specify a "key" for every @Cacheable annotation. I like being able to auto-generate the key. Given this constraint, how do I evict the appropriate entity from my cache using @CacheEvict? Do you understand what I'm asking?Jammie
A cache is like a Map. @Cacheable is like Map.put("key", "value") and @CacheEvict is like Map.remove("key"). With your key generator you are performing Map.put("key", "value") and Map.remove("someotherkey"). If you then expect the entry corresponding to "key" being evicted, of course it will not be evicted because you have used different keys for put and remove. You need to change your key generator so that it passes exactly the same key to @Cacheable and @CacheEvict.Nickelsen
If you can suggest a new CacheKeyGenerator that does not require me to write "key" attributes for each Cacheable annotation and then what the appropriate CacheEvict annotation would be in my question, then I'm all set. Otherwise, my question remains -- what is the CacheEvict code I should use given the constraints I have outlined.Jammie
D
1

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.

Do answered 3/3, 2016 at 20:51 Comment(5)
This is a distributed application that will use one JVM per application server and will use Spring and ehcache as a second-level cache. How would I write the appropriate @CacheEvict annotation given the constraints of my question? The solution shoudl apply to the "org.springframework.cache.annotation.CacheEvict" annotation.Jammie
Can you post the code how you retrieve m_cache instance? Currently your application has two caching layers. One is the Spring Cache (that caches the calls to your DAO), the second one is the actual second-level cache of Hibernate. Unless it is known what kind of cache m_cache is, it's hard to to any assessment.Do
I have posted the definition in the question. "m_cache" is of type "net.sf.ehcache.Cache".Jammie
Ok, but how do you obtain that instance?In the code, there's just a field declaration. The point I've been making is that if it's Hibernate's cache, it won't get evicted by @CacheEvict annotation.Do
I have edited my question further. Cache is taken from teh @Autowired net.sf.ehcache.CacheManager instance. You are focussing on the wrong details, though. If the second-level cache weren't configured properly, I wouldn't be registering second-level cache hits but I am. If you are confused by the configuration, all you need to do is tell me what the appropriate CacheEvict annotation would be given my key generator and this will be and the bounty is yours.Jammie

© 2022 - 2024 — McMap. All rights reserved.