Ashot Golovenko opened SPR-10958 and commented

Currently you can evict only one or all elements in a single cache region. Allowing something like

@CacheEvict(value = "userCache", key = {"key1", "key2"})

would be really handful.


Affects: 3.2.4

12 votes, 14 watchers

Comment From: spring-projects-issues

Phil Webb commented

Usually the key attribute is a SpEL expression that obtains the key from an input parameter.

e.g.:

@Override
@CacheEvict(value = "default", key = "#p0")
public void evict(Object arg1, Object arg2) {
}

What use-case do you have in mind where two keys would need to be evicted?

Comment From: spring-projects-issues

Ashot Golovenko commented

Well, let's say I have the following logic:

@CacheEvict(value = "userCache", key = "#userId + '/foo1'")
public void updateFoo(long userId, someParams)

@Cacheable(value = "userCache", key = "#userId + '/foo1'")
public void getFoo1(long userId)

@Cacheable(value = "userCache", key = "#userId + '/foo2'")
public void getFoo2(long userId)

The update method changes the database so that calculations of both getFoo1 and getFoo2 methods are obsolete. Now I can evict only one key or all. Ideally I'd really love to have a keymask support so I could evict all keys starting like "#userId*" but array of keys is also fine.

Comment From: spring-projects-issues

Phil Webb commented

You could consider using different cache stores rather than using the key:

@CacheEvict(value = "foo1Cache", key = "#userId")
public void updateFoo(long userId, someParams)

@Cacheable(value = "foo1Cache", key = "#userId")
public void getFoo1(long userId)

@Cacheable(value = "foo2Cache", key = "#userId")
public void getFoo2(long userId)

You can then use:

@CacheEvict(value = {"foo1Cache", "foo2Cache"}, key = "#userId")

Comment From: spring-projects-issues

Lives commented

This would be very helpful in different use cases. For eg : I have a city master which is cached. The service for city master can accept a collection of cities for creation / modification

Since currently cache evict does not support multiple keys , developer ends up with clearing the entire cache. Using SPEL developers can pass the list of keys to be removed from cache.If the cache evict aspect can handle it, it would be very useful.

Comment From: spring-projects-issues

shirish commented

This is a really good feature request. One such use case that I have encountered is to update user messages. Ideally an user can have messages in multiple locales I would like to evict all the messages with key= "{#userid_en, #userid_de}" instead of maintaining a separate cache for each locale.

Also if eviction is possible using regex of the key that would solve this entirely, how ever I am not sure about how big a task this is. some thing like key={"#userid_*"} should evict every thing under that key regex.

Comment From: spring-projects-issues

Stéphane Nicoll commented

My own personal feeling about it is that if you need to do this you probably reached the boundaries of what's safe to do declaratively. If we're talking about multiple keys and spel then it starts to be awkward for several reasons:

  1. We need this to be consistent so we should support that everywhere we support the key attribute (not only cache evict)
  2. If you can do that with SpEL you must be able to do that programmatically. Yet, the KeyGenerator returns an object so we'll have to deal with the return type and handle a collection of something explicitly (which means you can't use that as a single key anywhere even though I doubt someone was doing that)
  3. Going further down that road, things like pattern matching to retrieve the number of elements comes to mind (there is an example above) and we don't want to do that with SpEL.

Since this adds a lot of complexity I want to be sure that this is really needed so please cast your vote (and add your use case please).

Comment From: spring-projects-issues

Johannes Kuhs commented

For more advanced caching scenarios, some concept of having multiple keys is definitely necessary in my opinion. I'm not sure it needs to be in the form of allowing multiple key values though. The key is used for retrieving a cache entry but really we just need a way to evict specific entries. So instead, it might make more sense to add a new evictKeys parameter. evictKeys simply point to the key and can thus be used to evict the actual entry.

Another use-case for this kind of functionality would be the caching of different product information. A product might have skus, images, prices, features, facets, and other information attached to it that require more expensive operations (e.g. looping through all images to find a certain image type + view, getting a specific customer price, creating a distinct set of facets across skus, etc.). It would be nice to be able to cache the results of these kind of operations and simply evict them based on the product ID when the product is updated.

Updating the existing annotations to support this could look like this:

@Cacheable(value = "productImages", key = "#product.id + #imageType", evictKeys = { "#product.id" })
public Image getProductImageOfType(Product product, String imageType)

