60

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?

xlm
  • 6,854
  • 14
  • 53
  • 55
Shisoft
  • 4,197
  • 7
  • 44
  • 61
  • 16
    Note that caching the nulls might save you a lot of database accesses, of course, depending on your access pattern. Thus I wouldn't refuse to cache them without some thoughts. – maaartinus Nov 15 '12 at 09:39
  • 5
    guava will NOT save null with key in the cache but throw exception – bylijinnan Jul 07 '15 at 12:55
  • 2
    an interesting tidbit: if you happen to use RuntimeExcpetions for this, guava repackages these into com.google.common.util.concurrent.UncheckedExecutionException ;-| – Andrew Norman Apr 19 '16 at 18:23

5 Answers5

83

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.

Grzegorz Rożniecki
  • 27,415
  • 11
  • 90
  • 112
  • The get() method only throws ExecutionException for all checked exception. So how the client can differentiate from actual exceptions that are thrown because of an error and the UserNotFoundException in this example? Generally speaking a well designed api should not force its clients to use exception to control ordinary workflow. – Arash Jun 24 '16 at 01:09
  • 3
    @Arash You can catch `ExecutionException` and its cause would be `UserNotFoundException` in that case (of course you should do an `instanceof` check after `.getCause()` to make sure it was not another checked exception). Or you can use `getUnckecked` (if you don't fancy checked exceptions) or `getIfPresent` if `get` is not what you need. – Grzegorz Rożniecki Jun 24 '16 at 06:22
  • 4
    This violates Effective Java 69: Use exceptions only for exceptional conditions. – Adam Bliss May 07 '18 at 18:16
  • @AdamBliss I beg to differ, it's exceptional case in terms of Guava's `LoadingCache` contract - you were "unable to load result" and thus throw exception. Also, item 55 (regarding returning `Optional`) states that "In summary, if you find yourself writing a method that can’t always return a value (...) for performance-critical methods, it may be better to return a null or throw an exception." – Grzegorz Rożniecki May 10 '18 at 05:15
  • @Xaerxess I agree that "unable to load result" is exceptional from the Cache's perspective. But "no entry in DB" is not exceptional from the Loader's perspective, and that's what's throwing. What this really means is that the LoadingCache contract is not quite suitable for this use case. One could implement a "PartialCache" atop a LoadingCache>, but I think doing so with invalidate() would be better than throw/catch. – Adam Bliss May 10 '18 at 19:08
  • 1
    @AdamBliss Can't argue with Guava cache design decisions, especially that [Caffeine](https://github.com/ben-manes/caffeine), its successor, [allows returning `null` as "value-not-found" from `CacheLoader#load(K)`](https://static.javadoc.io/com.github.ben-manes.caffeine/caffeine/2.6.2/com/github/benmanes/caffeine/cache/CacheLoader.html#load-K-). I've edited my answer and mentioned Caffeine cache. – Grzegorz Rożniecki May 11 '18 at 07:26
  • This will not work now as null values now throwing com.google.common.util.concurrent.UncheckedExecutionException – quintin Oct 16 '19 at 12:01
  • @quintin It hasn't changed IMO, it's always been wrapping any runtime exceptions with `UncheckedExecutionException`, but the cause should be `UserNotFoundException` for the answer above. – Grzegorz Rożniecki Nov 19 '19 at 09:19
  • @Xaerxess I found this while debugging with null value. – quintin Nov 19 '19 at 10:01
  • @quintin Yes, I was not talking about OP's solution with returning nulls, which is wrong, but my answer. In that case it was always this way, as stated in `get` / `getUnchecked` javadoc it throws "`UncheckedExecutionException` - if an unchecked exception was thrown while loading the value". The cause would be `UserNotFoundException `. – Grzegorz Rożniecki Nov 19 '19 at 11:19
59

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.

卢声远 Shengyuan Lu
  • 31,208
  • 22
  • 85
  • 130
  • 7
    Thechnically it will cache "null" values (i.e. `Optional.absent()` objects), so it does not avoid caching null values as OP wanted, but the cost isn't big. – Grzegorz Rożniecki Nov 14 '12 at 13:45
  • Doesn't matter. I had a slightly different problem, and your solution helped. ;) – Haroldo_OK Jun 16 '14 at 19:35
  • I think this answer should be removed. Not because it is not ok for certain situations - but it might confuse readers as IT CACHES ABSENT VALUES (as opposed to the requirements in the question). – Gilbert Sep 26 '14 at 03:10
  • 8
    when using the `cache.get( key )`, in case `Optional.absent` is returned, you can just `cache.invalidate( key )` to make sure `Optional.absent` is only there for the life of the `get` call. and then poof.. it's gone. – tolitius Nov 13 '14 at 06:40
  • 7
    Storing Optional.absent() is very useful for cases where you want to avoid doing the re-loading of the absent values all the time, but rather with the same expiry timeframe as the other cached values, which works nicely with Optional! – centic Oct 29 '15 at 11:18
5

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;
    }
}
Raman Yelianevich
  • 1,117
  • 11
  • 10
  • 1
    The problem is that many parallel calls of the same key can be done to `fallback.get()`, causing to calculate the same value. There are cases where this can be quite heavy. – AlikElzin-kilaka Jan 16 '19 at 10:50
  • Yes. This is the answer. The Guava LoadingCache is worthless because of its poor null handling. – ccleve Sep 01 '20 at 22:50
0

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;
}
jayxhj
  • 2,829
  • 1
  • 19
  • 24
0

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);
                }
            });
Harvey
  • 51
  • 1
  • 3