Implement multiple row lookup by default

Performing multiple sequential row lookups can be inefficient,
so try to simplify the implementation of batching here by pushing
it down into the GenericSchema.

Change-Id: I770e8690d48ea1c22cb55f96dd636498b5a938bc
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 a17727b..2a5b294 100644
--- a/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
+++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericAccess.java
@@ -81,6 +81,54 @@
     }
   }
 
+  @Override
+  public ResultSet<T> get(final Iterable<K> keys) throws OrmException {
+    final ResultSet<Row> rs = db.fetchRows(new Iterable<byte[]>() {
+      @Override
+      public Iterator<byte[]> iterator() {
+        return new Iterator<byte[]>() {
+          private final Iterator<K> i = keys.iterator();
+
+          @Override
+          public boolean hasNext() {
+            return i.hasNext();
+          }
+
+          @Override
+          public byte[] next() {
+            return dataRowKey(i.next());
+          }
+
+          @Override
+          public void remove() {
+            throw new UnsupportedOperationException();
+          }
+        };
+      }
+    });
+
+    final Iterator<Row> i = rs.iterator();
+    return new AbstractResultSet<T>() {
+      @Override
+      protected boolean hasNext() {
+        return i.hasNext();
+      }
+
+      @Override
+      protected T next() {
+        byte[] bin = i.next().getValue();
+        T obj = getObjectCodec().decode(bin);
+        cache().put(primaryKey(obj), bin);
+        return obj;
+      }
+
+      @Override
+      public void close() {
+        rs.close();
+      }
+    };
+  }
+
   /**
    * Scan a range of keys from the data rows and return any matching objects.
    *
diff --git a/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java b/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java
index cfbda78..81b62aa 100644
--- a/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java
+++ b/src/main/java/com/google/gwtorm/nosql/generic/GenericSchema.java
@@ -19,13 +19,16 @@
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
 import com.google.gwtorm.client.Schema;
+import com.google.gwtorm.client.impl.ListResultSet;
 import com.google.gwtorm.nosql.CounterShard;
 import com.google.gwtorm.nosql.IndexKeyBuilder;
 import com.google.gwtorm.nosql.IndexRow;
 import com.google.gwtorm.nosql.NoSqlSchema;
 import com.google.gwtorm.schema.SequenceModel;
 
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 
 /**
  * Base implementation for {@link Schema} in a {@link GenericDatabase}.
@@ -146,6 +149,27 @@
   }
 
   /**
+   * Fetch multiple rows at once.
+   * <p>
+   * The default implementation of this method is a simple iteration over each
+   * key and executes a sequential fetch with {@link #fetchRow(byte[])}.
+   *
+   * @param keys keys to fetch and return.
+   * @return iteration over the rows that exist and appear in {@code keys}.
+   * @throws OrmException the data store cannot process the request.
+   */
+  public ResultSet<Row> fetchRows(Iterable<byte[]> keys) throws OrmException {
+    List<Row> r = new ArrayList<Row>();
+    for (byte[] key : keys) {
+      byte[] val = fetchRow(key);
+      if (val != null) {
+        r.add(new Row(key, val));
+      }
+    }
+    return new ListResultSet<Row>(r);
+  }
+
+  /**
    * Scan a range of keys and return any matching objects.
    * <p>
    * To fetch a single record with a scan, set {@code toKey} to the same array