Skip to content

Commit 2f5ecfd

Browse files
committed
Migrated to pure Java CompletableFuture<List<V>>
1 parent c56ed84 commit 2f5ecfd

File tree

11 files changed

+303
-283
lines changed

11 files changed

+303
-283
lines changed

README.md

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
[ ![Download](https://api.bintray.com/packages/bbakerman/maven/java-dataloader/images/download.svg) ](https://bintray.com/bbakerman/maven/java-dataloader/_latestVersion)
66

77

8-
This small and simple utility library is a Pure Java 8 port of [Facebook DataLoader](https://github.com/facebook/dataloader).
8+
This small and simple utility library is a pure Java 8 port of [Facebook DataLoader](https://github.com/facebook/dataloader).
99

1010
It can serve as integral part of your application's data layer to provide a
1111
consistent API over various back-ends and reduce message communication overhead through batching and caching.
1212

1313
An important use case for `java-dataloader` is improving the efficiency of GraphQL query execution. Graphql fields
1414
are resolved in a independent manner and with a true graph of objects, you may be fetching the same object many times.
15-
Also a naive implementation of graphql data fetchers can easily lead to the dreaded "n+1" fetch problem.
15+
A naive implementation of graphql data fetchers can easily lead to the dreaded "n+1" fetch problem.
1616

1717
There are many other use cases where you can also benefit from using this utility.
1818

@@ -67,15 +67,13 @@ a list of keys
6767

6868
BatchLoader<Long, User> userBatchLoader = new BatchLoader<Long, User>() {
6969
@Override
70-
public PromisedValues<User> load(List<Long> userIds) {
71-
List<CompletableFuture<User>> futures = userIds.stream()
72-
.map(userId ->
73-
CompletableFuture.supplyAsync(() ->
74-
userManager.loadUsersById(userId)))
75-
.collect(Collectors.toList());
76-
return PromisedValues.allOf(futures);
70+
public CompletionStage<List<User>> load(List<Long> userIds) {
71+
return CompletableFuture.supplyAsync(() -> {
72+
return userManager.loadUsersById(userIds);
73+
});
7774
}
7875
};
76+
7977
DataLoader<Long, User> userLoader = new DataLoader<>(userBatchLoader);
8078

8179
```
@@ -88,6 +86,7 @@ You can then use it to load values which will be `CompleteableFuture` promises t
8886

8987
or you can use it to compose future computations as follows. The key requirement is that you call
9088
`dataloader.dispatch()` or its variant `dataloader.dispatchAndJoin()` at some point in order to make the underlying calls happen to the batch loader.
89+
9190
In this version of data loader, this does not happen automatically. More on this in [Manual dispatching](#manual-dispatching) .
9291

9392
```java
@@ -126,12 +125,47 @@ maintain minimal outgoing data requests.
126125

127126
In the example above, the first call to dispatch will cause the batched user keys (1 and 2) to be fired at the BatchLoader function to load 2 users.
128127

129-
Since each `thenAccept` callback made more calls to `userLoader` to get the "user they they invited", another 2 user keys are fired at the BatchLoader function for them.
128+
Since each `thenAccept` callback made more calls to `userLoader` to get the "user they they invited", another 2 user keys are given at the `BatchLoader`
129+
function for them.
130130

131-
In this case the `userLoader.dispatchAndJoin()` is used to fire a dispatch call, wait for it (aka join it), see if the data loader has more batched entries, (which is does)
132-
and then it repeats this until the data loader internal queue of keys is empty. At this point we have made 2 batched calls instead of the niave 4 calls we might have made if
131+
In this case the `userLoader.dispatchAndJoin()` is used to make a dispatch call, wait for it (aka join it), see if the data loader has more batched entries, (which is does)
132+
and then it repeats this until the data loader internal queue of keys is empty. At this point we have made 2 batched calls instead of the naive 4 calls we might have made if
133133
we did not "batch" the calls to load data.
134134

135+
## Batching requires batch backing APIs
136+
137+
You will notice in our BatchLoader example that the backing service had the ability to get a list of users give a list of user ids in one call.
138+
139+
```java
140+
public CompletionStage<List<User>> load(List<Long> userIds) {
141+
return CompletableFuture.supplyAsync(() -> {
142+
return userManager.loadUsersById(userIds);
143+
});
144+
}
145+
```
146+
147+
This is important consideration. By using `dataloader` you have batched up the request for N keys in a list of keys that can be retrieved at one time.
148+
If you don't have batch backing services, then you cant be as efficient as possible if you then have to make N calls for each key.
149+
150+
```java
151+
BatchLoader<Long, User> lessEfficientUserBatchLoader = new BatchLoader<Long, User>() {
152+
@Override
153+
public CompletionStage<List<User>> load(List<Long> userIds) {
154+
return CompletableFuture.supplyAsync(() -> {
155+
//
156+
// notice how it makes N calls to load by single user id out of the batch of N keys
157+
//
158+
return userIds.stream()
159+
.map(id -> userManager.loadUserById(id))
160+
.collect(Collectors.toList());
161+
});
162+
}
163+
};
164+
165+
```
166+
167+
That said, with key caching turn on (the default), it may still be more efficient using `dataloader` than without it
168+
135169
## Differences to reference implementation
136170

137171
### Manual dispatching
@@ -162,6 +196,23 @@ and there are also gains to this different mode of operation:
162196
However, with batch execution control comes responsibility! If you forget to make the call to `dispatch()` then the futures
163197
in the load request queue will never be batched, and thus _will never complete_! So be careful when crafting your loader designs.
164198

199+
### Error object is not a thing in Java
200+
201+
In the reference JS implementation if the batch loader returns an `Error` object back then the `loadKey()` promise is rejected
202+
with that error. This allows fine grain (per object in the list) sets of error. If I ask for keys A,B,C and B errors out the promise
203+
for B can contain a specific eror.
204+
205+
This is not (easily) possible in a Java implementation
206+
207+
A batch loader function is defined as `BatchLoader<K, V>` meaning for a key of type `K` it returns a value of type `V`. It can just return
208+
some `Exception` as an object of type `V` since Java is type safe.
209+
210+
We could have made it return an `List<Either<V,Exception>>` but this greatly complicates the method signatures and so this aspect was not ported.
211+
212+
Instead when the batch loader function is called, it returns a promise of `List<V>` and if that promise fails in aggregate then that is what
213+
is reported back.
214+
215+
165216
## Let's get started!
166217

167218
### Installing
@@ -214,13 +265,15 @@ itself. All the heavy lifting has been done by this project : [vertx-dataloader
214265
including the extensive testing.
215266

216267
This particular port was done to reduce the dependency on Vertx and to write a pure Java 8 implementation with no dependencies and also
217-
to use the more normative Java CompletableFuture. [vertx-core](http://vertx.io/docs/vertx-core/java/) is not a lightweight library by any means
268+
to use the more normative Java CompletableFuture.
269+
270+
[vertx-core](http://vertx.io/docs/vertx-core/java/) is not a lightweight library by any means
218271
so having a pure Java 8 implementation is very desirable.
219272

220273

221274
This library is entirely inspired by the great works of [Lee Byron](https://github.com/leebyron) and
222275
[Nicholas Schrock](https://github.com/schrockn) from [Facebook](https://www.facebook.com/) whom we would like to thank, and
223-
especially @leebyron for taking the time and effort to provide 100% coverage on the codebase. A set of tests which
276+
especially @leebyron for taking the time and effort to provide 100% coverage on the codebase. The original set of tests
224277
were also ported.
225278

226279

src/main/java/org/dataloader/BatchLoader.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,27 @@
1717
package org.dataloader;
1818

1919
import java.util.List;
20+
import java.util.concurrent.CompletionStage;
2021

2122
/**
2223
* A function that is invoked for batch loading a list of data values indicated by the provided list of keys. The
23-
* function returns a {@link PromisedValues} to aggregate results of individual load requests.
24+
* function returns a promise of a list of results of individual load requests.
2425
*
2526
* There are a few constraints that must be upheld:
2627
* <ul>
2728
* <li>The list of values must be the same size as the list of keys.</li>
2829
* <li>Each index in the list of values must correspond to the same index in the list of keys.</li>
2930
* </ul>
3031
*
31-
* For example, if your batch function was provided the list of keys: [ 2, 9, 6, 1 ], and loading from a back-end service returned the values:
32+
* For example, if your batch function was provided the list of keys:
33+
*
34+
* <pre>
35+
* [
36+
* 2, 9, 6, 1
37+
* ]
38+
* </pre>
39+
*
40+
* and loading from a back-end service returned this list of values:
3241
*
3342
* <pre>
3443
* [
@@ -38,10 +47,13 @@
3847
* ]
3948
* </pre>
4049
*
41-
* The back-end service returned results in a different order than we requested, likely because it was more efficient for it to do so. Also, it omitted a result for key 6, which we can interpret as no value
42-
* existing for that key.
50+
* then the batch loader function contract has been broken.
51+
*
52+
* The back-end service returned results in a different order than we requested, likely because it was more efficient for it to
53+
* do so. Also, it omitted a result for key 6, which we may interpret as no value existing for that key.
4354
*
44-
* To uphold the constraints of the batch function, it must return an List of values the same length as the List of keys, and re-order them to ensure each index aligns with the original keys [ 2, 9, 6, 1 ]:
55+
* To uphold the constraints of the batch function, it must return an List of values the same length as
56+
* the List of keys, and re-order them to ensure each index aligns with the original keys [ 2, 9, 6, 1 ]:
4557
*
4658
* <pre>
4759
* [
@@ -56,16 +68,17 @@
5668
* @param <V> type parameter indicating the type of values returned
5769
*
5870
* @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
71+
* @author <a href="https://github.com/bbakerman/">Brad Baker</a>
5972
*/
6073
@FunctionalInterface
6174
public interface BatchLoader<K, V> {
6275

6376
/**
64-
* Called to batch load the provided keys and return a {@link PromisedValues} which is a promise to a list of values
77+
* Called to batch load the provided keys and return a promise to a list of values
6578
*
6679
* @param keys the collection of keys to load
6780
*
6881
* @return a promise of the values for those keys
6982
*/
70-
PromisedValues<V> load(List<K> keys);
83+
CompletionStage<List<V>> load(List<K> keys);
7184
}

src/main/java/org/dataloader/CacheMap.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @param <V> type parameter indicating the type of the data that is cached
3434
*
3535
* @author <a href="https://github.com/aschrijver/">Arnold Schrijver</a>
36+
* @author <a href="https://github.com/bbakerman/">Brad Baker</a>
3637
*/
3738
public interface CacheMap<U, V> {
3839

@@ -41,6 +42,7 @@ public interface CacheMap<U, V> {
4142
*
4243
* @param <U> type parameter indicating the type of the cache keys
4344
* @param <V> type parameter indicating the type of the data that is cached
45+
*
4446
* @return the cache map
4547
*/
4648
static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
@@ -51,6 +53,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
5153
* Checks whether the specified key is contained in the cach map.
5254
*
5355
* @param key the key to check
56+
*
5457
* @return {@code true} if the cache contains the key, {@code false} otherwise
5558
*/
5659
boolean containsKey(U key);
@@ -62,6 +65,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
6265
* so be sure to check {@link CacheMap#containsKey(Object)} first.
6366
*
6467
* @param key the key to retrieve
68+
*
6569
* @return the cached value, or {@code null} if not found (depends on cache implementation)
6670
*/
6771
V get(U key);
@@ -71,6 +75,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
7175
*
7276
* @param key the key to cache
7377
* @param value the value to cache
78+
*
7479
* @return the cache map for fluent coding
7580
*/
7681
CacheMap<U, V> set(U key, V value);
@@ -79,6 +84,7 @@ static <U, V> CacheMap<U, CompletableFuture<V>> simpleMap() {
7984
* Deletes the entry with the specified key from the cache map, if it exists.
8085
*
8186
* @param key the key to delete
87+
*
8288
* @return the cache map for fluent coding
8389
*/
8490
CacheMap<U, V> delete(U key);

0 commit comments

Comments
 (0)