You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2020/03/11 13:07:58 UTC

[cayenne] branch master updated: CAY-2651 Support multiple IDs in the SelectById query

This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


The following commit(s) were added to refs/heads/master by this push:
     new e76287c  CAY-2651 Support multiple IDs in the SelectById query
e76287c is described below

commit e76287c9fb22a06e0bbb0062e0464bc60fa5fdc6
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Wed Mar 11 16:07:29 2020 +0300

    CAY-2651 Support multiple IDs in the SelectById query
---
 RELEASE-NOTES.txt                                  |   1 +
 .../{SelectByIdTest.java => SelectById_IT.java}    |  19 +-
 .../java/org/apache/cayenne/query/SelectById.java  | 272 +++++++++++++++------
 .../org/apache/cayenne/query/SelectById_RunIT.java | 187 ++++++++++----
 4 files changed, 360 insertions(+), 119 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index fa40183..5827792 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -56,6 +56,7 @@ CAY-2611 Exclude system catalogs and schemas when run dbImport without config
 CAY-2612 Modeler: add lazy-loading to dbImport tab
 CAY-2645 Modeler: DbImport tree highlight improvement
 CAY-2650 Support using generated primary keys along with batch inserts
+CAY-2651 Support multiple IDs in the SelectById query
 
 Bug Fixes:
 
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/SelectByIdTest.java b/cayenne-client/src/test/java/org/apache/cayenne/query/SelectById_IT.java
similarity index 70%
rename from cayenne-client/src/test/java/org/apache/cayenne/query/SelectByIdTest.java
rename to cayenne-client/src/test/java/org/apache/cayenne/query/SelectById_IT.java
index 6f44f70..2589203 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/SelectByIdTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/SelectById_IT.java
@@ -22,12 +22,21 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 
+import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.remote.hessian.service.HessianUtil;
 import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
