How to avoid caching when values are null?

CachingGuava

Caching Problem Overview


I am using Guava to cache hot data. When the data does not exist in the cache, I have to get it from database:

public final static LoadingCache<ObjectId, User> UID2UCache = CacheBuilder.newBuilder()
        //.maximumSize(2000)
        .weakKeys()
        .weakValues()
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .build(
        new CacheLoader<ObjectId, User>() {
            @Override
            public User load(ObjectId k) throws Exception {
                User u = DataLoader.datastore.find(User.class).field("_id").equal(k).get();
                return u;
            }
        });

My problem is when the data does not exists in database, I want it to return null and to not do any caching. But Guava saves null with the key in the cache and throws an exception when I get it:

> com.google.common.cache.CacheLoader$InvalidCacheLoadException: > CacheLoader returned null for key shisoft.

How do we avoid caching null values?

Caching Solutions


Solution 1 - Caching

Just throw some Exception if user is not found and catch it in client code while using get(key) method.

new CacheLoader<ObjectId, User>() {
    @Override
    public User load(ObjectId k) throws Exception {
        User u = DataLoader.datastore.find(User.class).field("_id").equal(k).get();
        if (u != null) {
             return u;
        } else {
             throw new UserNotFoundException();
        }
    }
}

From CacheLoader.load(K) Javadoc:

> Returns:
> the value associated with key; must not be null
> Throws:
> Exception - if unable to load the result

Answering your doubts about caching null values:

> Returns the value associated with key in this cache, first loading > that value if necessary. No observable state associated with this > cache is modified until loading completes.

(from LoadingCache.get(K) Javadoc)

If you throw an exception, load is not considered as complete, so no new value is cached.

EDIT:

Note that in Caffeine, which is sort of Guava cache 2.0 and "provides an in-memory cache using a Google Guava inspired API" you can return null from load method:

> Returns: > the value associated with key or null if not found

If you may consider migrating, your data loader could freely return when user is not found.

Solution 2 - Caching

Simple solution: use com.google.common.base.Optional<User> instead of User as value.

public final static LoadingCache<ObjectId, Optional<User>> UID2UCache = CacheBuilder.newBuilder()
        ...
        .build(
        new CacheLoader<ObjectId, Optional<User>>() {
            @Override
            public Optional<User> load(ObjectId k) throws Exception {
                return Optional.fromNullable(DataLoader.datastore.find(User.class).field("_id").equal(k).get());
            }
        });

EDIT: I think @Xaerxess' answer is better.

Solution 3 - Caching

Faced the same issue, cause missing values in the source was part of the normal workflow. Haven't found anything better than to write some code myself using getIfPresent, get and put methods. See the method below, where local is Cache<Object, Object>:

private <K, V> V getFromLocalCache(K key, Supplier<V> fallback) {
    @SuppressWarnings("unchecked")
    V s = (V) local.getIfPresent(key);
    if (s != null) {
        return s;
    } else {
        V value = fallback.get();
        if (value != null) {
            local.put(key, value);
        }
        return value;
    }
}

Solution 4 - Caching

When you want to cache some NULL values, you could use other staff which namely behave as NULL.

And before give the solution, I would suggest you not to expose LoadingCache to outside. Instead, you should use method to restrict the scope of Cache.

For example, you could use LoadingCache<ObjectId, List<User>> as return type. And then, you could return empty list when you could'n retrieve values from database. You could use -1 as Integer or Long NULL value, you could use "" as String NULL value, and so on. After this, you should provide a method to handler the NULL value.

when(value equals NULL(-1|"")){
   return null;
}

Solution 5 - Caching

I use the getIfPresent

@Test
    public void cache() throws Exception {
        System.out.println("3-------" + totalCache.get("k2"));
        System.out.println("4-------" + totalCache.getIfPresent("k3"));
    }



    private LoadingCache<String, Date> totalCache = CacheBuilder
            .newBuilder()
            .maximumSize(500)
            .refreshAfterWrite(6, TimeUnit.HOURS)
            .build(new CacheLoader<String, Date>() {
                @Override
                @ParametersAreNonnullByDefault
                public Date load(String key) {
                    Map<String, Date> map = ImmutableMap.of("k1", new Date(), "k2", new Date());
                    return map.get(key);
                }
            });

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionShisoftView Question on Stackoverflow
Solution 1 - CachingXaerxessView Answer on Stackoverflow
Solution 2 - Caching卢声远 Shengyuan LuView Answer on Stackoverflow
Solution 3 - CachingRaman YelianevichView Answer on Stackoverflow
Solution 4 - CachingjayxhjView Answer on Stackoverflow
Solution 5 - CachingHarveyView Answer on Stackoverflow