0
votes

I have dozens of data access objects like PersonDao with methods like:

Person findById(String id) {}
List<Person> search(String firstName, LastName, Page) {}
int searchCount(String firstName, LastName) {}

I've experimented by adding guava cache with one of these classes and it's really nice, but there's a lot of boilerplate.

Here's an example of making findById look in the cache first:

private final LoadingCache<String, Person> cacheById = CacheBuilder.newBuilder()
  .maximumSize(maxItemsInCache)
  .expireAfterWrite(cacheExpireAfterMinutes, TimeUnit.MINUTES)
  .build(new CacheLoader<String, Person>() {
    public Person load(String key) {
      return findByIdNoCache(key);
  });
//.... and update findById to call the cache ...
@Override
public Person findById(String id) {
  return cacheById.getUnchecked(id);
}

So, because each method has different params and return types, I end up created a separate cacheLoader for every method!

I tried consolidating everything into a single CacheLoader that returns Object type and accepts a Map of objects, but then I end up with big ugly if/else to figure out which method to call to load the cache.

I'm struggling to find an elegant way to add caching to these data access objects, any suggestions? Maybe guava cache isn't meant for this use case?

2

2 Answers

5
votes

Try this. Unfortunately, there are compiler warnings due to generics... But we may supress them because we know nothing bad will happen.

public class CacheContainer {

    private static final long maxItemsInCache = 42;
    private static final long cacheExpireAfterMinutes = 42;
    private final Map<String, LoadingCache> caches = Maps.newHashMap();


    public <K, V> V getFromCache(String cacheId, K key, CacheLoader<K, V> loader) throws ExecutionException {
        LoadingCache<K, V> cache = caches.get(cacheId);
        if (cache == null) {
            cache = CacheBuilder.newBuilder().
                     maximumSize(maxItemsInCache).
                     expireAfterWrite(cacheExpireAfterMinutes, TimeUnit.MINUTES).
                     build(loader);
            caches.put(cacheId, cache);
        }
        return cache.get(key);
    }
}

And then in your dao:

private final CacheContainer cacheContainer = new CacheContainer();


public Person findById(String id) {
    cacheContainer.getFromCache("personById", id, new CacheLoader<String, Person>() {
        @Override
        public Person load(String key) {
          return findByIdNoCache(key);
      });
}

Other methods in the same way. I don't think you can reduce boilerplate any more than that.

1
votes

Creating a CacheLoader (and separate cache) for each method you want to cache the results of is necessary. You could simplify things a little by creating a single CacheBuilder with the cache configuration you want and then creating each of the caches like that:

private final CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder()
    .maximumSize(maxItemsInCache)
    .expireAfterWrite(cacheExpireAfterMinutes, TimeUnit.MINUTES);

private final LoadingCache<String, Person> cacheById = builder.build(
    new CacheLoader<String, Person>() {
      // ...
    });

 private final LoadingCache<Search, List<Person>> searchCache = builder.build(
     new CacheLoader<Search, List<Person>>() {
       // ...
     });

  // etc.