-public class SelectByIdTest {
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class SelectById_IT extends ServerCase {
+
+	@Inject
+	private EntityResolver resolver;
 
 	@Test
 	public void testSerializabilityWithHessian() throws Exception {
@@ -38,7 +47,11 @@ public class SelectByIdTest {
 		SelectById<?> c1 = (SelectById<?>) clone;
 
 		assertNotSame(o, c1);
-		assertEquals(o.entityType, c1.entityType);
-		assertEquals(o.singleId, c1.singleId);
+
+		ObjEntity artistEntity = resolver.getObjEntity(Artist.class);
+
+		assertEquals(artistEntity, o.root.resolve(resolver));
+		assertEquals(o.root.resolve(resolver), c1.root.resolve(resolver));
+		assertEquals(o.idSpec.getQualifier(artistEntity), c1.idSpec.getQualifier(artistEntity));
 	}
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java
index a15ec2f..949efdb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectById.java
@@ -29,15 +29,17 @@ import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
 
+import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 
-import static java.util.Collections.singletonMap;
-import static org.apache.cayenne.exp.ExpressionFactory.matchAllDbExp;
+import static org.apache.cayenne.exp.ExpressionFactory.*;
 
 /**
- * A query to select single objects by id.
+ * A query to select objects by id.
  * 
  * @since 4.0
  */
@@ -45,88 +47,135 @@ public class SelectById<T> extends IndirectQuery implements Select<T> {
 
 	private static final long serialVersionUID = -6589464349051607583L;
 
-	// type is not same as T, as T maybe be DataRow or scalar
-	// either type or entity name is specified, but not both
-	Class<?> entityType;
-	String entityName;
+	final QueryRoot root;
+	final IdSpec idSpec;
+	final boolean fetchingDataRows;
 
-	// only one of the two id forms is provided, but not both
-	Object singleId;
-	Map<String, ?> mapId;
-
-	boolean fetchingDataRows;
 	QueryCacheStrategy cacheStrategy;
 	String cacheGroup;
 	PrefetchTreeNode prefetches;
 
 	public static <T> SelectById<T> query(Class<T> entityType, Object id) {
-		SelectById<T> q = new SelectById<>();
-
-		q.entityType = entityType;
-		q.singleId = id;
-		q.fetchingDataRows = false;
-
-		return q;
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new SingleScalarIdSpec(id);
+		return new SelectById<>(root, idSpec);
 	}
 
 	public static <T> SelectById<T> query(Class<T> entityType, Map<String, ?> id) {
-		SelectById<T> q = new SelectById<>();
-
-		q.entityType = entityType;
-		q.mapId = id;
-		q.fetchingDataRows = false;
-
-		return q;
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new SingleMapIdSpec(id);
+		return new SelectById<>(root, idSpec);
 	}
 
 	public static <T> SelectById<T> query(Class<T> entityType, ObjectId id) {
 		checkObjectId(id);
+		QueryRoot root = resolver -> resolver.getObjEntity(id.getEntityName());
+		IdSpec idSpec = new SingleMapIdSpec(id.getIdSnapshot());
+		return new SelectById<>(root, idSpec);
+	}
 
-		SelectById<T> q = new SelectById<>();
-
-		q.entityName = id.getEntityName();
-		q.mapId = id.getIdSnapshot();
-		q.fetchingDataRows = false;
+	/**
+	 * @since 4.2
+	 */
+	public static <T> SelectById<T> query(Class<T> entityType, Object firstId, Object... otherIds) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new MultiScalarIdSpec(firstId, otherIds);
+		return new SelectById<>(root, idSpec);
+	}
 
-		return q;
+	/**
+	 * @since 4.2
+	 */
+	public static <T> SelectById<T> query(Class<T> entityType, Collection<Object> ids) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new MultiScalarIdSpec(ids);
+		return new SelectById<>(root, idSpec);
 	}
 
-	public static SelectById<DataRow> dataRowQuery(Class<?> entityType, Object id) {
-		SelectById<DataRow> q = new SelectById<>();
+	/**
+	 * @since 4.2
+	 */
+	@SafeVarargs
+	public static <T> SelectById<T> query(Class<T> entityType, Map<String, ?> firstId, Map<String, ?>... otherIds) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new MultiMapIdSpec(firstId, otherIds);
+		return new SelectById<>(root, idSpec);
+	}
 
-		q.entityType = entityType;
-		q.singleId = id;
-		q.fetchingDataRows = true;
+	/**
+	 * @since 4.2
+	 */
+	public static <T> SelectById<T> query(Class<T> entityType, ObjectId firstId, ObjectId... otherIds) {
+		checkObjectId(firstId);
+		for(ObjectId id : otherIds) {
+			checkObjectId(id, firstId.getEntityName());
+		}
 
-		return q;
+		QueryRoot root = resolver -> resolver.getObjEntity(firstId.getEntityName());
+		IdSpec idSpec = new MultiMapIdSpec(firstId, otherIds);
+		return new SelectById<>(root, idSpec);
 	}
 
-	public static SelectById<DataRow> dataRowQuery(Class<?> entityType, Map<String, Object> id) {
-		SelectById<DataRow> q = new SelectById<>();
-
-		q.entityType = entityType;
-		q.mapId = id;
-		q.fetchingDataRows = true;
+	public static SelectById<DataRow> dataRowQuery(Class<?> entityType, Object id) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new SingleScalarIdSpec(id);
+		return new SelectById<>(root, idSpec, true);
+	}
 
-		return q;
+	public static SelectById<DataRow> dataRowQuery(Class<?> entityType, Map<String, ?> id) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new SingleMapIdSpec(id);
+		return new SelectById<>(root, idSpec, true);
 	}
 
 	public static SelectById<DataRow> dataRowQuery(ObjectId id) {
 		checkObjectId(id);
+		QueryRoot root = resolver -> resolver.getObjEntity(id.getEntityName());
+		IdSpec idSpec = new SingleMapIdSpec(id.getIdSnapshot());
+		return new SelectById<>(root, idSpec, true);
+	}
 
-		SelectById<DataRow> q = new SelectById<>();
-
-		q.entityName = id.getEntityName();
-		q.mapId = id.getIdSnapshot();
-		q.fetchingDataRows = true;
+	/**
+	 * @since 4.2
+	 */
+	public static SelectById<DataRow> dataRowQuery(Class<?> entityType, Object firstId, Object... otherIds) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new MultiScalarIdSpec(firstId, otherIds);
+		return new SelectById<>(root, idSpec, true);
+	}
 
