Use a tiny local cache to improve NoSQL upsert performance

If the application is using upsert we can avoid performing a query
against the database for the old object and just blind-write the new
and old index rows.  Since most NoSQL systems have higher round-trip
latency than they do per-row operation costs this should improve the
performance of most upsert operations.

During upsert we do try to avoid unnecessary index updates by
tracking the old copy of an object in a local cache.

Change-Id: I2f2aa7269f4049b86d5cb2c8d2079263335f37ec
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
index 739c3da..08e03bc 100644
--- a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
+++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
@@ -30,18 +30,36 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /** Base implementation for {@link Access} in a {@link GenericDatabase}. */
 public abstract class GenericAccess<T, K extends Key<?>> extends
     NoSqlAccess<T, K> {
+  /** Maximum number of results to cache to improve updates on upsert. */
+  private static final int MAX_SZ = 64;
+
   private final GenericSchema db;
+  private LinkedHashMap<K, byte[]> cache;
 
   protected GenericAccess(final GenericSchema s) {
     super(s);
     db = s;
   }
 
+  protected LinkedHashMap<K, byte[]> cache() {
+    if (cache == null) {
+      cache = new LinkedHashMap<K, byte[]>(8) {
+        @Override
+        protected boolean removeEldestEntry(Entry<K, byte[]> entry) {
+          return MAX_SZ <= size();
+        }
+      };
+    }
+    return cache;
+  }
+
   /**
    * Lookup a single entity via its primary key.
    * <p>
@@ -109,7 +127,10 @@
     final ArrayList<T> res = new ArrayList<T>();
     Iterator<Map.Entry<byte[], byte[]>> i = db.scan(fromKey, toKey, limit);
     while (i.hasNext()) {
-      res.add(getObjectCodec().decode(i.next().getValue()));
+      byte[] bin = i.next().getValue();
+      T obj = getObjectCodec().decode(bin);
+      res.add(obj);
+      cache().put(primaryKey(obj), bin);
     }
     return new ListResultSet<T>(res);
   }
@@ -187,6 +208,7 @@
         //
         final T obj = getObjectCodec().decode(objData);
         if (matches(idx, obj, idxkey)) {
+          cache().put(primaryKey(obj), objData);
           res.add(obj);
           if (limit > 0 && res.size() == limit) {
             break SCAN;
@@ -254,13 +276,18 @@
 
   private void upsertOne(T newObj, boolean mustExist) throws OrmException {
     final byte[] key = dataRowKey(primaryKey(newObj));
-    final byte[] oldBin = db.fetchRow(key);
-    final T oldObj;
 
+    T oldObj;
+    byte[] oldBin = cache().get(primaryKey(newObj));
     if (oldBin != null) {
       oldObj = getObjectCodec().decode(oldBin);
     } else if (mustExist) {
-      throw new OrmConcurrencyException();
+      oldBin = db.fetchRow(key);
+      if (oldBin != null) {
+        oldObj = getObjectCodec().decode(oldBin);
+      } else {
+        throw new OrmConcurrencyException();
+      }
     } else {
       oldObj = null;
     }
@@ -324,6 +351,7 @@
     for (T oldObj : instances) {
       db.delete(dataRowKey(primaryKey(oldObj)));
       pruneOldIndexes(oldObj, null);
+      cache().remove(primaryKey(oldObj));
     }
     maybeFlush();
   }
@@ -445,7 +473,8 @@
     encodePrimaryKey(b, primaryKey(obj));
     final byte[] key = b.toByteArray();
 
-    return IndexRow.CODEC.encodeToByteString(IndexRow.forKey(now, key)).toByteArray();
+    return IndexRow.CODEC.encodeToByteString(IndexRow.forKey(now, key))
+        .toByteArray();
   }
 
   private static class IndexException extends RuntimeException {