I have recently started working on caching the result from a method. I am using @Cacheable and @CachePut to implement the desired the functionality.
But somehow, the save operation is not updating the cache for findAll method. Below is the code snippet for the same:
@RestController
@RequestMapping(path = "/test/v1")
@CacheConfig(cacheNames = "persons")
public class CacheDemoController {
@Autowired
private PersonRepository personRepository;
@Cacheable
@RequestMapping(method = RequestMethod.GET, path="/persons/{id}")
public Person getPerson(@PathVariable(name = "id") long id) {
return this.personRepository.findById(id);
}
@Cacheable
@RequestMapping(method = RequestMethod.GET, path="/persons")
public List<Person> findAll() {
return this.personRepository.findAll();
}
@CachePut
@RequestMapping(method = RequestMethod.POST, path="/save")
public Person savePerson(@RequestBody Person person) {
return this.personRepository.save(person);
}
}
For the very first call to the findAll method, it is storing the the result in the "persons" cache and for all the subsequent calls it is returning the same result even if the save() operation has been performed in between.
I am pretty new to caching so any advice on this would be of great help.
Thanks!
The simplest way to enable caching behavior for a method is to mark it with @Cacheable and parameterize it with the name of the cache where the results would be stored. It provides a parameter called allEntries that evicts all entries rather than one entry based on the key.
We can enable caching in the Spring Boot application by using the annotation @EnableCaching. It is defined in org. springframework. cache.
It caches entities that are explicitly declared to be cacheable. Upon retrieval of a cacheable entity, the JPA runtime first looks for this entity in the persistence context, then in the entity cache, and finally in the database.
So, a few things come to mind regarding your UC and looking at your code above.
First, I am not a fan of users enabling caching in either the UI or Data tier of the application, though it makes more sense in the Data tier (e.g. DAOs or Repos). Caching, like Transaction Management, Security, etc, is a service-level concern and therefore belongs in the Service tier IMO, where your application consists of: [Web|Mobile|CLI]+ UI -> Service -> DAO (a.k.a. Repo). The advantage of enabling Caching in the Service tier is that is is more reusable across your application/system architecture. Think, servicing Mobile app clients in addition to Web, for instance. Your Controllers for you Web tier may not necessarily be the same as those handling Mobile app clients.
I encourage you to read the chapter in the core Spring Framework's Reference Documentation on Spring's Cache Abstraction. FYI, Spring's Cache Abstraction, like TX management, is deeply rooted in Spring's AOP support. However, for your purposes here, let's break your Spring Web MVC Controller (i.e. CacheDemoController
) down a bit as to what is happening.
So, you have a findAll()
method that you are caching the results for.
WARNING: Also, I don't generally recommend that you cache the results of a
Repository.findAll()
call, especially in production! While this might work just fine locally given a limited data set, theCrudRepository.findAll()
method returns all results in the data structure in the backing data store (e.g. thePerson
Table in an RDBMS) for that particular object/data type (e.g.Person
) by default, unless you are employing paging or some LIMIT on the result set returned. When it comes to caching, always think a high degree of reuse on relatively infrequent data changes; these are good candidates for caching.
Given your Controller's findAll()
method has NO method parameters, Spring is going to determine a "default" key to use to cache the findAll()
method's return value (i.e. List<Person
).
TIP: see Spring's docs on "Default Key Generation" for more details.
NOTE: In Spring, as with caching in general, Key/Value stores (like
java.util.Map
) are the primary implementation's for Spring's notion of aCache
. However, not all "caching providers" are equal (e.g. Redis vs. ajava.util.concurrent.ConcurrentHashMap
, for instance).
After calling the findAll()
Controller method, your cache will have...
KEY | VALUE
------------------------
abc123 | List of People
NOTE: the cache will not store each
Person
in the list individually as a separate cache entry. That is not how method-level caching works in Spring's Cache Abstraction, at least not by default. However, it is possible.
Then, suppose your Controller's cacheable getPerson(id:long)
method is called next. Well, this method includes a parameter, the Person's
ID. The argument to this parameter will be used as the key in Spring's Cache Abstraction when the Controller getPerson(..)
method is called and Spring attempts to find the (possibly existing) value in the cache. For example, say the method is called with controller.getPerson(1)
. Except a cache entry with key 1 does not exist in the cache, even if that Person
(1) is in list mapped to key abc123
. Thus, Spring is not going to find Person
1 in the list and return it, and so, this op results in a cache miss. When the method returns the value (the Person
with ID 1) will be cached. But, the cache now looks like this...
KEY | VALUE
------------------------
abc123 | List of People
1 | Person(1)
Finally, a user invokes the Controller's savePerson(:Person)
method. Again, the savePerson(:Person)
Controller method's parameter value is used as the key (i.e. a "Person
" object). Let's say the method is called as so, controller.savePerson(person(1))
. Well, the CachePut
happens when the method returns, so the existing cache entry for Person
1 is not updated since the "key" is different, so a new cache entry is created, and your cache again looks like this...
KEY | VALUE
---------------------------
abc123 | List of People
1 | Person(1)
Person(1) | Person(1)
None of which is probably what you wanted nor intended to happen.
So, how do you fix this. Well, as I mentioned in the WARNING above, you probably should not be caching an entire collection of values returned from an op. And, even if you do, you need to extend Spring's Caching infrastructure OOTB to handle Collection
return types, to break the elements of the Collection
up into individual cache entries based on some key. This is intimately more involved.
You can, however, add better coordination between the getPerson(id:long)
and savePerson(:Person)
Controller methods, however. Basically, you need to be a bit more specific about your key to the savePerson(:Person)
method. Fortunately, Spring allows you to "specify" the key, by either providing s custom KeyGenerator
implementation or simply by using SpEL. Again, see the docs for more details.
So your example could be modified like so...
@CachePut(key = "#result.id"
@RequestMapping(method = RequestMethod.POST, path="/save")
public Person savePerson(@RequestBody Person person) {
return this.personRepository.save(person);
}
Notice the @CachePut
annotation with the key
attribute containing the SpEL expression. In this case, I indicated that the cache "key" for this Controller savePerson(:Person)
method should be the return value's (i.e. the "#result") or Person
object's ID, thereby matching the Controller getPerson(id:long)
method's key, which will then update the single cache entry for the Person
keyed on the Person's
ID...
KEY | VALUE
---------------------------
abc123 | List of People
1 | Person(1)
Still, this won't handle the findAll()
method, but it works for getPerson(id)
and savePerson(:Person)
. Again, see my answers to the posting(s) on Collection values as return types in Spring's Caching infrastructure and how to handle them properly. But, be careful! Caching an entire Collection of values as individual cache entries could reck havoc on your application's memory footprint, resulting in OOME. You definitely need to "tune" the underlying caching provider in this case (eviction, expiration, compression, etc) before putting a large deal of entires in the cache, particular at the UI tier where literally thousands of requests maybe happening simultaneously, then "concurrency" becomes a factor too! See Spring's docs on sync capabilities.
Anyway, hope this helps aid your understanding of caching, with Spring in particular, as well as caching in general.
Cheers, -John
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With