doragon
doragon

Reputation: 197

Caching on methods in JpaRepository (Spring Data)

Tools: Spring-Boot : 1.5.9.RELEASE Spring-Data-JPA : 1.11.9.RELEASE

Issue: Currently I have a repository that extended from JpaRepository. In order to avoid frequent DB access, I want to cache some of the CRUD methods in the JpaRepository. I tried a few ways from what I can find with Mr.Google but non of them working except one.

EDITED 1. Solution mentioned in this link is workable. However, there is a bad practice (redundancy to me) at here. Imagine if I have 50 repositories extending the JpaRepository, this means that I have to override the save method in 50 repositories.

     public interface UserRepository extends CrudRepository<User, Long> {
        @Override
        @CacheEvict("user")
        <S extends User> S save(S entity);

        @Cacheable("user")
        User findByUsername(String username);
     }

EDITED 2. Extend the JpaRepository interface. I saw something that might works at link2.

In the link, it mentioned 3 different ways to caching the JpaRepository methods. the 1st method is same as what I mentioned in #1. However, I want something similar to 2nd/3rd method so that I no need to keep repeating overriding the CRUD methods in all repositories.

Below is some sample code that I have written.

    @NoRepositoryBean        
    public interface BaseRepository<T, ID extends Serializable> extends 
    JpaRepository<T, ID> {

        @CacheEvict
        <S extends User> S save(S entity);

        @Cacheble
        T findOne(ID id);
    }

    @Repository
    @CacheConfig("user")
    public interface UserRepository extends BaseRepository<User, Integer> {
        // when I calling findOne/save method from UserRepository, it should 
        // caching the methods based on the CacheConfig name defined in the 
        // child class.
    }

However, it seems like the code (above) ain't working as I getting below exception. I understand the issue mainly happened because there is no name being assigned to the cacheable annotation in the BaseRepository. But I would need to cache the CRUD methods in the BaseRepository that extend from JpaRepository.

java.lang.IllegalStateException: No cache could be resolved for 'Builder[public abstract java.util.List com.sdsap.app.repository.BaseRepository.findAll()] caches=[] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'' using resolver 'org.springframework.cache.interceptor.SimpleCacheResolver@30a9fd0'. At least one cache should be provided per cache operation.

I have been asking Mr.Google for few days and yet can't find any suitable solution. I hope someone can help me at here. Sorry if my question isn't clear or missing something as this is my first time posting at here. Thanks!

Upvotes: 10

Views: 29964

Answers (3)

Charly
Charly

Reputation: 1139

This is a great idea. I ended up trying this and getting it to work.

I created a BaseRepository:

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
    @Cacheable(cacheResolver = "cachingConfig")
    Optional<T> findById(UUID id);
    
    @CachePut(cacheResolver = "cachingConfig", key = "#p0.id")
    // Worth noting - add multiple cache puts if caching by different keys(queries)
    // This gets hard when caching special queries per resource - best I've
    //   found so far is to override this method in resource repositories and add all
    //   the puts/evicts needed
    <S extends T> S save(S entity);
}

Note the cacheResolve = "cachingConfig"

Then CachingConfig (CacheResolver) to resolve your cache exception issue:

@Configuration
@EnableCaching
@Log4j2
public class CachingConfig implements CacheResolver {
    private final CacheManager cacheManager;
    private final ObjectMapper objectMapper;

    public CachingConfig(ObjectMapper objectMapper) {
        this.cacheManager = new ConcurrentMapCacheManager();
        this.objectMapper = objectMapper;
    }

    @Bean
    public CacheManager cacheManager() {
        return cacheManager;
    }

    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        Collection<Cache> caches = new ArrayList<>();

        String cacheName = (context.getTarget() instanceof BaseRepository)
                // When BaseRepository, first interface in list is specific Repository Interface
                ? context.getTarget().getClass().getInterfaces()[0].getSimpleName()
                    // I've standardized around all uppercase domain (UserRepository = USER)
                    .replace("Repository", "").toUpperCase(Locale.ROOT)
                // Fallback to class name (you may have different ideas here)
                : context.getTarget().getClass().getSimpleName();

        caches.add(cacheManager.getCache(cacheName));

        return caches;
    }

    // Periodic cache dump - used to see what caches exist and contents when dumped
    @Scheduled(fixedDelay = 10000)
    public void cacheEvict() {
        cacheManager.getCacheNames().forEach(cacheName -> {
            final Cache         cache       = cacheManager.getCache(cacheName);
            if (log.isTraceEnabled()) {
                Map<String, Object> nativeCache = (Map) cache.getNativeCache();
                nativeCache.forEach((k, v) -> {
                    try {
                        log.trace(String.format("Clearing %s:%s:%s", cacheName, k, objectMapper.writeValueAsString(v)));
                    } catch (JsonProcessingException e) {
                        log.trace("Error", e);
                    }
                });
            }
            Objects.requireNonNull(cache).clear();
        });
    }
}

Example repositories:

@Repository
public interface UserRepository extends BaseRepository<User, Long> {
}
@Repository
public interface TestRepository extends BaseRepository<Test, Long> {
}

Example log statement from the dump:

14:06:46.033 [scheduling-1] TRACE CachingConfig - Clearing USER:1:{"id":1,"firstName":"test","lastName":"user"}
14:06:46.033 [scheduling-1] TRACE CachingConfig - Clearing TEST:5:{"id":5,"cool":true}

Upvotes: 1

Mehraj Malik
Mehraj Malik

Reputation: 15854

I am assuming that you have required configuration already set up and the stack trace you have posted is the problem. So let's dig it.

There are two problems I see:

  1. java.lang.IllegalStateException: No cache could be resolved, At least one cache should be provided per cache operation.

    Resolution: Whenever you want to cache the data or evict the data you MUST provide the name of the cache, which I don't see provided in your code.

    @Cacheable's cacheNames or value should be defined in order to get the cache working.

    Example : @Cacheable(value = "usersCache")

  2. The proper cache key

    Because cache works on key-value pair, you should provide a proper cache key. If you don't provide the cache key then, by default, a default key generation strategy that creates a SimpleKey that consists of all the parameters with which the method was called.

Suggestion: You should provide the cache key manually.

Example :

@Cacheable(value = "usersCache", key = "#username")
User findByUsername(String username);

Note: Make sure username is unique because cache key must be unique.

You can read more Spring cache annotations: some tips & tricks

Upvotes: 4

Shanu Gupta
Shanu Gupta

Reputation: 3807

Use @CachedResult on method you want to cache.

In your main class use @EnableCaching.

Sample code: Main class

@SpringBootApplication
@EnableCaching
@RestController
public class SpringBootCacheApplication {

    @Autowired
    SomeBean someBean;

    @RequestMapping(value = "/cached/{key}")
    public int getCachedMethod(@PathVariable("key") String key) {
        System.out.println("Got key as " + key);
        return someBean.someCachedResult(key);
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringBootCacheApplication.class, args);
    }
}

SomeBean class containig method which I wish to cache

@Component
public class SomeBean {

    @CacheResult
    public int someCachedResult(String key) {
        System.out.println("Generating random number");
        int num = new Random().nextInt(200);
        return num;
    }

}

In the someCachedResult method I'm always returning some random value. Since its cached, you'll get random value the first time only.

Here the SomeBean should correspond to your CachingUserRepository class.

Upvotes: 0

Related Questions