@CacheEvict(value = "productImages", evictKeys = { "product.id" })
public void updateProduct(Product product)

Comment From: spring-projects-issues

Kyle Lieber commented

We could really use this functionality as well. I think the suggestions made by @jkuhs would work. Here my scenario if it helps:

We have a Policy and a Product.

I have a Policy which is identified by a policyId and each Policy can have multiple Products which are identified by the policyId and the productCode.

So I have a ProductService with a method for getting a product by policyId and productCode.

public class ProductService {
  @Cacheable("products")
  public Product getProduct(Integer policyId, String productCode) {  }
}

Then I have a PolicyService which has a method for deleting a Policy when given the policyId.

public class PolicyService {
  public void deletePolicy(Integer policyId) {  }
}

The PolicyService#deletePolicy will also delete all products for that policy but I have no way to clear the products cache for that policy.

My workaround options are:

  1. Clear all entries in the cache. This is really not an option because this will affect all products for all policies.
  2. Do something really ugly like this:
public class ProductService {
  @Cacheable("products")
  public Product getProduct(Integer policyId, String productCode) {  }

  public List<Product> getAllProducts(Integer policyId) { }

  @CacheEvict("products")
  public void evictFromCache(Integer policyId, String productCode) {
    // this method doesn't do anything and is only here for evicting the cache
  }
}
public class PolicyService {
  public void deletePolicy(Integer policyId) {
    // first delete the policy
    doDelete(policyId);
    // then evict the products
    for (Product product : productService.getAllProducts(policyId)) {
      productService.evictFromCache(policyId, product.code);
    }
  }
}

Comment From: spring-projects-issues

Johannes Kuhs commented

To add to my previous comment, it would be nice to also have a evictKeysGenerator parameter on the Cacheable annotation. It would follow the same concept as keyGenerator but have a return type of String[] (or even Object[] - the point being that multiple evict keys are supported).

I'm really hoping some support for multiple evict keys will be implemented. In it's current form, the cache invalidation is quite limited for more advanced scenarios.

Comment From: spring-projects-issues

Stéphane Nicoll commented

We discussed this internally and decided to reject this feature. This is mostly programming by annotations and it would make the API and implementation much more complex. This idea of a separate KeysGenerator interface for evictions is an obvious example.

While we could implement a simpler solution for some cases, the solution would become inconsistent overall as all the other annotation attributes and interface method handle a single value for the key.

Comment From: spring-projects-issues

Johannes Kuhs commented

It's disappointing to hear that. Would you mind elaborating on why you think this would make the API and implementation "much more complex"? Not sure I understand the argument for a KeysGenerator adding that much complexity. The concept for key generators is not a new one after all.

If this decision is mostly based on not wanting to change the current interfaces, would you consider alternative solutions to support multiple evict keys in some capacity? I haven't thought about alternative solutions a lot, but maybe a separate set of annotations for advanced use, or some hooks that would allow for a more programmatic approach after the logic behind the @Cacheable and @CacheEvict annotations has been executed?

Comment From: spring-projects-issues

Sarath Akula commented

As @jkuhs mentioned, can it be a separate set of annotations for advanced use.It would avoid writing custom code for clearing up the cache in our application.

Comment From: spring-projects-issues

Ranadeep Sharma Hidangmayum commented

Hi Team,

The use cases mentioned by everyone here are very common ones, infact. Here is another usecase -

Let's consider an application where insurance plans for participants belonging to various clients are being read/updated in DB. This application has lots of methods where numerous plan objects have been pumped to cache(s), no matter how many.

@Cacheable(value="participantCache" , key = "{#clientId, #planId, #participantId}")
public List<ParticipantPlanDetails> getParticipantPlanDetails(String clientId, String planId, String participantId)  { ... }

@Cacheable(value="clientCache" , key = "{#clientId, #planId}")
public PlanInfo getEasePlanInfo(String clientId, String planId)  { ... }

Now, I want to delete items from applicable cache(s) where the item's key is matching partially with the collective *clientId * used for deleting all relevant data from DB for a particular client.

@CacheEvict(key = "#clientId")
public void deleteClient(String clientId)  { ... }

In short, I would like the above CacheEvict_to be able to convey to cache handler that all the objects for this specific _clientId be cleared, including the List and PlanInfo objects from participantCache and clientCache respectively.

Comment From: spring-projects-issues

Ranadeep Sharma Hidangmayum commented

