Spring #Cacheable for method no parameter - spring

I want to cache some db data. for example Cache Customer and use customer.id as the key.
How could I set the key if I want to load all customers (allCustomer() in the code) ?
#Cacheable(value = "customer", key = "#customerID")
public Customer getCustomer(Long customerID) {
return getCustomerData(customerID);
}
// How to setup this key?
#Cacheable(value = "customer", key = "?")
public List<Customer> allCustomer(){
return db.values().stream().collect(Collectors.toList());
}
#CachePut(value = "customer", key = "#customer.id")
public void updateCustomer(Customer customer){
db.put(customer.getId(), customer);
}
#CacheEvict(value = "customer", key = "#customerID")
public void deleteCustomer(Long customerID){
db.remove(customerID);
}

I would recommend using #CachePut instead of #Cacheable. In the case that a new entry is added to the DB from outside of this application instance, the cache would not contain that new value.
You can use #result.id to tell Spring which value to use as a key and I've included a conditional so that you don't get strange errors in case of a null value.
#CachePut(value = "customer", key = "#result.id", condition = "#result != null")

It's impossible to do it for collections with the Spring's annotations - with #Cacheable you'd have just one element in a cache with a computed key and a value with the whole list inside.
If performance is not that important in your app, use getCustomer(...) in a loop.
Otherwise, you'll need to update your cache manually. Unfortunately, Cache interface doesn't provide a method to retrieve all keys/values/key-value pairs from a cache, so a bit of casting is required.
The example for the default in-memory cache (spring.cache.type=simple):
#Autowired
private org.springframework.CacheManager cacheManager;
public List<Customer> allCustomers() {
ConcurrentMap<Long, Customer> customerCache = (ConcurrentMap<Long, Customer>)
cacheManager.getCache("customer").getNativeCache();
if (!customerCache.isEmpty()) {
return new ArrayList<>(customerCache.values());
}
List<Customer> customers = db.values().stream().collect(Collectors.toList());
customers.forEach(customer -> customerCache.put(customer.getId(), customer));
return customers;
}
Or for spring.cache.type=jcache with backed EhCache 3:
#Autowired
private org.springframework.CacheManager cacheManager;
public List<Customer> allCustomers() {
javax.cache.Cache<Long, Customer> customerCache = (javax.cache.Cache<Long, Customer>)
cacheManager.getCache("customer").getNativeCache();
Iterator<Cache.Entry<Long, Customer>> iterator = customerCache.iterator();
List<Customer> cachedCustomers = new ArrayList<>();
while (iterator.hasNext()) {
Cache.Entry<Long, Customer> entry = iterator.next();
cachedCustomers.add(entry.getValue());
}
if (!cachedCustomers.isEmpty()) {
return cachedCustomers;
}
List<Customer> customers = db.values().stream().collect(Collectors.toList());
customers.forEach(customer -> customerCache.put(customer.getId(), customer));
return customers;
}
The same can be done similarly for any other cache type (redis, hazelcast, caffeine etc.).
The corresponding eviction method can be written much easier:
#CacheEvict(value = "customer", allEntries = true)
public void deleteAllCustomers(){
db.removeAll(); //pseudocode
}

Related

Spring caching eviction

I am having trouble with understanding spring #cacheEvict annotation. Does it a method to trigger cache eviction? Please see below.
#CacheEvict
public void clearEmployeeById(int id) {
//Do we have to add a method as trigger here in order to trigger the cache eviction? or can we leave this method without implementation
}
``
You need to specify the cache name in #CacheEvict
#CacheEvict(value = {"employee"}, allEntries = true)
public void clearEmployeeById(int id) {
//Logic
}
Cache employee will get evicted, Whenever clearEmplyoeeByID() method gets invoked.
To add cache into employee
#Cacheable(value = {"employee"})
public Employee addEmployeeById(int id) {
//Logic
return employee;
}
To clear all cache, you need to use CacheManager
#Autowired
private CacheManager cacheManager;
#Scheduled(fixedDelay = 86400000)
public boolean evictAllCaches() {
cacheManager.getCacheNames().stream().forEach(c -> cacheManager.getCache(c).clear());
return true;
}

Spring Cache property extraction

I have two methods for which i have use #Cacheable annotation.
#Cacheable(value = "product", key = "#id")
public Product getProduct(String id) {
Product product = productRepository.getProductById(id)
return product;
}
and the second method is,
#Cacheable(value = "product", key="#id")
public Integer getProductType(String id) {
return getProduct(id).getProductType();
}
The second method does not work. How can i extract only a property from the "product" cache? In this case, already product for that specific id is cached. i just need only the property from the cache. I am using Hazelcast as Cache provider.
If you want to share the cache between two methods, I think you should do the following:
#Resource
private Service self;
#Cacheable(value = "product", key = "#id")
public Product getProduct(String id) {
Product product = productRepository.getProductById(id)
return product;
}
public Integer getProductType(String id) {
return self.getProduct(id).getProductType();
}
Here is the complete working example that works.

