26

I'm trying to add caching to a CRUD app, I started doing something like this:

@Cacheable("users")
List<User> list() {
    return userRepository.findAll()
}
@CachePut(value = "users", key = "#user.id") 
void create(User user) {
    userRepository.create(user)
}
@CachePut(value = "users", key = "#user.id") 
void update(User user) {
    userRepository.update(user)
}
@CacheEvict(value = "users", key = "#user.id") 
void delete(User user) {
    userRepository.delete(user)
}

The problem I have is that I would like that create/update/delete operations can update the elements already stored in the cache for the list() operation (note that list() is not pulling from database but an data engine), but I am not able to do it.

I would like to cache all elements returned by list() individually so all other operations can update the cache by using #user.id. Or perhaps, make all operations to update the list already stored in cache.

I read that I could evict the whole cache when it is updated, but I want to avoid something like:

@CacheEvict(value = "users", allEntries=true) 
void create(User user) {
    userRepository.create(user)
}

Is there any way to create/update/remove values within a cached collection? Or to cache all values from a collection as individual keys?

John Blum
  • 7,381
  • 1
  • 20
  • 30
Federico Piazza
  • 30,085
  • 15
  • 87
  • 123
  • 1
    I think that should be done on the Hibernate level (if you use it). Hibernate knows about single entities, while cache abstraction does not - these are just some return values for it to cache. With Hibernate, you could try using Query Cache for this purpose. – Rafal G. Jul 09 '18 at 05:43
  • hey @RafalG. we are not using hibernate, we have a heavy engine that takes few seconds. – Federico Piazza Jul 09 '18 at 13:37

3 Answers3

12

I'll self answer my question since no one gave any and could help others.

The problem I had when dealing with this issue was a problem of misconception of Cache usage. My need posted on this question was related to how to update members of a cached list (method response). This problem cannot be solved with cache, because the cached value was the list itself and we cannot update a cached value partially.

The way I wanted to tackle this problem is related to a "map" or a distributed map, but I wanted to use the @Cacheable annotation. By using a distributed map would have achieved what I asked in my question without using @Cacheable. So, the returned list could have been updated.

So, I had (wanted) to tackle this problem using @Cacheable from other angle. Anytime the cache needed to update I refreshed it with this code.

I used below code to fix my problem:

@Cacheable("users")
List<User> list() {
    return userRepository.findAll()
}
// Refresh cache any time the cache is modified
@CacheEvict(value = "users", allEntries = true") 
void create(User user) {
    userRepository.create(user)
}
@CacheEvict(value = "users", allEntries = true") 
void update(User user) {
    userRepository.update(user)
}
@CacheEvict(value = "users", allEntries = true") 
void delete(User user) {
    userRepository.delete(user)
}

In addition, I have enabled the logging output for spring cache to ensure/learn how the cache is working:

# Log Spring Cache output
logging.level.org.springframework.cache=TRACE
Yennefer
  • 5,704
  • 7
  • 31
  • 44
Federico Piazza
  • 30,085
  • 15
  • 87
  • 123
  • I tried what you did but my problem still occurs. When evicting cache by giving value name, does spring delete all the cache and put or modify the new one ? – Alpcan Yıldız Dec 16 '19 at 20:19
  • @AlpcanYıldız when you evict it will delete the `key` inside the cache. I have updated my answer to use `allEntries` this will refresh the whole cache, so next time my findAll is called it will be cached again. – Federico Piazza Dec 16 '19 at 20:27
  • so basically I dont want to delete all my cache, you are deleting all the entries not updating new current cache with the new entry :/ – Alpcan Yıldız Dec 16 '19 at 20:28
  • @AlpcanYıldız exactly, that's the point. You cannot update a cached method response, you must refresh the whole cached resource, you are thinking like a hash map. When your cached response needs to be updated, you need to update everything. You can evict and insert specific keys, but you want to update a response cached list. Bear in min that you can cache findById methods easily and evict it easily as well, but for the case of a list response you cannot do this, because the whole cached resourced is the list itself – Federico Piazza Dec 16 '19 at 20:31
  • Thank you but we can using cacheManager, take a look at this https://stackoverflow.com/questions/35826385/how-to-update-spring-cache-partialllyone-field-onlywhen-object-mutates – Alpcan Yıldız Dec 16 '19 at 20:43
  • @AlpcanYıldız that question is evicting the key (deleting) and re-inserting. It's the same approach I took – Federico Piazza Dec 16 '19 at 20:46
  • Are you sure because you are deleting "allEntries = true" not the specific key in the cache – Alpcan Yıldız Dec 16 '19 at 20:49
  • @AlpcanYıldız for my specific case, I have used a `cache` named `users` and it had a unique cached value that was the list. I only wanted to cache the `findAll` response. I'm not sure if your case is the same as mine. – Federico Piazza Dec 16 '19 at 20:51
  • Okay seems like we have different cases. Thank you for your time – Alpcan Yıldız Dec 16 '19 at 21:02
  • what will be done, when list(String param) has parameter. so, you can give #param, but add time how to evict if create having param of same value. listAll(String group), you can give #group as key. and for add() you need to evict particular list based on group of list so, if I add element of group1, then #group having group1 only should evict. Is there anyway to achieve? – Satish Patro Jul 21 '20 at 10:08
2

Not Sure if, using Spring's @Cacheable is a hard constraint for you, but this essentially worked for me.

I tried using Spring's RedisTemplate and Redis HasMap data-structure for storing the list elements.

Store a single User:

redisTemplate.opsForHash().put("usersRedisKey" ,user.id,user);

Storing List of Users:

Need to map this with user.id first

Map<Long, User> userMap = users.stream()
                .collect(Collectors.toMap(User::getId, Function.identity()));

    redisTemplate.opsForHash().putAll("usersRedisKey", userMap);

Get single user from Cache:

redisTemplate.opsForHash().get("usersRedisKey",user.id);

Get list of users:

redisTemplate.opsForHash().multiGet("usersRedisKey", userIds); //userIds is List of ids

Delete user from List:

redisTemplate.opsForHash().delete("usersRedisKey",user.id);

Similarly you could try using other operations from Redis HashMap to update individual objects based on ids.

I understand I am quite late to the party here, but do let me know if this works for you.

1

Try below given solution:

  @Caching(put = @CachePut(cacheNames = "product", key = "#result.id"),
            evict = @CacheEvict(cacheNames = "products", allEntries = true))
    public Product create(ProductCreateDTO dto) {
        return repository.save(mapper.asProduct(dto));
    }
    
    @Caching(put = @CachePut(cacheNames = "product", key = "#result.id"),
            evict = @CacheEvict(cacheNames = "products", allEntries = true))
    public Product update(long id, ProductCreateDTO dto) {
        return repository.save(mapper.merge(dto, get(id)));
    }

    @Caching(evict = {
            @CacheEvict(cacheNames = "product", key = "#result.id"),
            @CacheEvict(cacheNames = "products", allEntries = true)
    })
    public void delete(long id) {
        repository.delete(get(id));
    }

    @Cacheable(cacheNames = "product", key = "#id")
    public Product get(long id) {
        return repository.findById(id).orElseThrow(() -> new RuntimeException("product not found"));
    }

    @Cacheable(cacheNames = "products", key = "#pageable")
    public Page<Product> getAll(Pageable pageable) {
        return repository.findAll(pageable);
    }
Rohan Lodhi
  • 1,385
  • 9
  • 24
xtreme
  • 19
  • 3