Skip to content

Commit 931ac8b

Browse files
committed
Blog on DataFetcherResult
1 parent dab7d2c commit 931ac8b

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
+++
2+
title = "graphql-java - In Depth - Part 1: DataFetcherResult"
3+
author = "Brad Baker"
4+
tags = []
5+
categories = []
6+
date = 2019-04-11T00:00:00+10:00
7+
+++
8+
9+
# graphql-java - In Depth series
10+
11+
Welcome to the new series "graphql-java - In Depth" where we will explore more specific graphql-java topics.
12+
13+
# DataFetcherResult
14+
15+
Today we are looking into the `graphql.execution.DataFetcherResult` object.
16+
17+
# The scenario
18+
19+
But first lets set the scene. Imagine we have a system that can return `issues` and the `comments` on those `issues`
20+
21+
22+
{{< highlight Scala "linenos=table" >}}
23+
{
24+
issues {
25+
key
26+
summary
27+
comments {
28+
text
29+
}
30+
}
31+
{{< / highlight >}}
32+
33+
<p/>
34+
35+
Nominally we would have a `graphql.schema.DataFetcher` on `issues` that returns a list of issues and one on the field `comments` that returns the list of comments
36+
for each issue `source` object.
37+
38+
As you can see this naively creates an N+1 problem where we need to fetch data multiple times, one for each `issue` object in isolation.
39+
40+
We could attack this using the `org.dataloader.DataLoader` pattern but there is another way which will discuss in this article.
41+
42+
# Look ahead
43+
44+
The data fetcher behind the `issues` field is able to look ahead and see what sub fields are being asked for. In this case it knows that `comments` are being asked
45+
for and hence it could prefetch them at the same time.
46+
47+
`graphql.schema.DataFetchingEnvironment#getSelectionSet` can be used by data fetcher code to get the selection set of fields for a given parent field.
48+
49+
{{< highlight Java "linenos=table" >}}
50+
DataFetcher issueDataFetcher = environment -> {
51+
DataFetchingFieldSelectionSet selectionSet = environment.getSelectionSet();
52+
if (selectionSet.contains("comments")) {
53+
List<IssueAndCommentsDTO> data = getAllIssuesWithComments(environment, selectionSet.getFields());
54+
return data;
55+
} else {
56+
List<IssueDTO> issues = getAllIssuesWitNoComments(environment);
57+
return issues;
58+
}
59+
};
60+
{{< / highlight >}}
61+
62+
Imagine this is backed by an SQL system we might be able to use this field look ahead to produce the following SQL
63+
64+
{{< highlight Sql "linenos=table" >}}
65+
SELECT Issues.Key, Issues.Summary, Comments.Text
66+
FROM Issues
67+
INNER JOIN Comments ON Issues.CommentID=Comments.ID;
68+
{{< / highlight >}}
69+
70+
So we have looked ahead and returned different data depending on the field sub selection. We have made our system more efficient by using look ahead
71+
to fetch data just the 1 time and not N+1 times.
72+
73+
# Code Challenges
74+
75+
The challenge with this is that the shapes of the returned data is now field sub selection specific. We needed a `IssueAndCommentsDTO` for one sub selection
76+
path and a simpler `IssueDTO` for another path.
77+
78+
With enough paths this becomes problematic as it adds new DTO classes per path and makes out child data fetchers more complex
79+
80+
Also the standard graphql pattern is that the returned object becomes the `source` aka `graphql.schema.DataFetchingEnvironment#getSource` of the next child
81+
data fetcher. But we might have pre fetched data that is need 2 levels deep and this is challenging to do since each data fetcher would need to capture and copy
82+
that down to the layers below.
83+
84+
85+
# Passing Data and Local Context
86+
87+
graphql-java offers a capability that helps with this pattern. We go beyond what the reference graphql-js system gives you where the object you
88+
returned is automatically and only the `source` of the next child fetcher and that's it.
89+
90+
In graphql-java you can use `graphql.execution.DataFetcherResult` to return three sets of values
91+
92+
* `data` - which will be used as the source on the next set of sub fields
93+
* `errors` - allowing you to return data as well as errors
94+
* `localContext` - which allows you to pass down field specific context
95+
96+
In our example case we will be use `data` and `localContext` to communicate between fields easily.
97+
98+
99+
{{< highlight Java "linenos=table" >}}
100+
101+
DataFetcher issueDataFetcher = environment -> {
102+
DataFetchingFieldSelectionSet selectionSet = environment.getSelectionSet();
103+
if (selectionSet.contains("comments")) {
104+
List<IssueAndCommentsDTO> data = getAllIssuesWithComments(environment, selectionSet.getFields());
105+
106+
List<IssueDTO> issues = data.stream().map(dto -> dto.getIssue()).collect(toList());
107+
108+
Map<IssueDTO, List<CommentDTO>> preFetchedComments = mkMapOfComments(data);
109+
110+
return DataFetcherResult.newResult()
111+
.data(issues)
112+
.localContext(preFetchedComments)
113+
.build();
114+
} else {
115+
List<IssueDTO> issues = getAllIssuesWitNoComments(environment);
116+
return DataFetcherResult.newResult()
117+
.data(issues)
118+
.build();
119+
}
120+
};
121+
{{< / highlight >}}
122+
123+
If you look now you will see that our data fetcher returns a compound `DataFetcherResult` object that contains data for the child data fetchers which is the
124+
list of `issueDTO` objects as per usual.
125+
126+
It also passes down field specific `localContext` which is the pre-fetched comment data.
127+
128+
Unlike the global context object, local context objects are passed down from a specific field to its children and are not shared across to peer fields. This means
129+
a parent field has a "back channel" to talk to the child fields without having to "pollute" the DTO source objects with that information and it is local in the sense
130+
that it given only to this field and its children and not any other field in the query.
131+
132+
Now lets look at the `comments` data fetcher and how it consumes this back channel of data
133+
134+
135+
{{< highlight Java "linenos=table" >}}
136+
137+
DataFetcher commentsDataFetcher = environment -> {
138+
IssueDTO issueDTO = environment.getSource();
139+
Map<IssueDTO, List<CommentDTO>> preFetchedComments = environment.getLocalContext();
140+
List<CommentDTO> commentDTOS = preFetchedComments.get(issueDTO);
141+
return DataFetcherResult.newResult()
142+
.data(commentDTOS)
143+
.localContext(preFetchedComments)
144+
.build();
145+
};
146+
{{< / highlight >}}
147+
148+
Notice how it got the `issueDTO` as its source object as expected but it also got a local context object which is our pre-fetched comments. It can choose
149+
to pass on new local context OR if it passes nothing then the previous value will bubble down to the next lot of child fields. So you can think of `localContext`
150+
as being inherited unless a fields data fetcher explicitly overrides it.
151+
152+
Our data fetcher is a smidge more complex because of the data pre-fetching but 'localContext' allows us a nice back channel to pass data without modifying our DTO objects
153+
that are being used in more simple data fetchers.
154+
155+
156+
# Passing back Errors or Data or Both
157+
158+
For completeness we will show you that you can also pass down errors or data or local context or all of them at once.
159+
160+
It is perfectly valid to fetch data in graphql and to ALSO send back errors. Its not common but its valid. For example you might be able to select issues data but
161+
not the associated comment data. Some data is better than no data.
162+
163+
{{< highlight Java "linenos=table" >}}
164+
165+
GraphQLError error = mkSpecialError("Its Tuesday");
166+
167+
return DataFetcherResult.newResult()
168+
.data(commentDTOS)
169+
.error(error)
170+
.build();
171+
172+
{{< / highlight >}}
173+
174+
175+
176+
177+

0 commit comments

Comments
 (0)