JPA: cache queries only if resultset is not empty

I am on JPA 2.1 + Hibernate + EHCache.

Here is my named query (the query code is not relevant):

List<MyEntity> list = getEntityManager()
    .createNamedQuery("my-query-id", MyEntity.class))
    .setHint(QueryHints.CACHEABLE,    true)
    .setHint(QueryHints.CACHE_REGION, "my-query-region")
    .setParameter("my-query-param", "my-param-value")
    .setMaxResults(1)
    .getResultList();

if (list.isEmpty()) {
    log.warn("No data found.");
    return null;
}

return list;

The goal I wish to achieve is to cache query result only if its result is non empty.

I am sure, beacause I inspected it by hibernate logging at a trace level, that empty result set is cached anyway.

Any suggestion would be appreciated.

Regards!

Answer

I found a solution by writing an EHCache Decorator like follow:

EHCache XML configuration fragment

<cache name="my-queries-region"
       maxEntriesLocalHeap="50000"
       eternal="false"
       timeToLiveSeconds="14400">

    <persistence strategy="none"/>

    <!-- https://www.ehcache.org/ehcache.xml -->

    <cacheDecoratorFactory
        class="com.example.JpaCacheDecoratorNotEmptyQueryFactory" />
</cache>

Decorator factory implementation

package com.example;

import net.sf.ehcache.Ehcache;
import net.sf.ehcache.constructs.CacheDecoratorFactory;

import java.util.Properties;

public class JpaCacheDecoratorNotEmptyQueryFactory extends CacheDecoratorFactory {

    @Override
    public Ehcache createDecoratedEhcache(Ehcache cache, Properties properties) {
        return new JpaCacheDecoratorNotEmptyQueryDecorator(cache);
    }

    @Override
    public Ehcache createDefaultDecoratedEhcache(Ehcache cache, Properties properties) {
        return new JpaCacheDecoratorNotEmptyQueryDecorator(cache);
    }
}

Decorator implementation

package com.example;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.constructs.EhcacheDecoratorAdapter;
import org.hibernate.cache.internal.QueryResultsCacheImpl;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;

@Slf4j
public class JpaCacheDecoratorNotEmptyQueryDecorator extends EhcacheDecoratorAdapter {

    private final Field resultsField;

    @SneakyThrows
    @SuppressWarnings("rawtypes")
    protected boolean canCache(Element element) {
        boolean cacheable = true;
        Object  value     = element.getObjectValue();

        if (value instanceof QueryResultsCacheImpl.CacheItem) {
            List results = (List)resultsField.get(value);
            cacheable    = !results.isEmpty();
        }

        if (!cacheable) {
            if (log.isDebugEnabled()) {
                log.debug("Query not cacheable due to empty result set.");
            }
        }

        return cacheable;
    }

    protected boolean canCache(Collection<Element> elements) {
        for (Element element: elements) {
            if (!canCache(element)) {
                return false;
            }
        }

        return true;
    }

    @SneakyThrows
    public JpaCacheDecoratorNotEmptyQueryDecorator(Ehcache underlyingCache) {
        super(underlyingCache);

        resultsField = QueryResultsCacheImpl
            .CacheItem
            .class
            .getDeclaredField("results");

        resultsField.setAccessible(true);
    }

    @Override
    public void put(Element element, boolean doNotNotifyCacheReplicators)
        throws IllegalArgumentException,
               IllegalStateException,
               CacheException
    {
        if (canCache(element)) {
            super.put(element, doNotNotifyCacheReplicators);
        }
    }

    @Override
    public void put(Element element)
        throws IllegalArgumentException,
               IllegalStateException,
               CacheException
    {
        if (canCache(element)) {
            super.put(element);
        }
    }

    @Override
    public void putAll(Collection<Element> elements)
        throws IllegalArgumentException,
               IllegalStateException,
               CacheException
    {
        if (canCache(elements)) {
            super.putAll(elements);
        }
    }

    @Override
    public void putQuiet(Element element)
        throws IllegalArgumentException,
               IllegalStateException,
               CacheException
    {
        if (canCache(element)) {
            super.putQuiet(element);
        }
    }

    @Override
    public void putWithWriter(Element element)
        throws IllegalArgumentException,
               IllegalStateException,
               CacheException
    {
        if (canCache(element)) {
            super.putWithWriter(element);
        }
    }

    @Override
    public Element putIfAbsent(Element element)
        throws NullPointerException
    {
        if (canCache(element)) {
            return super.putIfAbsent(element);
        } else {
            return null;
        }
    }

    @Override
    public Element putIfAbsent(Element element, boolean doNotNotifyCacheReplicators)
        throws NullPointerException
    {
        if (canCache(element)) {
            return super.putIfAbsent(element, doNotNotifyCacheReplicators);
        } else {
            return null;
        }
    }
}