1

I'm trying to configure L2+QueryCache for Hibernate 6.1.7 in my Spring 6 app running Ehcache 3.10.x

In my app, Spring-level caches with method-level @Cacheable, @CachePut, @CacheEvict, etc. work fine. I create the caches and they work as expected.

L2 cache also works correctly, but only if I don't create any L2 caches and let Spring create the default ones (missing_cache_strategy: create). This is presentig heap-related problems in production because the default cache settings in Ehcache are allowing infinite heap usage.

How do I create L2 caches with my own configs, from code? I want to avoid the xml Ehcache config at this time, and Ehcache seems to be moving away from it anyway.

This is how I configure Spring-level caches, and it works:

    @Configuration
    @EnableCaching
    public class MyCacheConfig {
      @Bean
      public CacheManager ehCacheManager() {
          CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();

         //Create a Spring-level cache
         var config = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(SomeMethodArg.class, SomeMethodReturnType.class)
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(30))
                .build();
          cacheManager.createCache("my_spr_cache", Eh107Configuration.fromEhcacheCacheConfiguration(config));

          return cacheManager;
      }
    }

Following the Ehcache docs and other sources, I was able to create L2 caches in a similar manner by adding another cache to the bean above:

//Create a L2 cache
var config = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(CacheKeyImplementation.class, AbstractReadWriteAccess.Item.class)
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(30))
                .build();
        
//L2 cache name should be the fully qualified name of the Entity
cacheManager.createCache("com.mydomain.model.MyClass", Eh107Configuration.fromEhcacheCacheConfiguration(config));

And of course, the MyClass Entity is annotated with:

@jakarta.persistence.Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) //Note that some other classes instead use NONSTRICT_READ_WRITE 

Now the cache I made is correctly created at runtime and Ehcache doesn't build a default one. I can confirm this from the startup logs - manually created caches are logged significantly earlier on.

Now the issue: this method is unreliable. Firstly, it seems that changing the ConcurrencyStrategy breaks the cache creation code, for example, switching to NONSTRICT_READ_WRITE on the entity will cause Exceptions:

Exception in thread "(3/16)" java.lang.ClassCastException: Invalid value type, 
expected:  org.hibernate.cache.spi.support.AbstractReadWriteAccess$Item 
but was :  org.hibernate.cache.spi.entry.StandardCacheEntryImpl

The first attempt I made was obviously following the exception literally and changing the value class to StandardCacheEntryImpl. This just causes even more erratic behavior, with the cache behaving fine at low usage but failing when consistent load is applied:

Caused by: java.lang.ClassCastException: Invalid value type, 
expected : org.hibernate.cache.spi.support.AbstractReadWriteAccess$Item 
but was : org.hibernate.cache.spi.support.AbstractReadWriteAccess$SoftLockImpl

If I switch those two classes back and forth, I get the same exception but reversed.

This leads me to believe that I'm using the wrong approach entirely. I can't find any modern and updated question on this, could someone kindly point me in the right direction?


If it can be useful, this is the relevant config in my application.yml:

# [More settings here...]
  jpa:
    # [More settings here...]
    properties:
      jakarta:
        persistence:
          sharedCache:
            #required - enable selective caching mode - only entities with @Cacheable annotation will use L2 cache.
            mode: ENABLE_SELECTIVE
      hibernate:
        cache:
          use_second_level_cache: true
          use_query_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
        jakarta:
          cache:
            missing_cache_strategy: create
  • In depth investigation reveals that when Hibernate does not find a cache for a certain name, the default caches it builds are specified at `org.hibernate.cache.jcache.internal.JCacheRegionFactory#getOrCreateCache -> createCache` which in turn makes use of an empty `javax.cache.configuration.MutableConfiguration`. The default constructor of this class specifies `Object.class` as the value type for the cache. This is not ideal, but in absence of better answers to my question, I will resort to sticking with my approach and using Object as a value class for the cache map. It looks wrong, however. – RemasteredDruid May 07 '23 at 01:53
  • replace `.newCacheConfigurationBuilder(CacheKeyImplementation.class, AbstractReadWriteAccess.Item.class)` with `.newCacheConfigurationBuilder(Object.class, Object.class)` – Andrey B. Panfilov May 07 '23 at 01:56
  • @AndreyB.Panfilov yes, I was getting to that same conclusion, see my comment above. But is this really the way? More than for it to "just work", I would like to use the proper way. This feels like a hack. – RemasteredDruid May 07 '23 at 02:15
  • no, that is not a hack, that is how L2-cache is implemented in NBH, it does not assume the cache must be type-safe. – Andrey B. Panfilov May 07 '23 at 02:55

1 Answers1

0

For cache keys Hibernate may potentially use three different implementations (please check org.hibernate.cache.spi.CacheKeysFactory interface and it's createCollectionKey, createEntityKey and createNaturalIdKey methods), moreover, Hibernate, allows to override default CacheKeysFactory via hibernate.cache.keys_factory setting, so, technically you have no chance to guess what cache key implementation will be used for particular cache region, moreover cache regions may contain different data, so, Object.class is a valid definition of cache key implementation.

In case of cache values implementations may also differ:

  • for natural id cache Hibernate stores entity identifiers there
  • for other caches that can be either Map or StandardCacheEntryImpl or implementation of org.hibernate.cache.spi.support.AbstractReadWriteAccess.Lockable

so, for cache values Object.class is the only valid definition as well.

Andrey B. Panfilov
  • 4,324
  • 2
  • 12
  • 18
  • Thank you for the detailed explanation. Since the question is more general I'll take this chance to add: did you see other issues with how I'm handling this? (That is, the bean, how I obtain the cache manager, mixing L2 cache defintion with Spring cache, the yml, etc). Comprehensive documentation of L2 cache in Spring has been very hard to find for me. – RemasteredDruid May 07 '23 at 03:22
  • 1. caching queries [might be not a good idea](https://dzone.com/articles/pitfalls-hibernate-second-0) - in order to make it work you need to cache entities as well and [enable batch fetching](https://prasanthmathialagan.wordpress.com/2017/04/20/beware-of-hibernate-batch-fetching/). – Andrey B. Panfilov May 07 '23 at 03:58
  • 2. since they have removed `hibernate-ehcache` in version 6 in favour of `jcache`, it seems there are only two options to configure `ehcache`: via `xml` or via `spring`, I don't like both, moreover, I don't like the idea to configure cache via `@Cache` annotation: from my perspective the cache strategy should not depend on domain model - that is a responsibility of particular application, so we configure caches via `org.hibernate.integrator.spi.Integrator` (`metadata.getEntityBindings()` > `persistentClass.setCacheRegionName/setCacheConcurrencyStrategy`) and use our own `RegionFactoryTemplate` – Andrey B. Panfilov May 07 '23 at 04:04