Reputation: 197
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
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
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:
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")
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
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