How update/remove an item already cached within a collection of items
Solution 1
Caching the collection using the caching abstraction is a duplicate of what the underlying caching system is doing. And because this is a duplicate, it turns out that you have to resort to some kind of duplications in your own code in one way or the other (the duplicate key for the set is the obvious representation of that). And because there is duplication, you have to sync state somehow
If you really need to access to the whole set and individual elements, then you should probably use a shortcut for the easiest leg. First, you should make sure your cache contains all elements which is not something that is obvious. Far from it actually. Considering you have that:
//EhCacheCache cache = (EhCacheCache) cacheManager.getCache("products");
@Override
public Set<Product> findAll() {
Ehcache nativeCache = cache.getNativeCache();
Map<Object, Element> elements = nativeCache.getAll(nativeCache.getKeys());
Set<Product> result = new HashSet<Product>();
for (Element element : elements.values()) {
result.add((Product) element.getObjectValue());
}
return Collections.unmodifiableSet(result);
}
The elements
result is actually a lazy loaded map so a call to values()
may throw an exception. You may want to loop over the keys or something.
You have to remember that the caching abstraction eases the access to the underlying caching infrastructure and in no way it replaces it: if you had to use the API directly, this is probably what you would have to do in some sort.
Now, we can keep the conversion on SPR-12036 if you believe we can improve the caching abstraction in that area. Thanks!
Solution 2
I think something like this schould work... Actually it's only a variation if "Stéphane Nicoll" answer ofcourse, but it may be useful for someone. I write it right here and haven't check it in IDE, but something similar works in my Project.
-
Override CacheResolver:
@Cacheable(value="products", key="#root.target.PRODUCTS", cacheResolver = "customCacheResolver")
-
Implement your own cache resolver, which search "inside" you cached items and do the work in there
public class CustomCacheResolver implements CacheResolver{ private static final String CACHE_NAME = "products"; @Autowired(required = true) private CacheManager cacheManager; @SuppressWarnings("unchecked") @Override public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> cacheOperationInvocationContext) { // 1. Take key from query and create new simple key SimpleKey newKey; if (cacheOperationInvocationContext.getArgs().length != null) { //optional newKey = new SimpleKey(args); //It's the key of cached object, which your "@Cachable" search for } else { //Schould never be... DEFAULT work with cache if something wrong with arguments return new ArrayList<>(Arrays.asList(cacheManager.getCache(CACHE_NAME))); } // 2. Take cache EhCacheCache ehCache = (EhCacheCache)cacheManager.getCache(CACHE_NAME); //this one we bringing back Ehcache cache = (Ehcache)ehCache.getNativeCache(); //and with this we working // 3. Modify existing Cache if we have it if (cache.getKeys().contains(newKey) && YouWantToModifyIt) { Element element = cache.get(key); if (element != null && !((List<Products>)element.getObjectValue()).isEmpty()) { List<Products> productsList = (List<Products>)element.getObjectValue(); // ---**--- Modify your "productsList" here as you want. You may now Change single element in this list. ehCache.put(key, anfragenList); //this method NOT adds cache, but OVERWRITE existing // 4. Maybe "Create" new cache with this key if we don't have it } else { ehCache.put(newKey, YOUR_ELEMENTS); } return new ArrayList<>(Arrays.asList(ehCache)); //Bring all back - our "new" or "modified" cache is in there now... }
Read more about CRUD of EhCache: EhCache code samples
Hope it helps. And sorry for my English:(
Solution 3
I think there is a way to read the collection from underlying cache structure of spring. You can retrieve the collection from underlying ConcurrentHashMap as key-value pairs without using EhCache or anything else. Then you can update or remove an entry from that collection and then you can update the cache too. Here is an example that may help:
import com.crud.model.Post;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
@Slf4j
public class CustomCacheResolver implements CacheResolver {
private static final String CACHE_NAME = "allPost";
@Autowired
private CacheManager cacheManager;
@SuppressWarnings("unchecked")
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> cacheOperationInvocationContext) {
log.info(Arrays.toString(cacheOperationInvocationContext.getArgs()));
String method = cacheOperationInvocationContext.getMethod().toString();
Post post = null;
Long postId = null;
if(method.contains("update")) {
//get the updated post
Object[] args = cacheOperationInvocationContext.getArgs();
post = (Post) args[0];
}
else if(method.contains("delete")){
//get the post Id to delete
Object[] args = cacheOperationInvocationContext.getArgs();
postId = (Long) args[0];
}
//read the cache
Cache cache = cacheManager.getCache(CACHE_NAME);
//get the concurrent cache map in key-value pair
assert cache != null;
Map<SimpleKey, List<Post>> map = (Map<SimpleKey, List<Post>>) cache.getNativeCache();
//Convert to set to iterate
Set<Map.Entry<SimpleKey, List<Post>>> entrySet = map.entrySet();
Iterator<Map.Entry<SimpleKey, List<Post>>> itr = entrySet.iterator();
//if a iterated entry is a list then it is our desired data list!!! Yayyy
Map.Entry<SimpleKey, List<Post>> entry = null;
while (itr.hasNext()){
entry = itr.next();
if(entry instanceof List) break;
}
//get the list
assert entry != null;
List<Post> postList = entry.getValue();
if(method.contains("update")) {
//update it
for (Post temp : postList) {
assert post != null;
if (temp.getId().equals(post.getId())) {
postList.remove(temp);
break;
}
}
postList.add(post);
}
else if(method.contains("delete")){
//delete it
for (Post temp : postList) {
if (temp.getId().equals(postId)) {
postList.remove(temp);
break;
}
}
}
//update the cache!! :D
cache.put(entry.getKey(),postList);
return new ArrayList<>(Collections.singletonList(cacheManager.getCache(CACHE_NAME)));
}
}
Here are the methods that uses the CustomCacheResolver
@Cacheable(key = "{#pageNo,#pageSize}")
public List<Post> retrieveAllPost(int pageNo,int pageSize){ // return list}
@CachePut(key = "#post.id",cacheResolver = "customCacheResolver")
public Boolean updatePost(Post post, UserDetails userDetails){ //your logic}
@CachePut(key = "#postId",cacheResolver = "customCacheResolver")
public Boolean deletePost(Long postId,UserDetails userDetails){ // your logic}
@CacheEvict(allEntries = true)
public Boolean createPost(String userId, Post post){//your logic}
Hope it helps to manipulate your spring application cache manually!
Manuel Jordan
Updated on August 03, 2022Comments
-
Manuel Jordan almost 2 years
I am working with Spring and EhCache
I have the following method
@Override @Cacheable(value="products", key="#root.target.PRODUCTS") public Set<Product> findAll() { return new LinkedHashSet<>(this.productRepository.findAll()); }
I have other methods working with @Cacheable and @CachePut and @CacheEvict.
Now, imagine the database returns 100 products and they are cached through
key="#root.target.PRODUCTS"
, then other method would insert - update - deleted an item into the database. Therefore the products cached through thekey="#root.target.PRODUCTS"
are not the same anymore such as the database.I mean, check the two following two methods, they are able to update/delete an item, and that same item is cached in the other
key="#root.target.PRODUCTS"
@Override @CachePut(value="products", key="#product.id") public Product update(Product product) { return this.productRepository.save(product); } @Override @CacheEvict(value="products", key="#id") public void delete(Integer id) { this.productRepository.delete(id); }
I want to know if is possible update/delete the item located in the cache through the
key="#root.target.PRODUCTS"
, it would be 100 with the Product updated or 499 if the Product was deleted.My point is, I want avoid the following:
@Override @CachePut(value="products", key="#product.id") @CacheEvict(value="products", key="#root.target.PRODUCTS") public Product update(Product product) { return this.productRepository.save(product); } @Override @Caching(evict={ @CacheEvict(value="products", key="#id"), @CacheEvict(value="products", key="#root.target.PRODUCTS") }) public void delete(Integer id) { this.productRepository.delete(id); }
I don't want call again the 500 or 499 products to be cached into the
key="#root.target.PRODUCTS"
Is possible do this? How?
Thanks in advance.
-
Manuel Jordan almost 10 yearsThank You Stephane, I am going keep the conversation through JIRA.