-		return q;
+	/**
+	 * @since 4.2
+	 */
+	@SafeVarargs
+	public static SelectById<DataRow> dataRowQuery(Class<?> entityType, Map<String, ?> firstId, Map<String, ?>... otherIds) {
+		QueryRoot root = resolver -> resolver.getObjEntity(entityType, true);
+		IdSpec idSpec = new MultiMapIdSpec(firstId, otherIds);
+		return new SelectById<>(root, idSpec, true);
 	}
 
-	private static void checkObjectId(ObjectId id) {
-		if (id.isTemporary() && !id.isReplacementIdAttached()) {
-			throw new CayenneRuntimeException("Can't build a query for temporary id: %s", id);
+	/**
+	 * @since 4.2
+	 */
+	public static SelectById<DataRow> dataRowQuery(ObjectId firstId, ObjectId... otherIds) {
+		checkObjectId(firstId);
+		for(ObjectId id : otherIds) {
+			checkObjectId(id, firstId.getEntityName());
 		}
+
+		QueryRoot root = resolver -> resolver.getObjEntity(firstId.getEntityName());
+		IdSpec idSpec = new MultiMapIdSpec(firstId, otherIds);
+		return new SelectById<>(root, idSpec, true);
+	}
+
+	protected SelectById(QueryRoot root, IdSpec idSpec, boolean fetchingDataRows) {
+		this.root = root;
+		this.idSpec = idSpec;
+		this.fetchingDataRows = fetchingDataRows;
+	}
+
+	protected SelectById(QueryRoot root, IdSpec idSpec) {
+		this(root, idSpec, false);
 	}
 
 	@Override
@@ -284,14 +333,12 @@ public class SelectById<T> extends IndirectQuery implements Select<T> {
 	@SuppressWarnings("deprecation")
 	@Override
 	protected Query createReplacementQuery(EntityResolver resolver) {
-
-		ObjEntity entity = resolveEntity(resolver);
-		Map<String, ?> id = resolveId(entity);
+		ObjEntity entity = root.resolve(resolver);
 
 		SelectQuery<Object> query = new SelectQuery<>();
 		query.setRoot(entity);
 		query.setFetchingDataRows(fetchingDataRows);
-		query.setQualifier(matchAllDbExp(id, Expression.EQUAL_TO));
+		query.setQualifier(idSpec.getQualifier(entity));
 
 		// note on caching... this hits query cache instead of object cache...
 		// until we merge the two this may result in not using the cache
@@ -303,31 +350,112 @@ public class SelectById<T> extends IndirectQuery implements Select<T> {
 		return query;
 	}
 
-	protected Map<String, ?> resolveId(ObjEntity entity) {
+	private static String resolveSinglePkName(ObjEntity entity) {
+		Collection<String> pkAttributes = entity.getPrimaryKeyNames();
+		if(pkAttributes.size() == 1) {
+			return pkAttributes.iterator().next();
+		}
+		throw new CayenneRuntimeException("PK contains %d columns, expected 1.",  pkAttributes.size());
+	}
 
-		if (singleId == null && mapId == null) {
-			throw new CayenneRuntimeException("Misconfigured query. Either singleId or mapId must be set");
+	private static void checkObjectId(ObjectId id) {
+		if (id.isTemporary() && !id.isReplacementIdAttached()) {
+			throw new CayenneRuntimeException("Can't build a query for a temporary id: %s", id);
 		}
+	}
 
-		if (mapId != null) {
-			return mapId;
+	private static void checkObjectId(ObjectId id, String entityName) {
+		checkObjectId(id);
+		if(!entityName.equals(id.getEntityName())) {
+			throw new CayenneRuntimeException("Can't build a query with mixed object types for given ObjectIds");
 		}
+	}
 
-		Collection<String> pkAttributes = entity.getPrimaryKeyNames();
-		if (pkAttributes.size() != 1) {
-			throw new CayenneRuntimeException("PK contains %d columns, expected 1.",  pkAttributes.size());
+	@SafeVarargs
+	private static <E, R> Collection<R> foldArguments(Function<E, R> mapper, E first, E... other) {
+		List<R> result = new ArrayList<>();
+		result.add(mapper.apply(first));
+		for(E next : other) {
+			result.add(mapper.apply(next));
+		}
+		return result;
+	}
+
+	protected interface QueryRoot extends Serializable {
+		ObjEntity resolve(EntityResolver resolver);
+	}
+
+	protected interface IdSpec extends Serializable{
+		Expression getQualifier(ObjEntity entity);
+	}
+
+	protected static class SingleScalarIdSpec implements IdSpec {
+
+		private final Object id;
+
+		protected SingleScalarIdSpec(Object id) {
+			this.id = id;
 		}
 
-		String pk = pkAttributes.iterator().next();
-		return singletonMap(pk, singleId);
+		@Override
+		public Expression getQualifier(ObjEntity entity) {
+			return matchDbExp(resolveSinglePkName(entity), id);
+		}
 	}
 
-	protected ObjEntity resolveEntity(EntityResolver resolver) {
+	protected static class MultiScalarIdSpec implements IdSpec {
 
-		if (entityName == null && entityType == null) {
-			throw new CayenneRuntimeException("Misconfigured query. Either entityName or entityType must be set");
+		private final Collection<Object> ids;
+
+		protected MultiScalarIdSpec(Object firstId, Object... otherIds) {
+			this.ids = foldArguments(Function.identity(), firstId, otherIds);
 		}
 
-		return entityName != null ? resolver.getObjEntity(entityName) : resolver.getObjEntity(entityType, true);
+		protected MultiScalarIdSpec(Collection<Object> ids) {
+			this.ids = ids;
+		}
+
+		@Override
+		public Expression getQualifier(ObjEntity entity) {
+			return inDbExp(resolveSinglePkName(entity), ids);
+		}
+	}
+
+	protected static class SingleMapIdSpec implements IdSpec {
+
+		private final Map<String, ?> id;
+
+		protected SingleMapIdSpec(Map<String, ?> id) {
+			this.id = id;
+		}
+
+		@Override
+		public Expression getQualifier(ObjEntity entity) {
+			return matchAllDbExp(id, Expression.EQUAL_TO);
+		}
+	}
+
+	protected static class MultiMapIdSpec implements IdSpec {
+
+		private final Collection<Map<String, ?>> ids;
+
+		@SafeVarargs
+		protected MultiMapIdSpec(Map<String, ?> firstId, Map<String, ?>... otherIds) {
+			this.ids = foldArguments(Function.identity(), firstId, otherIds);
+		}
+
+		protected MultiMapIdSpec(ObjectId firstId, ObjectId... otherIds) {
+			this.ids = foldArguments(ObjectId::getIdSnapshot, firstId, otherIds);
+		}
+
+		@Override
+		public Expression getQualifier(ObjEntity entity) {
+			List<Expression> expressions = new ArrayList<>();
+			for(Map<String, ?> id : ids) {
+				expressions.add(matchAllDbExp(id, Expression.EQUAL_TO));
+			}
+
+			return or(expressions);
+		}
 	}
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
index 9dfc5db..74dd0f4 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
@@ -18,13 +18,11 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
-import static java.util.Collections.singletonMap;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-
 import java.sql.Types;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.ObjectContext;
@@ -36,13 +34,16 @@ import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.testdo.testmap.Painting;
 import org.apache.cayenne.unit.di.DataChannelInterceptor;
-import org.apache.cayenne.unit.di.UnitTestClosure;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Before;
 import org.junit.Test;
 
+import static java.util.Collections.singletonMap;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class SelectById_RunIT extends ServerCase {
 
@@ -85,7 +86,27 @@ public class SelectById_RunIT extends ServerCase {
 		assertNotNull(a2);
 		assertEquals("artist2", a2.getArtistName());
 	}
-	
+
+	@Test
+	public void testIntPkMulti() throws Exception {
+		createTwoArtists();
+
+		List<Artist> artists = SelectById.query(Artist.class, 2, 3)
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(Artist.class));
+	}
+
+	@Test
+	public void testIntPkCollection() throws Exception {
+		createTwoArtists();
+
+		List<Artist> artists = SelectById.query(Artist.class, Arrays.asList(1, 2, 3, 4, 5))
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(Artist.class));
+	}
+
 	@Test
 	public void testIntPk_SelectFirst() throws Exception {
 		createTwoArtists();
@@ -113,6 +134,19 @@ public class SelectById_RunIT extends ServerCase {
 	}
 
 	@Test
+	public void testMapPkMulti() throws Exception {
+		createTwoArtists();
+
+		Map<String, ?> id2 = Collections.singletonMap(Artist.ARTIST_ID_PK_COLUMN, 2);
+		Map<String, ?> id3 = Collections.singletonMap(Artist.ARTIST_ID_PK_COLUMN, 3);
+
+		List<Artist> artists = SelectById.query(Artist.class, id2, id3)
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(Artist.class));
+	}
+
+	@Test
 	public void testObjectIdPk() throws Exception {
 		createTwoArtists();
 
@@ -128,6 +162,19 @@ public class SelectById_RunIT extends ServerCase {
 	}
 
 	@Test
+	public void testObjectIdPkMulti() throws Exception {
+		createTwoArtists();
+
+		ObjectId oid2 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
+		ObjectId oid3 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
+
+		List<Artist> artists = SelectById.query(Artist.class, oid2, oid3)
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(Artist.class));
+	}
+
+	@Test
 	public void testDataRowIntPk() throws Exception {
 		createTwoArtists();
 
@@ -141,6 +188,73 @@ public class SelectById_RunIT extends ServerCase {
 	}
 
 	@Test
+	public void testDataRowMapPk() throws Exception {
+		createTwoArtists();
+
+		Map<String, ?> id3 = Collections.singletonMap(Artist.ARTIST_ID_PK_COLUMN, 3);
+		DataRow a3 = SelectById.dataRowQuery(Artist.class, id3).selectOne(context);
+		assertNotNull(a3);
+		assertEquals("artist3", a3.get("ARTIST_NAME"));
+
+		Map<String, ?> id2 = Collections.singletonMap(Artist.ARTIST_ID_PK_COLUMN, 2);
+		DataRow a2 = SelectById.dataRowQuery(Artist.class, id2).selectOne(context);
+		assertNotNull(a2);
+		assertEquals("artist2", a2.get("ARTIST_NAME"));
+	}
+
+	@Test
+	public void testDataRowObjectIdPk() throws Exception {
+		createTwoArtists();
+
+		ObjectId oid3 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
+		DataRow a3 = SelectById.dataRowQuery(oid3).selectOne(context);
+		assertNotNull(a3);
+		assertEquals("artist3", a3.get("ARTIST_NAME"));
+
+		ObjectId oid2 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
+		DataRow a2 = SelectById.dataRowQuery(oid2).selectOne(context);
+		assertNotNull(a2);
+		assertEquals("artist2", a2.get("ARTIST_NAME"));
+	}
+
+	@Test
+	public void testDataRowIntPkMulti() throws Exception {
+		createTwoArtists();
+
+		List<DataRow> artists = SelectById.dataRowQuery(Artist.class, 2, 3)
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(DataRow.class));
+	}
+
+	@Test
+	public void testDataRowMapPkMulti() throws Exception {
+		createTwoArtists();
+
+		ObjectId oid2 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
+		ObjectId oid3 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
+
+		List<DataRow> artists = SelectById.dataRowQuery(oid2, oid3)
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(DataRow.class));
+	}
+
+	@Test
+	public void testDataRowObjectIdPkMulti() throws Exception {
+		createTwoArtists();
+
+		Map<String, ?> id2 = Collections.singletonMap(Artist.ARTIST_ID_PK_COLUMN, 2);
+		Map<String, ?> id3 = Collections.singletonMap(Artist.ARTIST_ID_PK_COLUMN, 3);
+
+		List<DataRow> artists = SelectById.dataRowQuery(Artist.class, id2, id3)
+				.select(context);
+		assertEquals(2, artists.size());
+		assertThat(artists.get(0), instanceOf(DataRow.class));
+	}
+
+
+	@Test
 	public void testMetadataCacheKey() throws Exception {
 		SelectById<Painting> q1 = SelectById.query(Painting.class, 4).localCache();
 		QueryMetadata md1 = q1.getMetaData(resolver);
@@ -170,13 +284,13 @@ public class SelectById_RunIT extends ServerCase {
 		assertNotEquals(md1.getCacheKey(), md4.getCacheKey());
 
 		SelectById<Painting> q5 = SelectById
-				.query(Painting.class, ObjectId.of("Painting", Painting.PAINTING_ID_PK_COLUMN, 4)).localCache();
+				.query(Painting.class, ObjectId.of("Painting", Painting.PAINTING_ID_PK_COLUMN, 4))
+				.localCache();
 		QueryMetadata md5 = q5.getMetaData(resolver);
 		assertNotNull(md5);
 		assertNotNull(md5.getCacheKey());
 
-		// this query is just a different form of q1, so should hit the same
-		// cache entry
+		// this query is just a different form of q1, so should hit the same cache entry
 		assertEquals(md1.getCacheKey(), md5.getCacheKey());
 	}
 
@@ -186,34 +300,21 @@ public class SelectById_RunIT extends ServerCase {
 
 		final Artist[] a3 = new Artist[1];
 
-		assertEquals(1, interceptor.runWithQueryCounter(new UnitTestClosure() {
-
-			@Override
-			public void execute() {
-				a3[0] = SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
-				assertNotNull(a3[0]);
-				assertEquals("artist3", a3[0].getArtistName());
-			}
+		assertEquals(1, interceptor.runWithQueryCounter(() -> {
+			a3[0] = SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
+			assertNotNull(a3[0]);
+			assertEquals("artist3", a3[0].getArtistName());
 		}));
 
-		interceptor.runWithQueriesBlocked(new UnitTestClosure() {
-
-			@Override
-			public void execute() {
-				Artist a3cached = SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
-				assertSame(a3[0], a3cached);
-			}
+		interceptor.runWithQueriesBlocked(() -> {
+			Artist a3cached = SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
+			assertSame(a3[0], a3cached);
 		});
 
 		context.performGenericQuery(new RefreshQuery("g1"));
 
-		assertEquals(1, interceptor.runWithQueryCounter(new UnitTestClosure() {
-
-			@Override
-			public void execute() {
-				SelectById.query(Artist.class, 3).localCache("g1").selectOne(context);
-			}
-		}));
+		assertEquals(1, interceptor.runWithQueryCounter(() ->
+				SelectById.query(Artist.class, 3).localCache("g1").selectOne(context)));
 	}
 
 	@Test
@@ -222,19 +323,17 @@ public class SelectById_RunIT extends ServerCase {
 		tPainting.insert(45, 3, "One");
 		tPainting.insert(48, 3, "Two");
 
-		final Artist a3 = SelectById.query(Artist.class, 3).prefetch(Artist.PAINTING_ARRAY.joint()).selectOne(context);
-
-		interceptor.runWithQueriesBlocked(new UnitTestClosure() {
+		final Artist a3 = SelectById.query(Artist.class, 3)
+				.prefetch(Artist.PAINTING_ARRAY.joint())
+				.selectOne(context);
 
-			@Override
-			public void execute() {
-				assertNotNull(a3);
-				assertEquals("artist3", a3.getArtistName());
-				assertEquals(2, a3.getPaintingArray().size());
+		interceptor.runWithQueriesBlocked(() -> {
+			assertNotNull(a3);
+			assertEquals("artist3", a3.getArtistName());
+			assertEquals(2, a3.getPaintingArray().size());
 
-				a3.getPaintingArray().get(0).getPaintingTitle();
-				a3.getPaintingArray().get(1).getPaintingTitle();
-			}
+			a3.getPaintingArray().get(0).getPaintingTitle();
+			a3.getPaintingArray().get(1).getPaintingTitle();
 		});
 	}
 }