5

I need a cached map in my Java code. The map is loaded from DB and needs reloaded periodically from DB (for all of the data in the map). Since we can't import any new package now. Is there any existing function in google's guava package or code example?

It is better if the map is implemented as thread safe. But it is still OK if it is not but simple enough.

The "LoadingCache" is kind of what I like but it doesn't have data initialization method for me to put the data into the map at the beginning. And it needs to reach the DB for everytime "get" comes after the map expired.

Thank you!


Some sample codes may help here:

public interface AToBMapper
{

    public static final String DEFAULT_B_NAME = "DEFAULT";

    public String getBForA(final String a);
}

public class AToBMapperImpl implements AToBMapper
{
    private final SomeDAO dao;

    private Map<String, String> cachedMap;

    public AToBMapperImpl(final SomeDAO dao)
    {
        this.dao = dao;
        cachedMap = new HashMap<String, String>();
    }

    public String getBForA(final String a)
    {
        // if the map is not initialized, initialize it with the data
        // if the map is expired, refresh all the data in the map
        // return the mapped B for A (if there is no mapping for A, return the "DEFAULT")
    }

    private Map<String, String> getTheData(final List<String> listOfB)
    {
        Map<String, String> newData = dao.getAToBMapping(listOfB);
    }
}
Xiezi
  • 2,949
  • 3
  • 21
  • 29

3 Answers3

1

The "LoadingCache" is kind of what I like but it doesn't have data initialization method for me to put the data into the map at the beginning.

Of course it does have such method - putAll(Map<K, V>) from Cache interface which LoadingCache extends. The method Copies all of the mappings from the specified map to the cache.

There's also similar put(K, V) method you can use for this purpose.

EDIT:

Based on your comments I can tell that you don't want LoadingCache at all but rather maintain expiration of all entries by yourself. Here is simple example of what you could use (only JDK's and Guava's classes):

public class AToBMapperImpl implements AToBMapper {
  public static final long EXPIRE_TIME_IN_SECONDS =
      TimeUnit.SECONDS.convert(1, TimeUnit.HOURS); // or whatever
  private final SomeDAO dao;
  private final ConcurrentMap<String, String> cache;
  private final Stopwatch stopwatch;

  public AToBMapperImpl(SomeDAO dao) {
    this.dao = dao;
    stopwatch = new Stopwatch();
    cache = new MapMaker().concurrencyLevel(2).makeMap();
  }

  @Override
  public synchronized String getBForA(final String a) {
    // if the map is not initialized, initialize it with the data
    if (!stopwatch.isRunning()) {
      cache.putAll(getNewCacheContents());
      stopwatch.start();
    }

    // if the map is expired, refresh all the data in the map
    if (stopwatch.elapsedTime(TimeUnit.SECONDS) >= EXPIRE_TIME_IN_SECONDS) {
      cache.clear();
      cache.putAll(getNewCacheContents());
      stopwatch.reset();
    }

    // return the mapped String for A
    // (if there is no mapping for A, return the "DEFAULT")
    return cache.containsKey(a) ? cache.get(a) : new String(DEFAULT_B_NAME);
  }

  private Map<String, String> getNewCacheContents() {
    return getTheData(Arrays.asList("keys", "you", "want", "to", "load"));
  }

  private Map<String, String> getTheData(List<String> listOfB) {
    return dao.getAToBMapping(listOfB);
  }
}
Grzegorz Rożniecki
  • 27,415
  • 11
  • 90
  • 112
  • Yes, but I also need the reload method to be like putAll, which means refresh all the data in the map. So that it won't reach the DB for every "get" after expired. Does it support that? – Xiezi Mar 12 '13 at 10:05
  • Question updated. So actually it needs to reload all of the data in the map periodically. – Xiezi Mar 12 '13 at 10:08
  • Some sample codes updated in the question. I'm not sure what your "side effects" means. My dao will return a Map which contains all the data. – Xiezi Mar 12 '13 at 11:06
  • Still I don't see what is the problem you are trying to solve here. Cache does not load whole DB periodically, but rather keeps track of each entry's write time, so calling get on Guava's cache loads only one entry - the one with key A and value B. Also, your getTheData takes List as an argument, while you want A -> B mapping, not the opposite. – Grzegorz Rożniecki Mar 12 '13 at 11:29
  • 1
    "Cache does not load whole DB periodically, but rather keeps track of each entry's write time, so calling get on Guava's cache loads only one entry - the one with key A and value B." This is the problem. When the keys are expired, it will be loaded from DB every time. I don't want it to query DB so many times. The single entry tracking is not necessary for me. I need whole map refreshing which query DB less times. – Xiezi Mar 12 '13 at 11:45
  • Seems like premature optimization to me - **is querying the DB a measured and profiled problem?** If not, does not optimize and use LoadingCache as it is designed (using get or getUnchecked). Otherwise (if the problem exists for you) Cache isn't for your use case (eviction takes place for each entry, not for all entries) and you should design a workaround yourself. – Grzegorz Rożniecki Mar 12 '13 at 11:55
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/26027/discussion-between-xaerxess-and-xiezi) – Grzegorz Rożniecki Mar 12 '13 at 12:17
1

While @Xaerxess has provided a workable solution I feel you could still have achieved this by simply using a LoadingCache instance with a single key value. I do understand your reasoning as to the scenario as I have worked on projects where a single object load may be just as expensive as the whole set.

public class AToBMapperImpl implements AToBMapper {

    private final LoadingCache<Boolean, Map<String, String>> cachedMap;

    public AToBMapperImpl(final SomeDAO dao, final List<String> listOfB) {
        this.cachedMap = CacheBuilder.newBuilder()
                .expireAfterAccess(4, TimeUnit.HOURS)
                .build(new CacheLoader<Boolean, Map<String, String>>() {
                    @Override
                    public Map<String, String> load(Boolean k) throws Exception {
                        return dao.getAToBMapping(listOfB);
                    }
                });
    }

    @Override
    public String getBForA(String a) {
        try {
            Map<String, String> map = cachedMap.get(Boolean.TRUE);
            return map.containsKey(a) ? map.get(a) : DEFAULT_B_NAME;
        } catch (ExecutionException ex) {
            // Do what you need to do on exception.
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }

}

This way the cache is bound to a single entry with the value being the map you're after. You get the benefit of the expiry time. If you find that listOfB could change over time you may need to write another service method to retrieve this which the cache loader could then call.

Brett Ryan
  • 26,937
  • 30
  • 128
  • 163
-1

You needn't data initialization method, it can be initialized at first access. Here is sample code how to do this

cachedWhatever = 
    cacheBuilder
    .maximumSize(5)
    .expireAfterAccess(30, TimeUnit.MINUTES)
    .removalListener(new RemovalListener<String, CachedWhatever>() {

        @Override
        public void onRemoval(RemovalNotification<String, CachedMetadata> notification) {
            logger.info("removed");
        }
    })
    .build(new CacheLoader<String, CachedWhatever>(){

            @Override
        public CachedMetadata load(String databaseName) throws Exception {
            logger.info("created");

            return new CachedWhatever();
            }
        });

and after accessing to the cache with

cachedWhatever.get([key]);

it will create cache if not exists

and using cachedWhatever.invalidate([key]); will clear the cache for the key

Arsen Alexanyan
  • 3,061
  • 5
  • 25
  • 45
  • I don't see your point... Could you please make the codes be more specific. For example, my map is Map. How can I refresh it all when it is expired and at the beginning? – Xiezi Mar 12 '13 at 11:18
  • This does not serve the purpose of expiring and reloading all content as the OP mentions. – Brett Ryan Oct 07 '13 at 05:08