1

I want fetch the data from the table . table Name : Student

Student {
  int id;
  String Name;
};

I have query :

select Name from Student where id in (:ids);

Need : When I call the repository student findByIdIn(:ids) then I want to get the students with matching ids and also cache them. If again I call with some another set of ids then flow will go like first It will check the cache if any ids data present then take that from the cache then go for the DB call for the ids which we didn't get in the cache then we will store it in the cache.

Expecting spring boot caching related information to achieve the above requirement.

John Blum
  • 7,381
  • 1
  • 20
  • 30

1 Answers1

1

The short answer to this question is NO.

If I understand your question correctly (and to verify) then you are expecting something like the following scenario:

Given a query for Student IDs [ 2, 3, 4, 5, 6 ] and assuming Student IDs [ 2, 4, 6 ] are in the Cache, but Student IDs [ 3, 5 ] are currently only in the Database, then you are looking for something similar to the following:

// PROBLEM 2
//@Cacheable("Students")
List<Student> findByIds(Set<Integer> studentIds) {

    List<Student> result = new ArrayList<>();

    // First load Students stored in Cache
    studentIds.stream()
        .map(id -> this.getCache().get(id))  // CACHE CALL - PROBLEM 3
        .filter(Objects::nonNull)
        .map(Cache.ValueWrapper::get)
        .filter(Student.class::isInstance)
        .map(Student.class::cast)
        .forEach(result::add);

    Set<Integer> idsToQuery = new HashSet<>(studentIds);

    idsToQuery.removeAll(result.stream()
        .map(Student::getId)
        .collect(Collectors.toSet()));

    // Assert Student IDs that need to be queried and loaded from the 
    // Database vs. Student IDs present in the Cache
    getStudentIdAssertion().accept(idsToQuery);

    // Run: SELECT * FROM Students WHERE id IN (...)
    getDatabase().query(idsToQuery).stream()
        .map(this::cache) // CACHE DATABASE QUERY RESULTS
        .forEach(result::add);

    return result;
}

See the complete test class here.

Spring's Cache Abstraction, which is part of the core Spring Framework, and is only extended by Spring Boot (e.g. with auto-configuration and a few extras), simply decorates or wraps existing and expensive service methods or data access calls (using AOP) with caching behavior. In other words, it is all or nothing.

In simple terms, if you have @Cacheable service (or repository / DAO) method like so:

@Cache("Students")
List<Student> findById(Set<Integer> ids) {
  // perform other processing as necessary
  return repository.findByIds(ids);
}

Then there are several problems with Spring's caching arrangement relative to your use case.

PROBLEM 1:

First, the findById(..) service method is all or nothing. Either the KEY (and I say KEY loosely here; see PROBLEM 2) IS IN the cache or IS NOT IN the cache.

If the KEY is in the cache, then the entire findById(..) service method will NOT even be invoked.

If the KEY is not in the cache, then the findById(..) service method will be invoked, but then the "Students" Cache is no longer involved during the processing of the service method, unless you explicitly involve the cache yourself, as I have demonstrated above (but that is another problem... PROBLEM 3).

NOTE: Spring's caching behavior is roughly equivalent to Map.computeIfAbsent(:KEY, :Function<KEY, VALUE>).

PROBLEM 2:

By default (see doc), Spring's Cache Abstraction uses all the method parameters to generate a key. Of course, you can customize key generation, but that will not help you in this case.

Therefore, what would actually get cached in the "Students" Cache with a call to @Cacheable findByIds(:Set<Integer>):List<Student> is the following:

CACHE KEY    | CACHE VALUE
-------------|--------------
Set<Integer> | List<Student>

That is, there would be a single cache entry where the entire Set of IDs is the KEY and the value is the entire result set, or List of Students returned.

Rather, you probably want each individual Student (object/record) stored in the cache individually, when queried with a Set of IDs (e.g. [1, 2, 3]):

CACHE KEY    | CACHE VALUE
-------------|--------------
1            | Jon Doe
2            | Jane Doe
3            | Pie Doe
...

Spring does not support this out-of-the-box, but it is possible to achieve.

PROBLEM 3:

As you can see in my generic solution above, Spring's Cache interface, which is an Adapter around existing caching provider's (actual) cache implementation only allows single key access.

This can be a rather expensive call for each and every key depending on 1) the number of keys queries (e.g. 10 vs. 1000 keys could be quite the difference) and 2) the cache topology (local vs. client/server vs. WAN, etc).

Ideally, you could pass all the keys to the Cache GET operation. And, while this is not supported by Spring's Cache Abstraction in general, it is generally supported by the underlying caching providers cache implementation.

For example, in Hazelcast, the Cache implementation is a [Distributed]IMap (depending on your topology and configuration) and therefore, you could call IMap.getAll(:Set<K>), when acquiring access to the "native" cache.

However, that is also very caching provider specific and dependent, so you have to be careful when coupling your application to the underlying caching provider API like this, particularly if you were ever to change caching providers. But, it does allow you to improve on the general solution I presented above.

CONCLUSION:

In conclusion, and in general, Spring's Cache Abstraction does not allow you to do what you are asking here, out-of-the-box. But, using a custom AOP Aspect perhaps, or maybe just simply creating a custom DAO wrapping your SD Repository you could achieve a similar effect.

I'll leave that as an exercise for you since the mileage in this UC varies greatly based on your application requirements, particularly your performance requirements.

Good luck!

John Blum
  • 7,381
  • 1
  • 20
  • 30