Skip to content

Commit f02ee24

Browse files
authored
PropertyDataFetcher methods can receive an DataFetchingEnvironment object (graphql-java#1169)
* Added @OverRide as part of errorprone code health check * Revert "Added @OverRide as part of errorprone code health check" This reverts commit 38dfab1 * Added the ability for a method to take DataFetchingEnvironment * Added more documentation of data fetchers and an end to end test of PropertyDataFetcher * PR doco feedback
1 parent 0deb127 commit f02ee24

File tree

7 files changed

+512
-50
lines changed

7 files changed

+512
-50
lines changed

docs/data_fetching.rst

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
Fetching data
2+
=============
3+
4+
How graphql fetches data
5+
------------------------
6+
7+
Each field in graphql has a ``graphql.schema.DataFetcher`` associated with it.
8+
9+
Some fields will use specialised data fetcher code that knows how to go to a database say to get field information while
10+
most simply take data from the returned in memory objects using the field name and Plain Old Java Object (POJO) patterns
11+
to get the data.
12+
13+
`Note : Data fetchers are some times called "resolvers" in other graphql implementations.`
14+
15+
So imagine a type declaration like the one below :
16+
17+
18+
.. code-block:: graphql
19+
20+
type Query {
21+
products(match : String) : [Product] # a list of products
22+
}
23+
24+
type Product {
25+
id : ID
26+
name : String
27+
description : String
28+
cost : Float
29+
tax : Float
30+
launchDate(dateFormat : String = "dd, MMM, yyyy') : String
31+
}
32+
33+
The ``Query.products`` field has a data fetcher, as does each field in the type ``Product``.
34+
35+
The data fetcher on the ``Query.products`` field is likely to be a more complex data fetcher, containing code that
36+
goes to a database say to get a list of ``Product`` objects. It takes an optional ``match`` argument and hence can filter these
37+
product results if the client specified it.
38+
39+
It might look like the following :
40+
41+
.. code-block:: java
42+
43+
DataFetcher productsDataFetcher = new DataFetcher<List<ProductDTO>>() {
44+
@Override
45+
public List<ProductDTO> get(DataFetchingEnvironment environment) {
46+
DatabaseSecurityCtx ctx = environment.getContext();
47+
48+
List<ProductDTO> products;
49+
String match = environment.getArgument("match");
50+
if (match != null) {
51+
products = fetchProductsFromDatabaseWithMatching(ctx, match);
52+
} else {
53+
products = fetchAllProductsFromDatabase(ctx);
54+
}
55+
return products;
56+
}
57+
};
58+
59+
Each ``DataFetcher`` is passed a ``graphql.schema.DataFetchingEnvironment`` object which contains what field is being fetched, what
60+
arguments have been supplied to the field and other information such as the field's type, its parent type, the query root object or the query
61+
context object.
62+
63+
Note how the data fetcher code above uses the ``context`` object as an application specific security handle to get access
64+
to the database. This is a common technique to provide lower layer calling context.
65+
66+
Once we have a list of ``ProductDTO`` objects we typically don't need specialised data fetchers on each field. graphql-java
67+
ships with a smart ``graphql.schema.PropertyDataFetcher`` that knows how to follow POJO patterns based
68+
on the field name. In the example above there is a ``name`` field and hence it will try to look for a ``public String getName()``
69+
POJO method to get the data.
70+
71+
72+
``graphql.schema.PropertyDataFetcher`` is the data fetcher that is automatically associated with each field by default.
73+
74+
You can however still get access to the ``graphql.schema.DataFetchingEnvironment`` in your DTO methods. This allows you to
75+
tweak values before sending them out. For example above we have a ``launchDate`` field that takes an optional ``dateFormat``
76+
argument. We can have the ProductDTO have logic that applies this date formatting to the desired format.
77+
78+
79+
.. code-block:: java
80+
81+
class ProductDTO {
82+
83+
private ID id;
84+
private String name;
85+
private String description;
86+
private Double cost;
87+
private Double tax;
88+
private LocalDateTime launchDate;
89+
90+
// ...
91+
92+
public String getName() {
93+
return name;
94+
}
95+
96+
// ...
97+
98+
public String getLaunchDate(DataFetchingEnvironment environment) {
99+
String dateFormat = environment.getArgument("dateFormat");
100+
return yodaTimeFormatter(launchDate,dateFormat);
101+
}
102+
}
103+
104+
Customising PropertyDataFetcher
105+
-------------------------------
106+
107+
As mentioned above ``graphql.schema.PropertyDataFetcher`` is the default data fetcher for fields in graphql-java and it will use standard patterns for fetching
108+
object field values.
109+
110+
It supports a ``POJO`` approach and a ``Map`` approach in a Java idiomatic way. By default it assumes that for a graphql field ``fieldX`` it can find a POJO property
111+
called ``fieldX`` or a map key called ``fieldX`` if the backing object is a ``Map``.
112+
113+
However you may have small differences between your graphql schema naming and runtime object naming. For example imagine that ``Product.description`` is actually
114+
represented as ``getDesc()`` in the runtime backing Java object.
115+
116+
If you are using SDL to specify your schema then you can use the ``@fetch`` directive to indicate this remapping.
117+
118+
.. code-block:: graphql
119+
120+
directive @fetch(from : String!) on FIELD_DEFINITION
121+
122+
type Product {
123+
id : ID
124+
name : String
125+
description : String @fetch(from:"desc")
126+
cost : Float
127+
tax : Float
128+
}
129+
130+
This will tell the ``graphql.schema.PropertyDataFetcher`` to use the property name ``desc`` when fetching data for the graphql field named ``description``.
131+
132+
If you are hand coding your schema then you can just specify it directly by wiring in a field data fetcher.
133+
134+
.. code-block:: java
135+
136+
GraphQLFieldDefinition descriptionField = GraphQLFieldDefinition.newFieldDefinition()
137+
.name("description")
138+
.type(Scalars.GraphQLString)
139+
.dataFetcher(PropertyDataFetcher.fetching("desc"))
140+
.build();
141+
142+
143+
144+
The interesting parts of the DataFetchingEnvironment
145+
----------------------------------------------------
146+
147+
Every data fetcher is passed a ``graphql.schema.DataFetchingEnvironment`` object which allows it to know more about what is being fetched
148+
and what arguments have been provided. Here are some of the more interesting parts of ``DataFetchingEnvironment``.
149+
150+
* ``<T> T getSource()`` - the ``source`` object is used to get information for a field. Its the object that is the result
151+
of the parent field fetch. In the common case it is an in memory DTO object and hence simple POJO getters will be used for fields values. In more complex cases, you may examine it to know
152+
how to get the specific information for the current field. As the graphql field tree is executed, each returned field value
153+
becomes the ``source`` object for child fields.
154+
155+
* ``<T> T getRoot()`` - this special object is used to seed the graphql query. The ``root`` and the ``source`` is the same thing for the
156+
top level fields. The root object never changes during the query and it may be null and hence no used.
157+
158+
* ``Map<String, Object> getArguments()`` - this represents the arguments that have been provided on a field and the values of those
159+
arguments that have been resolved from passed in variables, AST literals and default argument values. You use the arguments
160+
of a field to control what values it returns.
161+
162+
* ``<T> T getContext()`` - the context is object is set up when the query is first executed and stays the same over the lifetime
163+
of the query. The context can be any value and is typically used to give each data fetcher some calling context needed
164+
when trying to get field data. For example the current user credentials or the database connection parameters could be contained
165+
with a ``context`` object so that data fetchers can make business layer calls. One of the key design decisions you have as a graphql
166+
system designer is how you will use context in your fetchers if at all. Some people use a dependency framework that injects context into
167+
data fetchers automatically and hence don't need to use this.
168+
169+
170+
* ``ExecutionTypeInfo getFieldTypeInfo()`` - the field type information is a catch all bucket of field type information that is built up as
171+
the query is executed. The following section explains more on this.
172+
173+
* ``DataFetchingFieldSelectionSet getSelectionSet()`` - the selection set represents the child fields that have been "selected" under neath the
174+
currently executing field. This can be useful to help look ahead to see what sub field information a client wants. The following section explains more on this.
175+
176+
* ```ExecutionId getExecutionId()`` - each query execution is given a unique id. You can use this perhaps on logs to tag each individual
177+
query.
178+
179+
180+
181+
182+
The interesting parts of ExecutionTypeInfo
183+
------------------------------------------
184+
185+
The execution of a graphql query creates a call tree of fields and their types. ``graphql.execution.ExecutionTypeInfo.getParentTypeInfo``
186+
allows you to navigate upwards and see what types and fields led to the current field execution.
187+
188+
Since this forms a tree path during execution, the ``graphql.execution.ExecutionTypeInfo.getPath`` method returns the representation of that
189+
path. This can be useful for logging and debugging queries.
190+
191+
There are also helper methods there to help you get the underlying type name of non null and list wrapped types.
192+
193+
194+
The interesting parts of DataFetchingFieldSelectionSet
195+
------------------------------------------------------
196+
197+
Imagine a query such as the following
198+
199+
200+
.. code-block:: graphql
201+
202+
query {
203+
products {
204+
# the fields below represent the selection set
205+
name
206+
description
207+
sellingLocations {
208+
state
209+
}
210+
}
211+
}
212+
213+
214+
The sub fields here of the ``products`` field represent the selection set of that field. It can be useful to know what sub selection has been asked for
215+
so the data fetcher can optimise the data access queries. For example an SQL backed system may be able to use the field sub selection to
216+
only retrieve the columns that have been asked for.
217+
218+
In the example above we have asked for ``selectionLocations`` information and hence we may be able to make an more efficient data access query where
219+
we ask for product information and selling location information at the same time.
220+
Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -165,41 +165,3 @@ response. It does that for every field in the query on that type.
165165

166166
By creating a "unified view" at the higher level data fetcher, you have mapped between your runtime view of the data and the graphql schema view of the data.
167167

168-
PropertyDataFetcher and data mapping
169-
------------------------------------
170-
171-
As mentioned above ``graphql.schema.PropertyDataFetcher`` is the default data fetcher for fields in graphql-java and it will use standard patterns for fetching
172-
object field values.
173-
174-
It supports a ``Map`` approach and a ``POJO`` approach in a Java idiomatic way. By default it assumes that for a graphql field ``fieldX`` it can find a property
175-
called ``fieldX``.
176-
177-
However you may have small differences between your graphql schema naming and runtime object naming. For example imagine that ``Product.description`` is actually
178-
represented as ``getDesc()` in the runtime backing Java object.
179-
180-
If you are using SDL to specify your schema then you can use the ``@fetch`` directive to indicate this remapping.
181-
182-
.. code-block:: graphql
183-
184-
directive @fetch(from : String!) on FIELD_DEFINITION
185-
186-
type Product {
187-
id : ID
188-
name : String
189-
description : String @fetch(from:"desc")
190-
cost : Float
191-
tax : Float
192-
}
193-
194-
This will tell the ``graphql.schema.PropertyDataFetcher`` to use the property name ``desc`` when fetching data for the graphql field named ``description``.
195-
196-
If you are hand coding your schema then you can just specify it directly by wiring in a field data fetcher.
197-
198-
.. code-block:: java
199-
200-
GraphQLFieldDefinition descriptionField = GraphQLFieldDefinition.newFieldDefinition()
201-
.name("description")
202-
.type(Scalars.GraphQLString)
203-
.dataFetcher(PropertyDataFetcher.fetching("desc"))
204-
.build();
205-

docs/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ graphql-java is licensed under the MIT License.
4949
getting_started
5050
schema
5151
execution
52-
mapping
52+
data_fetching
53+
data_mapping
5354
scalars
5455
subscriptions
5556
defer

src/main/java/graphql/schema/PropertyDataFetcher.java

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.lang.reflect.Method;
1111
import java.lang.reflect.Modifier;
1212
import java.util.Arrays;
13+
import java.util.Comparator;
1314
import java.util.Map;
1415
import java.util.Optional;
1516
import java.util.concurrent.ConcurrentHashMap;
@@ -135,15 +136,15 @@ public T get(DataFetchingEnvironment environment) {
135136
if (source instanceof Map) {
136137
return (T) ((Map<?, ?>) source).get(propertyName);
137138
}
138-
return (T) getPropertyViaGetter(source, environment.getFieldType());
139+
return (T) getPropertyViaGetter(source, environment.getFieldType(), environment);
139140
}
140141

141-
private Object getPropertyViaGetter(Object object, GraphQLOutputType outputType) {
142+
private Object getPropertyViaGetter(Object object, GraphQLOutputType outputType, DataFetchingEnvironment environment) {
142143
try {
143-
return getPropertyViaGetterMethod(object, outputType, this::findPubliclyAccessibleMethod);
144+
return getPropertyViaGetterMethod(object, outputType, this::findPubliclyAccessibleMethod, environment);
144145
} catch (NoSuchMethodException ignored) {
145146
try {
146-
return getPropertyViaGetterMethod(object, outputType, this::findViaSetAccessible);
147+
return getPropertyViaGetterMethod(object, outputType, this::findViaSetAccessible, environment);
147148
} catch (NoSuchMethodException ignored2) {
148149
return getPropertyViaFieldAccess(object);
149150
}
@@ -155,23 +156,27 @@ private interface MethodFinder {
155156
Method apply(Class aClass, String s) throws NoSuchMethodException;
156157
}
157158

158-
private Object getPropertyViaGetterMethod(Object object, GraphQLOutputType outputType, MethodFinder methodFinder) throws NoSuchMethodException {
159+
private Object getPropertyViaGetterMethod(Object object, GraphQLOutputType outputType, MethodFinder methodFinder, DataFetchingEnvironment environment) throws NoSuchMethodException {
159160
if (isBooleanProperty(outputType)) {
160161
try {
161-
return getPropertyViaGetterUsingPrefix(object, "is", methodFinder);
162+
return getPropertyViaGetterUsingPrefix(object, "is", methodFinder, environment);
162163
} catch (NoSuchMethodException e) {
163-
return getPropertyViaGetterUsingPrefix(object, "get", methodFinder);
164+
return getPropertyViaGetterUsingPrefix(object, "get", methodFinder, environment);
164165
}
165166
} else {
166-
return getPropertyViaGetterUsingPrefix(object, "get", methodFinder);
167+
return getPropertyViaGetterUsingPrefix(object, "get", methodFinder, environment);
167168
}
168169
}
169170

170-
private Object getPropertyViaGetterUsingPrefix(Object object, String prefix, MethodFinder methodFinder) throws NoSuchMethodException {
171+
private Object getPropertyViaGetterUsingPrefix(Object object, String prefix, MethodFinder methodFinder, DataFetchingEnvironment environment) throws NoSuchMethodException {
171172
String getterName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
172173
try {
173174
Method method = methodFinder.apply(object.getClass(), getterName);
174-
return method.invoke(object);
175+
if (takesDataFetcherEnvironmentAsOnlyArgument(method)) {
176+
return method.invoke(object, environment);
177+
} else {
178+
return method.invoke(object);
179+
}
175180
} catch (IllegalAccessException | InvocationTargetException e) {
176181
throw new GraphQLException(e);
177182
}
@@ -209,6 +214,7 @@ private String mkKey(Class clazz, String propertyName) {
209214
}
210215

211216
// by not filling out the stack trace, we gain speed when using the exception as flow control
217+
@SuppressWarnings("serial")
212218
private static class FastNoSuchMethodException extends NoSuchMethodException {
213219
public FastNoSuchMethodException(String methodName) {
214220
super(methodName);
@@ -238,6 +244,17 @@ private Method findPubliclyAccessibleMethod(Class root, String methodName) throw
238244
return method;
239245
}
240246
if (Modifier.isPublic(currentClass.getModifiers())) {
247+
//
248+
// try a getter that takes DataFetchingEnvironment first
249+
try {
250+
method = currentClass.getMethod(methodName, DataFetchingEnvironment.class);
251+
if (Modifier.isPublic(method.getModifiers())) {
252+
METHOD_CACHE.putIfAbsent(key, method);
253+
return method;
254+
}
255+
} catch (NoSuchMethodException e) {
256+
// ok try the next approach
257+
}
241258
method = currentClass.getMethod(methodName);
242259
if (Modifier.isPublic(method.getModifiers())) {
243260
METHOD_CACHE.putIfAbsent(key, method);
@@ -259,6 +276,8 @@ private Method findViaSetAccessible(Class aClass, String methodName) throws NoSu
259276
Method[] declaredMethods = aClass.getDeclaredMethods();
260277
Optional<Method> m = Arrays.stream(declaredMethods)
261278
.filter(mth -> methodName.equals(mth.getName()))
279+
.filter(mth -> hasZeroArgs(mth) || takesDataFetcherEnvironmentAsOnlyArgument(mth))
280+
.sorted(mostMethodArgsFirst())
262281
.findFirst();
263282
if (m.isPresent()) {
264283
try {
@@ -273,6 +292,19 @@ private Method findViaSetAccessible(Class aClass, String methodName) throws NoSu
273292
throw new FastNoSuchMethodException(methodName);
274293
}
275294

295+
private boolean hasZeroArgs(Method mth) {
296+
return mth.getParameterCount() == 0;
297+
}
298+
299+
private boolean takesDataFetcherEnvironmentAsOnlyArgument(Method mth) {
300+
return mth.getParameterCount() == 1 &&
301+
mth.getParameterTypes()[0].equals(DataFetchingEnvironment.class);
302+
}
303+
304+
private Comparator<? super Method> mostMethodArgsFirst() {
305+
return Comparator.comparingInt(Method::getParameterCount).reversed();
306+
}
307+
276308
private Object getPropertyViaFieldAccess(Object object) {
277309
Class<?> aClass = object.getClass();
278310
String key = mkKey(aClass, propertyName);

0 commit comments

Comments
 (0)