We are using Redis cache for storing data in cache in our application. We are directly using @Cacheable to allow caching and using redis underneath to cache. Below is the config
Redis Config -
@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig implements CachingConfigurer {
@Value("${spring.cache.redis.time-to-live}")
Long redisTTL;
@Bean
public RedisCacheConfiguration cacheConfiguration(ObjectMapper objectMapper) {
objectMapper = objectMapper.copy();
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.registerModules(new JavaTimeModule(), new Hibernate5Module())
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(redisTTL))
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
}
@Bean
public RedissonClient reddison(@Value("${spring.redis.host}") final String redisHost,
@Value("${spring.redis.port}") final int redisPort,
@Value("${spring.redis.cluster.nodes}") final String clusterAddress,
@Value("${spring.redis.use-cluster}") final boolean useCluster,
@Value("${spring.redis.timeout}") final int timeout) {
Config config = new Config();
if (useCluster) {
config.useClusterServers().addNodeAddress(clusterAddress).setTimeout(timeout);
} else {
config.useSingleServer().setAddress(String.format("redis://%s:%d", redisHost, redisPort)).setTimeout(timeout);
}
return Redisson.create(config);
}
@Bean
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redissonClient) {
return new RedissonConnectionFactory(redissonClient);
}
@Bean
public RedisCacheManager cacheManager(RedissonClient redissonClient, ObjectMapper objectMapper) {
this.redissonConnectionFactory(redissonClient).getConnection().flushDb();
RedisCacheManager redisCacheManager= RedisCacheManager.builder(this.redissonConnectionFactory(redissonClient))
.cacheDefaults(this.cacheConfiguration(objectMapper))
.build();
redisCacheManager.setTransactionAware(true);
return redisCacheManager;
}
@Override
public CacheErrorHandler errorHandler() {
return new RedisCacheErrorHandler();
}
@Slf4j
public static class RedisCacheErrorHandler implements CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.info("Unable to get from cache " + cache.getName() + " : " + exception.getMessage());
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.info("Unable to put into cache " + cache.getName() + " : " + exception.getMessage());
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
log.info("Unable to evict from cache " + cache.getName() + " : " + exception.getMessage());
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.info("Unable to clean cache " + cache.getName() + " : " + exception.getMessage());
}
}
}
Service class -
@Service
@AllArgsConstructor
@Transactional
public class CompanyServiceImpl implements CompanyService {
private final CompanyRepository companyRepository;
@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager")
public Optional<CompanyEntity> findByName(String companyName) {
return companyRepository.findByName(companyName);
}
}
Company class -
@Entity
@Jacksonized
@AllArgsConstructor
@NoArgsConstructor
public class CompanyEntity {
@Id
private Long id;
@ToString.Exclude
@OneToMany(mappedBy = "comapnyENtity", cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private List<EmployeeEntity> employeeEntities;
}
Once we run the service, caching gets done properly too. Once we fire the query, we get following record in cache -
> get Company::ABC
" {"@class":"com.abc.entity.CompanyEntity","createdTs":1693922698604,"id":100000000002,"name":"ABC","description":"ABC Operations","active":true,"EmployeeEntities":["org.hibernate.collection.internal.PersistentBag",[{"@class":"com.abc.entity.EmployeeEntity","createdTs":1693922698604,"Id":100000000002,"EmployeeEntity":{"@class":"com.abc.EmployeeLevel","levelId":100000000000,"name":"H1","active":true}}]]}"
But while we try to execute the query the second time, it still goes inside the cache method with below logs -
Unable to get from cache Company : Could not read JSON: failed to lazily initialize a
collection, could not initialize proxy - no Session (through reference chain:
com.abc.entity.CompanyEntity$CompanyEntityBuilder["employeeEntities"]); nested exception
is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a c
collection, could not initialize proxy - no Session (through reference chain:
com.abc.entity.CompanyEntity$CompanyEntityBuilder["employeeEntities"])
I understood from various SO answers that it is due unavailability of session for proxy child object. But we are caching using EAGER mode and whole collection is present in cache too. But still it goes inside the cached method and get values from db. How can we prevent it and use it directly from cache.
UPDATE If we use LAZY loading, the collection objects doesn't get cached and comes as null. But we require cached collection as methods don't get call on order and cached method will return null later.