Skip to content

Commit d579ef0

Browse files
committed
Added map batch loaders so that you can be more natural in getting less results than asked for
1 parent a9ea42a commit d579ef0

File tree

11 files changed

+664
-94
lines changed

11 files changed

+664
-94
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,42 @@ credentials or the database connection parameters. You can do this by implement
194194
The batch loading code will now receive this context object and it can be used to get to data layers or
195195
to connect to other systems.
196196

197+
### Returning a Map of results from your batch loader
198+
199+
Often there is not a 1:1 mapping of your batch loaded keys to the values returned.
200+
201+
For example, let's assume you want to load users from a database, you could probably use a query that looks like this:
202+
203+
```sql
204+
SELECT * FROM User WHERE id IN (keys)
205+
```
206+
207+
Given say 10 user id keys you might only get 7 results back. This can be more naturally represented in a map
208+
than in an order list of values when returning values from the batch loader function.
209+
210+
You can use `org.dataloader.MapBatchLoader` for this purpose.
211+
212+
When the map is processed by the `DataLoader` code, any keys that are missing in the map
213+
will be replaced with null values. The semantics that the number of `DataLoader.load` requests
214+
are matched with values is kept.
215+
216+
Your keys provided MUST be first class keys since they will be used to examine the returned map and
217+
create the list of results, with nulls filling in for missing values.
218+
219+
```java
220+
MapBatchLoader<Long, User> mapBatchLoader = new MapBatchLoader<Long, User>() {
221+
@Override
222+
public CompletionStage<Map<Long, User>> load(List<Long> userIds, Object context) {
223+
SecurityCtx callCtx = (SecurityCtx) context;
224+
return CompletableFuture.supplyAsync(() -> userManager.loadMapOfUsersById(callCtx, userIds));
225+
}
226+
};
227+
228+
DataLoader<Long, User> userLoader = DataLoader.newDataLoader(mapBatchLoader);
229+
230+
// ...
231+
```
232+
197233
### Error object is not a thing in a type safe Java world
198234

199235
In the reference JS implementation if the batch loader returns an `Error` object back from the `load()` promise is rejected

src/main/java/org/dataloader/DataLoader.java