Spring Cache Abstraction with Hazelcast Doesn't Evict Key From Cache

With the following configuration, my return object is cached but when I try to evict a key manually it doesnt't work.
#Configuration
#EnableCaching
public class HazelCastConfiguration {
#Bean
public HazelcastCacheManager hazelcastCacheManager() {
return new HazelcastCacheManager(Hazelcast.newHazelcastInstance(hazelcastConfig()));
}
#Bean
public Config hazelcastConfig() {
return new Config()
.setInstanceName("hazelcast-instance")
.addMapConfig(new MapConfig()
.setName("myCache")
.setMaxSizeConfig(new MaxSizeConfig())
.setEvictionPolicy(EvictionPolicy.LRU)
.setStatisticsEnabled(true)
.setTimeToLiveSeconds(-1));
}
}
Cached method:
#Override
#Cacheable(value = "myCache", unless = "#result == null", key = "{#someString, #someLong, #someInteger}")
public List<MyReturnObject> methodWithCachedResults (String someString, Long someLong, Integer someInteger) {
//my logic
}
A sample helper method:
public void evictKey(String aString, Long aLong, Integer anInteger) {
IMap<Object, Object> hazelcastCache = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("myCache");
hazelcastCache.evict(Arrays.asList(aString, aLong, anInteger));
logger.info("{}", hazelcastCache.keySet());
}
When I trigger the method above, it logs the key even though I force the key to be evicted.
The result is the same when I try with the CacheManager :
#Autowired
private HazelcastCacheManager cacheManager;
public void evictKey(String aString, Long aLong, Integer anInteger) {
cacheManager.getCache("myCache").evict(Arrays.asList(aString, aLong, anInteger));
}
However if I try this, it clears the whole cache which it obviously states:
public void evictKey(String aString, Long aLong, Integer anInteger) {
IMap<Object, Object> hazelcastCache = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("myCache");
hazelcastCache.clear();
}
By the way, checking keySet().contains(Arrays.asList...) returns true.
It's far from obvious, but there's two implementations of List here.
#Cacheable will create an instance of java.util.ArrayList.
Arrays.asList will create an instance of java.util.Arrays.ArrayList.
This should make it clearer:
public void evictKey(String aString, Long aLong, Integer anInteger) {
IMap<Object, Object> hazelcastCache = Hazelcast.getHazelcastInstanceByName("hazelcast-instance").getMap("myCache");
java.util.List<Object> keyToEvict = Arrays.asList(aString, aLong, anInteger);
boolean success = hazelcastCache.evict(Arrays.asList(aString, aLong, anInteger));
logger.info("Evicted {}, {} == {}", keyToEvict, keyToEvict.getClass(), success);
for (Object key : hazelcastCache.keySet()) {
logger.info("Remaining key {}, {}", key, key.getClass());
}
}

Caffeine cache refresh / reload cache manually or on demand

I have implemented caffeine cache in my application. I am caching data from few static tables. But i want to know if i can refresh / clear / reload cache manually or on demand using a REST API or any other way.
Can any one please suggest a way to implement such a requirement.
I want something like :-
an endpoint url like :- http://localhost:8080/refreshCache
this will trigger some method internally and clear the cache or reload new values in cache manually.
Below is the cache configuration:
#Configuration
public class CacheConfig{
private com.github.benmanes.caffeine.cache.Cache<Object, Object> cache;
#Bean
Caffeine<Object,Object> cacheBuilder(){
return Caffeine.newBuilder()
.initialCapacity(300)
.maximumSize(50000)
.expireAfterAccess(1, TimeUnit.DAYS)
.removalListener(new CacheRemovalListener())
.recordStats();
}
class CacheRemovalListener implements RemovalListener<Object, Object> {
#Override
public void onRemoval(Object key, Object value, RemovalCause cause) {
System.out.format("Removal listener called with key [%s], cause[%s], evicted [%s] %n",
key , cause.toString(), cause.wasEvicted());
}
}
}
You can use Spring's CacheManager to create CaffeineCache instances and then you can perform CRUD operations on any cache using CacheManager.
See Below code.
Bean Configuration:
public class CacheBeansConfig {
#Bean
public CacheManager cacheManager() {
// create multiple instances of cache
CaffeineCacheManager cacheManager = new CaffeineCacheManager("UserCache","InventoryCache");
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
private Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(<initial capacity>)
.maximumSize(<max size>)
.expireAfterAccess(<expire after hrs>, TimeUnit.HOURS)
.recordStats();
}
This will initialize your CacheManager with two Caffeeine Cache instances.
Use below Rest Controller Class to access these class.
#RestController
#RequestMapping(path = "/v1/admin/cache")
public class ACSCacheAdminController {
#Autowired
private CacheManager cacheManager;
/**
* call this to invalidate all cache instances
*/
#DeleteMapping(
path = "/",
produces = {"application/json"})
public void invalidateAll() {
Collection<String> cacheNames = cacheManager.getCacheNames();
cacheNames.forEach(this::getCacheAndClear);
}
/**
* call this to invalidate a given cache name
*/
#DeleteMapping(
path = "/{cacheName}",
produces = {"application/json"})
public void invalidateCache(#PathVariable("cacheName") final String cacheName) {
getCacheAndClear(cacheName);
}
/**
* Use this to refresh a cache instance
*/
#PostMapping(
path = "/{cacheName}",
produces = {"application/json"})
public void invalidateCache(#PathVariable("cacheName") final String cacheName) {
getCacheAndClear(cacheName);
Cache cache = cacheManager.getCache(cacheName);
// your logic to put in above cache instance
// use cache.put(key,value)
}
/**
* call this to invalidate cache entry by given cache name and cache key
*/
#DeleteMapping(
path = "/{cacheName}/{key}/",
produces = {"application/json"})
public void invalidateCacheKey(
#PathVariable("cacheName") final String cacheName, #PathVariable("key") Object key) {
final Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("invalid cache name for key invalidation: " + cacheName);
}
cache.evict(key);
}
#GetMapping(
path = "/{cacheName}/{key}",
produces = {"application/json"})
public ResponseEntity<Object> getByCacheNameAndKey(
#PathVariable("cacheName") final String cacheName, #PathVariable("key") final int key) {
final Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("invalid cache name: " + cacheName);
}
return ResponseEntity.ok().body(cache.get(key));
}
private void getCacheAndClear(final String cacheName) {
final Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("invalid cache name: " + cacheName);
}
cache.clear();
}
Just change the code as per your need :)

