17

I'm using spring-cache to improve database queries, which works fine as follows:

@Bean
public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("books");
}

@Cacheable("books")
public Book getByIsbn(String isbn) {
    return dao.findByIsbn(isbn);
}

But now I want to prepopulate the full book-cache on startup. Which means I want to call dao.findAll() and put all values into the cache. This routine shall than only be scheduled periodically.

But how can I explicit populate a cache when using @Cacheable?

membersound
  • 81,582
  • 193
  • 585
  • 1,120
  • https://stackoverflow.com/questions/53030289/how-to-add-entire-table-to-cache-in-spring/53048961#53048961 – Mr Nobody Oct 29 '18 at 15:40

7 Answers7

26

Just use the cache as before, add a scheduler to update cache, code snippet is below.

@Service
public class CacheScheduler {
    @Autowired
    BookDao bookDao;
    @Autowired
    CacheManager cacheManager;

    @PostConstruct
    public void init() {
        update();
        scheduleUpdateAsync();
    }

    public void update() {
        for (Book book : bookDao.findAll()) {
            cacheManager.getCache("books").put(book.getIsbn(), book);
        }
    }
}

Make sure your KeyGenerator will return the object for one parameter (as default). Or else, expose the putToCache method in BookService to avoid using cacheManager directly.

@CachePut(value = "books", key = "#book.isbn")
public Book putToCache(Book book) {
    return book;
}
Loki
  • 931
  • 8
  • 13
13

I have encountered the following problem when using @PostConstruct: - even though the method I wanted to be cached was called, after calling it from swagger, it still didn't use the cached value. Only after called it once more.

That was because @PostConstruct is too early for caching something. (At least I think that was the issue)

Now I'm using it more late in the startup process and it works without problems:

@Component
public class CacheInit implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
       //call service method
    }

}
PMerlet
  • 2,568
  • 4
  • 23
  • 39
Andrea Calin
  • 308
  • 2
  • 10
5

An option would be to use the CommandLineRunner for populating the cache on startup.

From official CommandLineRunner documentation, it is an:

Interface used to indicate that a bean should run when it is contained within a SpringApplication.

Hence, we just need to retrieve the list of all available books and then, using CacheManager, we populate the book cache.

@Component
public class ApplicationRunner implements CommandLineRunner {
    @Autowired
    private BookDao dao;

    @Autowired
    private CacheManager cacheManager;

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("books");
    }

    @Override
    public void run(String... args) throws Exception {

        List<Book> results = dao.findAll();

        results.forEach(book -> 
            cacheManager.getCache("books").put(book.getId(), book));
    }
}
mladzo
  • 1,885
  • 1
  • 18
  • 10
2

If having all instances of Book in memory at startup is your requirement than you should store them in some buffer yourself. Putting them in the cache with the findAll() method means that you must annotate findAll() with @Cacheable. Then you would have to call findAll() at startup. But that does not mean that calling getByIsbn(String isbn) will access the cache even if the corresponding instance has been put in the cache when calling findAll(). Actually it won't because ehcache will cache method return value as a key/value pair where key is computed when method is called. Therefore I don't see how you could match the return value of findAll() and return value of getByIsbn(String) because returned types are not the same and moreover key won't never match for all your instances.

Olivier Meurice
  • 554
  • 8
  • 17
2

One way to circumvent the @PostConstruct lack of parameter binding is the following code, with the advantage that it will be executed once the parameters have been initialized:

@Bean
public Void preload(MyDAO dao) {
    dao.findAll();

    return null;
}
ATorras
  • 4,073
  • 2
  • 32
  • 39
  • would you be kind to improve your example, because as such, I don't get the point behind calling `sched.list()` (which does what...?) as well as calling `dao.findAll()` without actually using its result...?! – maxxyme Sep 16 '20 at 14:23
  • Sure! This response is a minor improvement on Loki's accepted response, as you won't need a class utility class (CacheScheduler) to perform the task of cache population. – ATorras Sep 17 '20 at 18:10
1

As Olivier has specified, since spring caches output of function as a single object, using @cacheable notation with findAll will not allow you to load all objects in cache such that they can later be accessed individually.

One possible way you can load all objects in cache is if caching solution being used provides you a way to load all objects at startup. E.g solutions like NCache / TayzGrid provides Cache startup loader feature, that allows you to load cache at startup with objects using a configurable cache startup loader.

Sameer Shah
  • 1,073
  • 7
  • 12
-1

Add another bean BookCacheInitialzer

Autowire the current bean BookService in BookCacheInitialzer

in PostConstruct method of BookCacheInitialzer pseudo code

Then can do something like

class BookService {
    @Cacheable("books")
    public Book getByIsbn(String isbn) {
        return dao.findByIsbn(isbn);
    }
    
    public List<Book> books;

    @Cacheable("books")
    public Book getByIsbnFromExistngBooks(String isbn) {
        return searchBook(isbn, books);
    }
}

class BookCacheInitialzer {

    @Autowired
    BookService  service;

    @PostConstruct
    public void initialize() {
        books = dao.findAll();
        service.books = books;
        for(Book book:books) {
            service.getByIsbnFromExistngBooks(book.getIsbn());
        }
    }
}
maxxyme
  • 2,164
  • 5
  • 31
  • 48
Dheerendra Kulkarni
  • 2,728
  • 1
  • 16
  • 18
  • 1
    in fact, I don't really get the purpose of saving `book` into `service.books` , beyond not understanding the utility of the `searchBook` method... please be more explicit in your example. – maxxyme Sep 16 '20 at 14:21