Kindly review this feature against my use case (mentioned in comments) for possible enhancement.

Comment From: spring-projects-issues

Stéphane Nicoll commented

As I already explained above, we've made up our mind on this feature and decided not to implement it.

Comment From: spring-projects-issues

Ashok commented

@snicoll,

If rejecting this request is only because many other annotations expect a single key, is it possible have a single, but the key supporting regex (which can match one or more elements)?

Comment From: spring-projects-issues

Ashok commented

Only the evict annotation's key regex matching more than one element in each of the cacheNames

Comment From: themafole

Hi There,

We're still encountering scenarios where this feature / implementation would have been of great importance.

Comment From: sergiocard

Hi There,

We're still encountering scenarios where this feature / implementation would have been of great importance.

I'me facing too: the following is a classic scenario in wich we handle different languages for same event. So Ho we can evict all the entries for the same eventId ?

@Cacheable(value = "EventsCache")
public Response getEvent(int eventId, String language) {
    return service.getEvent(eventId, language);
}

@CacheEvict(value = "EventsCache")
public void evictEvent(int eventId) {}

Comment From: william-aqn

Here's another example: Pagination of entities. (the key will be entityId+currentPage) We need to reset everything with entityId but without currentPage.

How? Need add this construction https://github.com/spring-projects/spring-framework/issues/15586#issuecomment-453405906

Comment From: waitpigfly

Johannes Kuhs commented

For more advanced caching scenarios, some concept of having multiple keys is definitely necessary in my opinion. I'm not sure it needs to be in the form of allowing multiple key values though. The key is used for retrieving a cache entry but really we just need a way to evict specific entries. So instead, it might make more sense to add a new evictKeys parameter. evictKeys simply point to the key and can thus be used to evict the actual entry.

Another use-case for this kind of functionality would be the caching of different product information. A product might have skus, images, prices, features, facets, and other information attached to it that require more expensive operations (e.g. looping through all images to find a certain image type + view, getting a specific customer price, creating a distinct set of facets across skus, etc.). It would be nice to be able to cache the results of these kind of operations and simply evict them based on the product ID when the product is updated.

Updating the existing annotations to support this could look like this:

```java @Cacheable(value = "productImages", key = "#product.id + #imageType", evictKeys = { "#product.id" }) public Image getProductImageOfType(Product product, String imageType)

@CacheEvict(value = "productImages", evictKeys = { "product.id" }) public void updateProduct(Product product) ```

use case like this:

// ex: find products of shop via rpc
@Cacheable(value = "products", key = "#shop.id+ '_' + #type")
public List<Product> findProductsOfType(Shop shop, String type)

// support key & keys: key require a object result, keys require a list of object
@CacheEvict(value = "products", keys = "#root.target.getCacheKeys(#p1)")
public void updateProduct(Product product)

// get multi cache keys related to product
public List<Object> getCacheKeys(Product product){
    Integer shopId = product.getShopId();
    String types = ...;// get types of shop
    List<Object> keys = new LinkedList<>();
    for(String t: types){
        keys.add(String.format("%d_%s", shopId, t));
    }
    return keys;
}

Comment From: LY1806620741

@spring-projects-issues

Hi,My requirement is to cache paginated data with filtering. I think we can build a Cache by Multi-Level cache.

we have code:

@Cacheable(cacheNames = "cacheName")
public GenericPageResult<NoticeBlockListDO> getByFilterThenPage(
                                                          //index params
                                                          Integer params1,
                                                          String params2,
                                                          //filter params
                                                          TypeEnum filter1,
                                                          //Pagination params
                                                          Integer currentPage,
                                                          Integer pageSize
) {
    return ...;
}

@CacheEvict(cacheNames = "cacheName")
public void removeByIndex(Integer params1, String params2){

}

@CacheEvict(cacheNames = "cacheName")
public void removeByFilter(Integer params1, String params2,TypeEnum filter1){

}

@CacheEvict(cacheNames = "cacheName")
public void removeByFilterAndOnePage(Integer params1, String params2,TypeEnum filter,Integer currentPage,Integer pageSize){

}

we can design then use this? define 3 level namespace,then build 3 level Map<String(indexkey),Map>? I'm writing it by hand now, looking forward to strengthening Spring Cache.

@Cacheable(cacheNames = "cacheName",levelkey={
                            "#params1,#params2",
                            "#filter",
                            "#currentPage,#pageSize",
})