spring-data-elasticsearch searching through different enttities/indicies

I have a requirement to provide functionality which will allow user to search through many different domain elements and see results as combined list. So in UI he will have to fill only one text-field and than retrive results.
To visualize lets assume i have 3 entities in domain:
#Document(indexName="car")
public class Car {
private int id;
private String type;
}
#Document(indexName="garage")
public class Garage{
private int id;
private String address;
}
#Document(indexName="shop")
public class Shop{
private int id;
private String name;
}
Now i thought i could achieve requirement like this:
...
#Inject
private ElasticsearchTemplate elasticsearchTemplate;
...
#RequestMapping(value = "/_search/all/{query}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<?> search(#PathVariable String query) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryString(query))
.withIndices("car", "garage", "shop")
.build();
//THIS WORKS
elasticsearchTemplate.queryForIds(searchQuery);
//THIS THROWS ERROR ABOUT WRONG INDEXES
return elasticsearchTemplate.queryForPage(searchQuery, GlobalSearchDTO.class, new GlobalSearchResultMapper()).getContent();
}
...
class GlobalSearchDTO {
public Long id;
public String type;
public Object obj;
}
...
but when calling 2nd function - the one which is responsible for returning actual documents, the following exception is thrown:
Unable to identify index name. GlobalSearchDTO is not a Document. Make
sure the document class is annotated with #Document(indexName="foo")
I've tried with passing any domain entity as a class argument, but than i am retriving only elements from the corresponding index, not all of them. For instance calling:
return elasticsearchTemplate.queryForPage(searchQuery, Shop.class, new GlobalSearchResultMapper()).getContent();
Results in retrivng elements only from 'shop' index. It seems like for some reason dynamically provided indicies are not used.
So the question is: Is it possible to retrive data like that? Why specifying '.withIndices("car", "garage", "shop")' is not enough?
Maybe i should consider other solutions like:
search through indexes in loop(one bye one), join results and order them by score
create separate GlobalSearch entity with 'globalsearch' index
and duplicate data there
Thanks in advance!
Krzysztof
I have managed to find suitable workaround for my problem. It turned out that when using 'scroll' and 'scan' functionality dynamically provided indicies are used which means that query works as expected. Code for solution:
#RequestMapping(value = "/_search/all/{query}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<?> search(#PathVariable String query) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryString(query))
.withIndices("car", "garage", "shop")
.withPageable(new PageRequest(0,1))
.build();
String scrollId = elasticsearchTemplate.scan(searchQuery, 1000, false);
List<GlobalSearchDTO> sampleEntities = new ArrayList<GlobalSearchDTO>();
boolean hasRecords = true;
while (hasRecords){
Page<GlobalSearchDTO> page = elasticsearchTemplate.scroll(scrollId, 5000L , new ResultMapper());
if(page != null) {
sampleEntities.addAll(page.getContent());
hasRecords = page.hasNext();
}
else{
hasRecords = false;
}
}
return sampleEntities;
}
}
and in the ResultMapper class:
...
for (SearchHit hit : response.getHits()) {
switch(hit.getIndex()) {
case "car": //map to DTO
case "shop": //map to DTO
case "garage": //map to DTO
}
}
...

Resources