Search code examples
javaspringspring-bootcachingspring-cache

How to get individual item by key from cache in Spring cache in Spring Boot?


We have spring-boot-starter-cache added in our project and not using any particular implementation of cache provider. we are loading all the data during application startup by calling following method:

@Override
@Cacheable(cacheNames = "foos")
public List<FooDto> getAllFoo() {
    return fooRepository.findAll().stream()
            .map(FooEntityDomainToDtoMapper::mapDomainToDto) // mapping entity to dto
            .collect(Collectors.toList());
}

//Want to implement something like:
    public FooDto getFoo(Long id) {
    //return single object from foos(which are cached in above method)
    }

It is storing all foos in the cache. And as expected next time when we are calling getAllFoo, it is returning from the cache rather than returning from the database. Now next time when user request for individual object by id, we want to return it from this already cached foos data rather than calling findById() of JPA. Is there any way to achieve this?


Solution

  • Is there any reason you want to, or need to, cache all Foos in your application collectively rather than individually?

    Keep in mind, it is by design that Spring's Cache Abstraction uses the method parameter(s) (if any) as the key and the return value as the value of the cache entry. If the method has no parameters then Spring will generate an id for you.

    I have written about how to customize Spring's CacheManager implementation to cache a Collection of values returned by a @Cacheable method, individually.

    However, for the moment, let's assume you do need/want to cache the entire List of Foos.

    Then, to create a method that pulls an individual Foo by ID from the "cached" List of Foos, you could, given your original cached method in a service class, do, for example...

    @Sevice
    class MyFooService {
    
      private final FooRepository<Foo, Long> fooRepository;
    
      @Cacheable(cacheNames = "foos")
      public List<FooDto> getAllFoos() {
        return this.fooRepository.findAll().stream()
          .map(FooEntityDomainToDtoMapper::mapDomainToDto) // mapping entity to dto
          .collect(Collectors.toList());
      }
    }
    

    Then, in another application component, you could...

    @Component
    class MyFooAccessor {
    
      private final MyFooService fooService;
    
      MyFooAccessor(MyFooService fooService) {
        this.fooService = fooService;
      }
    
      Optional<FooDto> getById(Long id) {
        this.fooService.getAllFoos().stream()
          .filter(fooDto -> fooDto.getId().equals(id))
          .findFirst();
      }
    
      ...
    
    }
    

    The MyFooAccessor makes sure you do not circumvent the caching proxy (i.e. the AOP Proxy + Caching Advice around the MyFooService applied by Spring). If the getById(..) method were a member of the MyFooService class, and called the getAllFoos() method directly, you would circumvent the proxy and caching advice resulting in a database access each time.

    NOTE: You could use Spring AOP Load Time Weaving (LTW) (see doc) to avoid circumventing the caching proxy if you want to keep the getById(:Long) method in the MyFooService class with the getAllFoos(), @Cacheable method. However...

    Typically, you can solve these sort of problems by (re-)structuring your code appropriately, using the proper design pattern. This is not the only solution here, either. That is the beautiful thing about Spring is that it gives you many choices. This is just but 1 choice.

    Hope this helps give you more ideas.