Lines changed: 167 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.ArrayList;
2525
import java.util.Collection;
2626
import java.util.List;
27+
import java.util.Map;
2728
import java.util.concurrent.CompletableFuture;
2829
import java.util.concurrent.CompletionStage;
2930
import java.util.stream.Collectors;
@@ -63,6 +64,7 @@
6364
public class DataLoader<K, V> {
6465

6566
private final BatchLoader<K, V> batchLoadFunction;
67+
private final MapBatchLoader<K, V> mapBatchLoadFunction;
6668
private final DataLoaderOptions loaderOptions;
6769
private final CacheMap<Object, CompletableFuture<V>> futureCache;
6870
private final List<SimpleImmutableEntry<K, CompletableFuture<V>>> loaderQueue;
@@ -96,6 +98,35 @@ public static <K, V> DataLoader<K, V> newDataLoader(BatchLoader<K, V> batchLoadF
9698
return new DataLoader<>(batchLoadFunction, options);
9799
}
98100

101+
/**
102+
* Creates new DataLoader with the specified map batch loader function and default options
103+
* (batching, caching and unlimited batch size).
104+
*
105+
* @param mapBatchLoaderFunction the batch load function to use
106+
* @param <K> the key type
107+
* @param <V> the value type
108+
*
109+
* @return a new DataLoader
110+
*/
111+
public static <K, V> DataLoader<K, V> newDataLoader(MapBatchLoader<K, V> mapBatchLoaderFunction) {
112+
return newDataLoader(mapBatchLoaderFunction, null);
113+
}
114+
115+
/**
116+
* Creates new DataLoader with the specified map batch loader function with the provided options
117+
*
118+
* @param mapBatchLoaderFunction the batch load function to use
119+
* @param options the options to use
120+
* @param <K> the key type
121+
* @param <V> the value type
122+
*
123+
* @return a new DataLoader
124+
*/
125+
public static <K, V> DataLoader<K, V> newDataLoader(MapBatchLoader<K, V> mapBatchLoaderFunction, DataLoaderOptions options) {
126+
return new DataLoader<>(mapBatchLoaderFunction, options);
127+
}
128+
129+
99130
/**
100131
* Creates new DataLoader with the specified batch loader function and default options
101132
* (batching, caching and unlimited batch size) where the batch loader function returns a list of
@@ -134,6 +165,43 @@ public static <K, V> DataLoader<K, V> newDataLoaderWithTry(BatchLoader<K, Try<V>
134165
return new DataLoader<>((BatchLoader<K, V>) batchLoadFunction, options);
135166
}
136167

168+
/**
169+
* Creates new DataLoader with the specified map abatch loader function and default options
170+
* (batching, caching and unlimited batch size) where the batch loader function returns a list of
171+
* {@link org.dataloader.Try} objects.
172+
*
173+
* This allows you to capture both the value that might be returned and also whether exception that might have occurred getting that individual value. If its important you to
174+
* know gather exact status of each item in a batch call and whether it threw exceptions when fetched then
175+
* you can use this form to create the data loader.
176+
*
177+
* @param mapBatchLoaderFunction the map batch load function to use that uses {@link org.dataloader.Try} objects
178+
* @param <K> the key type
179+
* @param <V> the value type
180+
*
181+
* @return a new DataLoader
182+
*/
183+
public static <K, V> DataLoader<K, V> newDataLoaderWithTry(MapBatchLoader<K, Try<V>> mapBatchLoaderFunction) {
184+
return newDataLoaderWithTry(mapBatchLoaderFunction, null);
185+
}
186+
187+
/**
188+
* Creates new DataLoader with the specified map batch loader function and with the provided options
189+
* where the batch loader function returns a list of
190+
* {@link org.dataloader.Try} objects.
191+
*
192+
* @param mapBatchLoaderFunction the map batch load function to use that uses {@link org.dataloader.Try} objects
193+
* @param options the options to use
194+
* @param <K> the key type
195+
* @param <V> the value type
196+
*
197+
* @return a new DataLoader
198+
*
199+
* @see #newDataLoaderWithTry(MapBatchLoader)
200+
*/
201+
@SuppressWarnings("unchecked")
202+
public static <K, V> DataLoader<K, V> newDataLoaderWithTry(MapBatchLoader<K, Try<V>> mapBatchLoaderFunction, DataLoaderOptions options) {
203+
return new DataLoader<>((MapBatchLoader<K, V>) mapBatchLoaderFunction, options);
204+
}
137205

138206
/**
139207
* Creates a new data loader with the provided batch load function, and default options.
@@ -144,19 +212,53 @@ public DataLoader(BatchLoader<K, V> batchLoadFunction) {
144212
this(batchLoadFunction, null);
145213
}
146214

215+
/**
216+
* Creates a new data loader with the provided map batch load function.
217+
*
218+
* @param mapBatchLoadFunction the map batch load function to use
219+
*/
220+
public DataLoader(MapBatchLoader<K, V> mapBatchLoadFunction) {
221+
this(mapBatchLoadFunction, null);
222+
}
223+
147224
/**
148225
* Creates a new data loader with the provided batch load function and options.
149226
*
150227
* @param batchLoadFunction the batch load function to use
151228
* @param options the batch load options
152229
*/
153230
public DataLoader(BatchLoader<K, V> batchLoadFunction, DataLoaderOptions options) {
154-
this.batchLoadFunction = nonNull(batchLoadFunction);
155-
this.loaderOptions = options == null ? new DataLoaderOptions() : options;
231+
this.batchLoadFunction = batchLoadFunction;
232+
this.mapBatchLoadFunction = null;
233+
this.loaderOptions = determineOptions(options);
156234
this.futureCache = determineCacheMap(loaderOptions);
157235
// order of keys matter in data loader
158236
this.loaderQueue = new ArrayList<>();
159-
this.stats = nonNull(this.loaderOptions.getStatisticsCollector());
237+
this.stats = determineCollector(this.loaderOptions);
238+
}
239+
240+
/**
241+
* Creates a new data loader with the provided map batch load function and options.
242+
*
243+
* @param mapBatchLoadFunction the map batch load function to use
244+
* @param options the batch load options
245+
*/
246+
public DataLoader(MapBatchLoader<K, V> mapBatchLoadFunction, DataLoaderOptions options) {
247+
this.batchLoadFunction = null;
248+
this.mapBatchLoadFunction = mapBatchLoadFunction;
249+
this.loaderOptions = determineOptions(options);
250+
this.futureCache = determineCacheMap(loaderOptions);
251+
// order of keys matter in data loader
252+
this.loaderQueue = new ArrayList<>();
253+
this.stats = determineCollector(this.loaderOptions);
254+
}
255+
256+
private StatisticsCollector determineCollector(DataLoaderOptions loaderOptions) {
257+
return nonNull(loaderOptions.getStatisticsCollector());
258+
}
259+
260+
private DataLoaderOptions determineOptions(DataLoaderOptions options) {
261+
return options == null ? new DataLoaderOptions() : options;
160262
}
161263

162264
@SuppressWarnings("unchecked")
@@ -197,11 +299,7 @@ public CompletableFuture<V> load(K key) {
197299
stats.incrementBatchLoadCountBy(1);
198300
// immediate execution of batch function
199301
Object context = loaderOptions.getBatchContextProvider().get();
200-
CompletableFuture<List<V>> batchedLoad = batchLoadFunction
201-
.load(singletonList(key), context)
202-
.toCompletableFuture();
203-
future = batchedLoad
204-
.thenApply(list -> list.get(0));
302+
future = invokeLoaderImmediately(key, context);
205303
}
206304
if (cachingEnabled) {
207305
futureCache.set(cacheKey, future);
@@ -210,6 +308,7 @@ public CompletableFuture<V> load(K key) {
210308
}
211309
}
212310

311+
213312
/**
214313
* Requests to load the list of data provided by the specified keys asynchronously, and returns a composite future
215314
* of the resulting values.
@@ -232,6 +331,25 @@ public CompletableFuture<List<V>> loadMany(List<K> keys) {
232331
}
233332
}
234333

334+
private CompletableFuture<V> invokeLoaderImmediately(K key, Object context) {
335+
List<K> keys = singletonList(key);
336+
CompletionStage<V> singleLoadCall;
337+
if (isMapLoader()) {
338+
singleLoadCall = mapBatchLoadFunction
339+
.load(keys, context)
340+
.thenApply(map -> map.get(key));
341+
} else {
342+
singleLoadCall = batchLoadFunction
343+
.load(keys, context)
344+
.thenApply(list -> list.get(0));
345+
}
346+
return singleLoadCall.toCompletableFuture();
347+
}
348+
349+
private boolean isMapLoader() {
350+
return mapBatchLoadFunction != null;
351+
}
352+
235353
/**
236354
* Dispatches the queued load requests to the batch execution function and returns a promise of the result.
237355
* <p>
@@ -302,17 +420,11 @@ private CompletableFuture<List<V>> sliceIntoBatchesOfBatches(List<K> keys, List<
302420
@SuppressWarnings("unchecked")
303421
private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<CompletableFuture<V>> queuedFutures) {
304422
stats.incrementBatchLoadCountBy(keys.size());
305-
CompletionStage<List<V>> batchLoad;
306-
try {
307-
Object context = loaderOptions.getBatchContextProvider().get();
308-
batchLoad = nonNull(batchLoadFunction.load(keys, context), "Your batch loader function MUST return a non null CompletionStage promise");
309-
} catch (Exception e) {
310-
batchLoad = CompletableFutureKit.failedFuture(e);
311-
}
423+
CompletionStage<List<V>> batchLoad = invokeBatchFunction(keys);
312424
return batchLoad
313425
.toCompletableFuture()
314426
.thenApply(values -> {
315-
assertState(keys.size() == values.size(), "The size of the promised values MUST be the same size as the key list");
427+
assertResultSize(keys, values);
316428

317429
for (int idx = 0; idx < queuedFutures.size(); idx++) {
318430
Object value = values.get(idx);
@@ -351,6 +463,45 @@ private CompletableFuture<List<V>> dispatchQueueBatch(List<K> keys, List<Complet
351463
});
352464
}
353465

466+
private CompletionStage<List<V>> invokeBatchFunction(List<K> keys) {
467+
CompletionStage<List<V>> batchLoad;
468+
try {
469+
Object context = loaderOptions.getBatchContextProvider().get();
470+
if (isMapLoader()) {
471+
batchLoad = invokeMapBatchLoader(keys, context);
472+
} else {
473+
batchLoad = invokeListBatchLoader(keys, context);
474+
}
475+
} catch (Exception e) {
476+
batchLoad = CompletableFutureKit.failedFuture(e);
477+
}
478+
return batchLoad;
479+
}
480+
481+
private CompletionStage<List<V>> invokeListBatchLoader(List<K> keys, Object context) {
482+
return nonNull(batchLoadFunction.load(keys, context), "Your batch loader function MUST return a non null CompletionStage promise");
483+
}
484+
485+
/*
486+
* Turns a map of results that MAY be smaller than the key list back into a list by mapping null
487+
* to missing elements.
488+
*/
489+
private CompletionStage<List<V>> invokeMapBatchLoader(List<K> keys, Object context) {
490+
CompletionStage<Map<K, V>> mapBatchLoad = nonNull(mapBatchLoadFunction.load(keys, context), "Your batch loader function MUST return a non null CompletionStage promise");
491+
return mapBatchLoad.thenApply(map -> {
492+
List<V> values = new ArrayList<>();
493+
for (K key : keys) {
494+
V value = map.get(key);
495+
values.add(value);
496+
}
497+
return values;
498+
});
499+
}
500+
501+
private void assertResultSize(List<K> keys, List<V> values) {
502+
assertState(keys.size() == values.size(), "The size of the promised values MUST be the same size as the key list");
503+
}
504+
354505
/**
355506
* Normally {@link #dispatch()} is an asynchronous operation but this version will 'join' on the
356507
* results if dispatch and wait for them to complete. If the {@link CompletableFuture} callbacks make more
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2016 The original author or authors
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License v1.0
6+
* and Apache License v2.0 which accompanies this distribution.
7+
*
8+
* The Eclipse Public License is available at
9+
* http://www.eclipse.org/legal/epl-v10.html
10+
*
11+
* The Apache License v2.0 is available at
12+
* http://www.opensource.org/licenses/apache2.0.php
13+
*
14+
* You may elect to redistribute this code under either of these licenses.
15+
*/
16+
17+
package org.dataloader;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.CompletionStage;
22+
23+
/**
24+
* A function that is invoked for batch loading a map of of data values indicated by the provided list of keys. The
25+
* function returns a promise of a map of results of individual load requests.
26+
*
27+
* There are a few constraints that must be upheld:
28+
* <ul>
29+
* <li>The keys MUST be able to be first class keys in a Java map. Get your equals() and hashCode() methods in order</li>
30+
* <li>The caller of the {@link org.dataloader.DataLoader} that uses this batch loader function MUSt be able to cope with
31+
* null values coming back as results
32+
* </li>
33+
* <li>The function MUST be resilient to the same key being presented twice.</li>
34+
* </ul>
35+
*
36+
* This form is useful when you don't have a 1:1 mapping of keys to values or when null is an acceptable value for a missing value.
37+
*
38+
* For example, let's assume you want to load users from a database, you could probably use a query that looks like this:
39+
*
40+
* <pre>
41+
* SELECT * FROM User WHERE id IN (keys)
42+
* </pre>
43+
*
44+
* Given say 10 user id keys you might only get 7 results back. This can be more naturally represented in a map
45+
* than in an order list of values when returning values from the batch loader function.
46+
*
47+
* When the map is processed by the {@link org.dataloader.DataLoader} code, any keys that are missing in the map
48+
* will be replaced with null values. The semantics that the number of {@link org.dataloader.DataLoader#load(Object)} requests
49+
* are matched with values is kept.
50+
*
51+
* This means that if 10 keys are asked for then {@link DataLoader#dispatch()} will return a promise 10 value results and each
52+
* of the {@link org.dataloader.DataLoader#load(Object)} will complete with a value, null or an exception.
53+
*
54+
* When caching is disabled, its possible for the same key to be presented in the list of keys more than once. You map
55+
* batch loader function needs to be resilient to this situation.
56+
*
57+
* @param <K> type parameter indicating the type of keys to use for data load requests.
58+
* @param <V> type parameter indicating the type of values returned
59+
*
60+
* @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
61+
* @author <a href="https://github.com/bbakerman/">Brad Baker</a>
62+
*/
63+
@FunctionalInterface
64+
public interface MapBatchLoader<K, V> {
65+
66+
/**
67+
* Called to batch load the provided keys and return a promise to a map of values. It can be given a context object to
68+
* that maybe be useful during the call. A typical use case is passing in security credentials or database connection details say.
69+
*
70+
* @param keys the collection of keys to load
71+
* @param context a context object that can help with the call
72+
*
73+
* @return a promise to a map of values for those keys
74+
*/
75+
@SuppressWarnings("unused")
76+
CompletionStage<Map<K, V>> load(List<K> keys, Object context);
77+
}

0 commit comments

Comments
 (0)