-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Singleton property data fetcher #3754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
689e401
a5a22c0
0d80213
612084e
b8bfd4b
70bac09
b8b2e26
424cac7
d1b132a
886d745
9ca4d30
b13b7c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,7 @@ public boolean hasDataFetcher(FieldCoordinates coordinates) { | |
| return hasDataFetcherImpl(coordinates, dataFetcherMap, systemDataFetcherMap); | ||
| } | ||
|
|
||
| @SuppressWarnings("deprecation") | ||
| private static DataFetcher<?> getDataFetcherImpl(FieldCoordinates coordinates, GraphQLFieldDefinition fieldDefinition, Map<FieldCoordinates, DataFetcherFactory<?>> dataFetcherMap, Map<String, DataFetcherFactory<?>> systemDataFetcherMap, DataFetcherFactory<?> defaultDataFetcherFactory) { | ||
| assertNotNull(coordinates); | ||
| assertNotNull(fieldDefinition); | ||
|
|
@@ -95,9 +96,15 @@ private static DataFetcher<?> getDataFetcherImpl(FieldCoordinates coordinates, G | |
| dataFetcherFactory = defaultDataFetcherFactory; | ||
| } | ||
| } | ||
| return dataFetcherFactory.get(newDataFetchingFactoryEnvironment() | ||
| .fieldDefinition(fieldDefinition) | ||
| .build()); | ||
| // call direct from the field - cheaper to not make a new environment object | ||
| DataFetcher<?> dataFetcher = dataFetcherFactory.get(fieldDefinition); | ||
| if (dataFetcher == null) { | ||
| DataFetcherFactoryEnvironment factoryEnvironment = newDataFetchingFactoryEnvironment() | ||
| .fieldDefinition(fieldDefinition) | ||
| .build(); | ||
| dataFetcher = dataFetcherFactory.get(factoryEnvironment); | ||
| } | ||
| return dataFetcher; | ||
| } | ||
|
|
||
| private static boolean hasDataFetcherImpl(FieldCoordinates coords, Map<FieldCoordinates, DataFetcherFactory<?>> dataFetcherMap, Map<String, DataFetcherFactory<?>> systemDataFetcherMap) { | ||
|
|
@@ -149,7 +156,7 @@ private static TypeResolver getTypeResolverForUnion(GraphQLUnionType parentType, | |
| if (typeResolver == null) { | ||
| typeResolver = parentType.getTypeResolver(); | ||
| } | ||
| return assertNotNull(typeResolver, "There must be a type resolver for union %s",parentType.getName()); | ||
| return assertNotNull(typeResolver, "There must be a type resolver for union %s", parentType.getName()); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -189,7 +196,7 @@ public static class Builder { | |
| private final Map<String, DataFetcherFactory<?>> systemDataFetcherMap = new LinkedHashMap<>(); | ||
| private final Map<String, TypeResolver> typeResolverMap = new HashMap<>(); | ||
| private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; | ||
| private DataFetcherFactory<?> defaultDataFetcherFactory = env -> PropertyDataFetcher.fetching(env.getFieldDefinition().getName()); | ||
| private DataFetcherFactory<?> defaultDataFetcherFactory = SingletonPropertyDataFetcher.singletonFactory(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. key change here - no object allocated per field |
||
| private boolean changed = false; | ||
|
|
||
| private Builder() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package graphql.schema; | ||
|
|
||
| import java.util.function.Supplier; | ||
|
|
||
| /** | ||
| * The {@link SingletonPropertyDataFetcher} is much like the {@link PropertyDataFetcher} except | ||
| * that it is designed to only ever fetch properties via the name of the field passed in. | ||
| * <p> | ||
| * This uses the same code as {@link PropertyDataFetcher} and hence is also controlled | ||
| * by static methods such as {@link PropertyDataFetcher#setUseNegativeCache(boolean)} | ||
| * | ||
| * @param <T> for two | ||
| */ | ||
| public class SingletonPropertyDataFetcher<T> implements LightDataFetcher<T> { | ||
|
|
||
| private static final SingletonPropertyDataFetcher<Object> SINGLETON_FETCHER = new SingletonPropertyDataFetcher<>(); | ||
|
|
||
| private static final DataFetcherFactory<?> SINGLETON_FETCHER_FACTORY = environment -> SINGLETON_FETCHER; | ||
|
|
||
| /** | ||
| * This returns the same singleton {@link LightDataFetcher} that fetches property values | ||
| * based on the name of the field that iis passed into it. | ||
| * | ||
| * @return a singleton property data fetcher | ||
| */ | ||
| public static LightDataFetcher<?> singleton() { | ||
| return SINGLETON_FETCHER; | ||
| } | ||
|
|
||
| /** | ||
| * This returns the same singleton {@link DataFetcherFactory} that returns the value of {@link #singleton()} | ||
| * | ||
| * @return a singleton data fetcher factory | ||
| */ | ||
| public static DataFetcherFactory<?> singletonFactory() { | ||
| return SINGLETON_FETCHER_FACTORY; | ||
| } | ||
|
|
||
| private SingletonPropertyDataFetcher() { | ||
| } | ||
|
|
||
| @Override | ||
| public T get(GraphQLFieldDefinition fieldDefinition, Object sourceObject, Supplier<DataFetchingEnvironment> environmentSupplier) throws Exception { | ||
| return fetchImpl(fieldDefinition, sourceObject, environmentSupplier); | ||
| } | ||
|
|
||
| @Override | ||
| public T get(DataFetchingEnvironment environment) throws Exception { | ||
| return fetchImpl(environment.getFieldDefinition(), environment.getSource(), () -> environment); | ||
| } | ||
|
|
||
| private T fetchImpl(GraphQLFieldDefinition fieldDefinition, Object source, Supplier<DataFetchingEnvironment> environmentSupplier) { | ||
| if (source == null) { | ||
| return null; | ||
| } | ||
| // this is the same code that PropertyDataFetcher uses and hence unit tests for it include this one | ||
| //noinspection unchecked | ||
| return (T) PropertyDataFetcherHelper.getPropertyValue(fieldDefinition.getName(), source, fieldDefinition.getType(), environmentSupplier); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,6 +57,7 @@ | |
| import graphql.schema.GraphQLUnionType; | ||
| import graphql.schema.GraphqlTypeComparatorRegistry; | ||
| import graphql.schema.PropertyDataFetcher; | ||
| import graphql.schema.SingletonPropertyDataFetcher; | ||
| import graphql.schema.TypeResolver; | ||
| import graphql.schema.TypeResolverProxy; | ||
| import graphql.schema.idl.errors.NotAnInputTypeError; | ||
|
|
@@ -801,23 +802,27 @@ GraphQLFieldDefinition buildField(BuildContext buildCtx, TypeDefinition<?> paren | |
| // if they have already wired in a fetcher - then leave it alone | ||
| FieldCoordinates coordinates = FieldCoordinates.coordinates(parentType.getName(), fieldDefinition.getName()); | ||
| if (!buildCtx.getCodeRegistry().hasDataFetcher(coordinates)) { | ||
| DataFetcherFactory<?> dataFetcherFactory = buildDataFetcherFactory(buildCtx, | ||
| Optional<DataFetcherFactory<?>> dataFetcherFactory = buildDataFetcherFactory(buildCtx, | ||
| parentType, | ||
| fieldDef, | ||
| fieldType, | ||
| appliedDirectives.first, | ||
| appliedDirectives.second); | ||
| buildCtx.getCodeRegistry().dataFetcher(coordinates, dataFetcherFactory); | ||
|
|
||
| // if the dataFetcherFactory is empty, then it must have been the code registry default one | ||
| // and hence we don't need to make a "map entry" in the code registry since it will be defaulted | ||
| // anyway | ||
| dataFetcherFactory.ifPresent(fetcherFactory -> buildCtx.getCodeRegistry().dataFetcher(coordinates, fetcherFactory)); | ||
| } | ||
| return directivesObserve(buildCtx, fieldDefinition); | ||
| } | ||
|
|
||
| private DataFetcherFactory<?> buildDataFetcherFactory(BuildContext buildCtx, | ||
| TypeDefinition<?> parentType, | ||
| FieldDefinition fieldDef, | ||
| GraphQLOutputType fieldType, | ||
| List<GraphQLDirective> directives, | ||
| List<GraphQLAppliedDirective> appliedDirectives) { | ||
| private Optional<DataFetcherFactory<?>> buildDataFetcherFactory(BuildContext buildCtx, | ||
| TypeDefinition<?> parentType, | ||
| FieldDefinition fieldDef, | ||
| GraphQLOutputType fieldType, | ||
| List<GraphQLDirective> directives, | ||
| List<GraphQLAppliedDirective> appliedDirectives) { | ||
| String fieldName = fieldDef.getName(); | ||
| String parentTypeName = parentType.getName(); | ||
| TypeDefinitionRegistry typeRegistry = buildCtx.getTypeRegistry(); | ||
|
|
@@ -847,16 +852,18 @@ private DataFetcherFactory<?> buildDataFetcherFactory(BuildContext buildCtx, | |
| if (dataFetcher == null) { | ||
| DataFetcherFactory<?> codeRegistryDFF = codeRegistry.getDefaultDataFetcherFactory(); | ||
| if (codeRegistryDFF != null) { | ||
| return codeRegistryDFF; | ||
| // this will use the default of the code registry when its | ||
| // asked for at runtime | ||
| return Optional.empty(); | ||
| } | ||
| dataFetcher = dataFetcherOfLastResort(wiringEnvironment); | ||
| dataFetcher = dataFetcherOfLastResort(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| dataFetcherFactory = DataFetcherFactories.useDataFetcher(dataFetcher); | ||
| } | ||
| return dataFetcherFactory; | ||
| return Optional.of(dataFetcherFactory); | ||
| } | ||
|
|
||
| GraphQLArgument buildArgument(BuildContext buildCtx, InputValueDefinition valueDefinition) { | ||
|
|
@@ -1087,9 +1094,8 @@ private Optional<OperationTypeDefinition> getOperationNamed(String name, Map<Str | |
| return Optional.ofNullable(operationTypeDefs.get(name)); | ||
| } | ||
|
|
||
| private DataFetcher<?> dataFetcherOfLastResort(FieldWiringEnvironment environment) { | ||
| String fieldName = environment.getFieldDefinition().getName(); | ||
| return new PropertyDataFetcher(fieldName); | ||
| private DataFetcher<?> dataFetcherOfLastResort() { | ||
| return SingletonPropertyDataFetcher.singleton(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is not actually used by default - unless GraphqlCodeRegistry had a null default DDF |
||
| } | ||
|
|
||
| private List<Directive> directivesOf(List<? extends TypeDefinition<?>> typeDefinitions) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| package graphql | ||
|
|
||
|
|
||
| import graphql.schema.GraphQLFieldDefinition | ||
| import graphql.schema.GraphQLOutputType | ||
| import graphql.schema.PropertyDataFetcher | ||
| import graphql.schema.SingletonPropertyDataFetcher | ||
| import spock.lang.Specification | ||
|
|
||
| import static graphql.Scalars.GraphQLBoolean | ||
|
|
@@ -55,43 +57,95 @@ class DataFetcherTest extends Specification { | |
|
|
||
| } | ||
|
|
||
| def env(GraphQLOutputType type) { | ||
| newDataFetchingEnvironment().source(dataHolder).fieldType(type).build() | ||
| def env(String propertyName, GraphQLOutputType type) { | ||
| GraphQLFieldDefinition fieldDefinition = mkField(propertyName, type) | ||
| newDataFetchingEnvironment().source(dataHolder).fieldType(type).fieldDefinition(fieldDefinition).build() | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a valid DataFetchingEnvironment will always have a field definition |
||
| } | ||
|
|
||
| def mkField(String propertyName, GraphQLOutputType type) { | ||
| GraphQLFieldDefinition.newFieldDefinition().name(propertyName).type(type).build() | ||
| } | ||
|
|
||
| def "get property value"() { | ||
| given: | ||
| def environment = env(GraphQLString) | ||
| def environment = env("property", GraphQLString) | ||
| def field = mkField("property", GraphQLString) | ||
| when: | ||
| def result = new PropertyDataFetcher("property").get(environment) | ||
| def result = fetcher.get(environment) | ||
| then: | ||
| result == "propertyValue" | ||
|
|
||
| when: | ||
| result = fetcher.get(field, dataHolder, { environment }) | ||
| then: | ||
| result == "propertyValue" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extra tests for light weight data fetching
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. boils down to the same code however in the end |
||
|
|
||
| where: | ||
| fetcher | _ | ||
| new PropertyDataFetcher("property") | _ | ||
| SingletonPropertyDataFetcher.singleton() | _ | ||
| } | ||
|
|
||
| def "get Boolean property value"() { | ||
| given: | ||
| def environment = env(GraphQLBoolean) | ||
| def environment = env("booleanField", GraphQLBoolean) | ||
| def field = mkField("booleanField", GraphQLBoolean) | ||
|
|
||
| when: | ||
| def result = fetcher.get(environment) | ||
| then: | ||
| result == true | ||
|
|
||
| when: | ||
| def result = new PropertyDataFetcher("booleanField").get(environment) | ||
| result = fetcher.get(field, dataHolder, { environment }) | ||
| then: | ||
| result == true | ||
|
|
||
| where: | ||
| fetcher | _ | ||
| new PropertyDataFetcher("booleanField") | _ | ||
| SingletonPropertyDataFetcher.singleton() | _ | ||
| } | ||
|
|
||
| def "get Boolean property value with get"() { | ||
| given: | ||
| def environment = env(GraphQLBoolean) | ||
| def environment = env("booleanFieldWithGet", GraphQLBoolean) | ||
| def field = mkField("booleanFieldWithGet", GraphQLBoolean) | ||
|
|
||
| when: | ||
| def result = fetcher.get(environment) | ||
| then: | ||
| result == false | ||
|
|
||
| when: | ||
| def result = new PropertyDataFetcher("booleanFieldWithGet").get(environment) | ||
| result = fetcher.get(field, dataHolder, { environment }) | ||
| then: | ||
| result == false | ||
|
|
||
| where: | ||
| fetcher | _ | ||
| new PropertyDataFetcher("booleanFieldWithGet") | _ | ||
| SingletonPropertyDataFetcher.singleton() | _ | ||
| } | ||
|
|
||
| def "get public field value as property"() { | ||
| given: | ||
| def environment = env(GraphQLString) | ||
| def environment = env("publicField", GraphQLString) | ||
| def field = mkField("publicField", GraphQLString) | ||
|
|
||
| when: | ||
| def result = fetcher.get(environment) | ||
| then: | ||
| result == "publicValue" | ||
|
|
||
| when: | ||
| def result = new PropertyDataFetcher("publicField").get(environment) | ||
| result = fetcher.get(field, dataHolder, { environment }) | ||
| then: | ||
| result == "publicValue" | ||
|
|
||
| where: | ||
| fetcher | _ | ||
| new PropertyDataFetcher("publicField") | _ | ||
| SingletonPropertyDataFetcher.singleton() | _ | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small clean up - generics!