'Spring Cache with collection of items/entities
I am using Spring Cache, where I pass in a collection of keys, and the return is a list of entities. I would like to have the caching framework understand that each element in the return list is to be cached with the corresponding code. At the moment, it seems that the key is the whole list, and if I am missing a key in the subsequent call, it'll try to reload the whole collection again.
@Override
@Cacheable(value = "countries")
public List<Country> getAll(List<String>codes) {
return countryDao.findAllInCodes(codes);
}
another possibility is that the return is a map, similarly I would like the cache to be intelligent enough to only query for items that were never queried before, also to cache them each item with its key.
@Override
@Cacheable(value = "countries")
public Map<String,Country> getAllByCode(List<String>codes) {
return countryDao.findAllInCodes(codes);
}
Suppose the country class looks like this:
class Country{
String code;
String fullName;
long id;
... // getters setters constructurs etc..
}
Is this possible with Spring Cache?
Solution 1:[1]
In fact, it is possible, even with Spring's Caching Abstraction, but not out-of-the-box (OOTB). Essentially, you must customize Spring's caching infrastructure (Explained further below)
By default, Spring's caching infrastructure uses the entire @Cacheable method parameter arguments as the cache "key", as explained here. Of course you can also customize the key resolution using either a SpEL Expression or with a custom KeyGenerator implementation, as explained here.
Still, that does not break up the collection or array of parameter arguments along with the @Cacheable method's return value into individual cache entries (i.e. key/value pairs based on the array/collection or Map).
For that, you need a custom implementation of Spring's CacheManager (dependent on your caching strategy/provider) and Cache interfaces.
NOTE: Ironically, this will be the 3rd time I have answered nearly the same question, first here, then here and now here, :-). Anyway...
I have updated/cleaned up my example (a bit) for this posting.
Notice that my example extends and customizes the ConcurrentMapCacheManager provided in the Spring Framework itself.
Theoretically, you could extend/customize any CacheManager implementation, like Redis's in Spring Data Redis, here (source), or Pivotal GemFire's CacheManager in Spring Data GemFire, here (source). The open source version of Pivotal GemFire is Apache Geode, which has a corresponding Spring Data Geode project, (source for CacheManager in Spring Data Geode, which is basically identical to SD GemFire). Of course, you can apply this technique to other caching providers... Hazelcast, Ehcache, etc.
However, the real guts of the work is handled by the custom implementation (or mores specifically, the base class) of Spring's Cache interface.
Anyway, hopefully from my example, you will be able to figure out what you need to do in your application to satisfy your application's caching requirements.
Additionally, you can apply the same approach to handling Maps, but I will leave that as an exercise for you, ;-).
Hope this helps!
Cheers, John
Solution 2:[2]
With @CachePut and a helper method you can achieve it very simply like this :
public List<Country> getAllByCode(List<String>codes) {
return countryDao.findAllInCodes(codes);
}
public void preloadCache(List<String>codes) {
List<Country> allCountries = getAllByCode(codes);
for (Country country : allCountries) {
cacheCountry(country);
}
}
@CachePut
public Country cacheCountry(Country country) {
return country;
}
Note
This will only add values to the cache, but never remove old values. You can easily do cache eviction before you add new values
Option 2
There is a proposal to make it work like this :
@CollectionCacheable
public List<Country> getAllByCode(List<String>codes) {
See :
- https://github.com/spring-projects/spring-framework/issues/23221
- https://github.com/qaware/collection-cacheable-for-spring
If you are impatient take the code from GitHub and integrate locally
Solution 3:[3]
Why not caching you list as a String?
@Cacheable(value = "my-cache-bucket:my-id-parameters", key = "{#id, #parameters}")
getMy(UUID id, String parameters) { ... }
Use like:
getMy(randomUUID(), parametersList.toString());
Your cache key will look like:
"my-cache-bucket:my-id-parameters::3ce42cd9-99d4-1d6e-a657-832b4a982c72,[parameterValue1,parameter-value2]
Solution 4:[4]
I found two native workarround methodologies to use a complex collection values as a cache key. The first method is using a computed string as the cache key:
@Cacheable(value = "Words", key = "{#root.methodName, #a1}", unless = "#result == null")
//or
@Cacheable(value = "Words", key = "{#root.methodName, #p1}", unless = "#result == null")
//or
@Cacheable(value = "Words", key = "{#root.methodName, #precomputedString}", unless = "#result == null")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) {
}
In order to call to this method service as follows:
//use your own complex object collection to string mapping as a second parammeter
service.findWords(request.getParameterMap().values(),request.getParameterMap()
.values()
.stream()
.map(strings -> Arrays.stream(strings)
.collect(Collectors.joining(",")))
.collect(Collectors.joining(","));)
And the second methodology (my prefered form):
@Cacheable(value = "Edges", key = "{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}", unless = "#result == null")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) {
}
Where package.relationalDatabase.utils.Functions getSpringCacheKey is a own created function as follows:
public static String getSpringCacheKey(Object o) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
boolean isSpringEntity = o.getClass().getAnnotation(javax.persistence.Entity.class) != null;
if (isSpringEntity) {
return objectMapper.writerWithView(JSONViews.Simple.class).writeValueAsString(o);
} else {
return objectMapper.writeValueAsString(o);
}
}
Note I: this methodology allow the combination of the native key cache notation with a custom wrapper. Unlike Spring cache's keyGenerator property which does not allow the key annotation (they are mutually exclusive) and wich require the creatation of a CustomKeyGenerator
@Cacheable(value = "Edges", unless = "#result == null", keyGenerator = "CustomKeyGenerator")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) {
}
////////
public class CustomKeyGenerator implements KeyGenerator {
Object generate(Object target, Method method, Object... params)
}
And the creation of a return wrapper for each complex collection key. For example:
@Override
public Object generate(Object target, Method method, Object... params) {
if(params[0] instanceof Collection)
//do something
if(params[0] instanceof Map)
//do something
if(params[0] instanceof HttpServletRequest)
//do something
}
Therefore, the proposed methodology allows:
//note #request.getParameterMap().values()
@Cacheable(value = "Edges", key = "{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}"
//note #request.getParameterMap().keySet()
@Cacheable(value = "Edges", key = "{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().keySet())}"
without need to update the method for each collection.
Note II: This methodology alow the usage of jackson views for spring entities but in some cases there is needed the @JsonIgnoreProperties({"hibernateLazyInitializer"}) annotation.
Finally the trace result of spring cache for this methodology is the following:
Computed cache key '[findWords, [[""],["0"],[""],[""],[""],[""],["brazil"],["on"],["false"]]]' for operation Builder[public java.util.List package.relationalDatabase.services.myClass.find(javax.servlet.http.HttpServletRequest)] caches=[myClass] | key='{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='#result == null' | sync='false'
On the other hand, it is recommended to use string hash functions to compress the resulting key value.
In order to avoid problems with the T() function in the JAR package lifecycle it is preferable to create a bean:
@Bean
KeySerializationComponent keySerializationComponent() {
return new KeySerializationComponent();
}
and call it with:
@Cacheable(value = "Document", key = "{#root.methodName,#size,@keySerializationComponent.getSpringCacheKey(#ids)}", unless = "#result == null")
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Pino |
| Solution 3 | Zon |
| Solution 4 |
