From e8eef2607c0740e45914cc341195d6a9f528d97b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Oct 2021 09:11:41 +0200 Subject: [PATCH 1/2] Migrate QuerydslDataFetcher to use fluent Querydsl API. Remove supporting classes for external projection as projections are handled by Spring Data directly. --- build.gradle | 2 +- samples/webmvc-http/build.gradle | 3 + spring-graphql/build.gradle | 2 + .../querydsl/DtoInstantiatingConverter.java | 99 ------- .../data/querydsl/DtoMappingContext.java | 84 ------ .../data/querydsl/QuerydslDataFetcher.java | 245 ++++++++++++------ .../graphql/data/querydsl/Book.java | 4 +- .../querydsl/QuerydslDataFetcherTests.java | 39 +-- 8 files changed, 191 insertions(+), 287 deletions(-) delete mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoInstantiatingConverter.java delete mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoMappingContext.java diff --git a/build.gradle b/build.gradle index fba837019..67407134e 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ configure(moduleProjects) { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5" mavenBom "io.projectreactor:reactor-bom:2020.0.11" mavenBom "org.springframework:spring-framework-bom:5.3.10" - mavenBom "org.springframework.data:spring-data-bom:2021.0.5" + mavenBom "org.springframework.data:spring-data-bom:2021.1.0-RC1" mavenBom "org.springframework.security:spring-security-bom:5.5.2" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.31" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2" diff --git a/samples/webmvc-http/build.gradle b/samples/webmvc-http/build.gradle index 7bfeb2b4b..4edee3f38 100644 --- a/samples/webmvc-http/build.gradle +++ b/samples/webmvc-http/build.gradle @@ -13,6 +13,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-hateoas' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-actuator' + // TODO: Remove after upgrade to Spring Boot 2.6 + implementation 'org.springframework.data:spring-data-commons:2.6.0-RC1' + implementation 'org.springframework.data:spring-data-jpa:2.6.0-RC1' implementation 'com.querydsl:querydsl-core' implementation 'com.querydsl:querydsl-jpa' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index b4cc4550d..28995b2c3 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -31,7 +31,9 @@ dependencies { testImplementation 'org.springframework:spring-websocket' testImplementation 'org.springframework:spring-test' testImplementation 'org.springframework.data:spring-data-commons' + testImplementation 'org.springframework.data:spring-data-keyvalue' testImplementation 'com.querydsl:querydsl-core' + testImplementation 'com.querydsl:querydsl-collections' testImplementation 'javax.servlet:javax.servlet-api' testImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoInstantiatingConverter.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoInstantiatingConverter.java deleted file mode 100644 index ef977befa..000000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoInstantiatingConverter.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2002-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.graphql.data.querydsl; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.PreferredConstructor.Parameter; -import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.EntityInstantiator; -import org.springframework.data.mapping.model.EntityInstantiators; -import org.springframework.data.mapping.model.ParameterValueProvider; - -/** - * {@link Converter} to instantiate DTOs from fully equipped domain objects. - * - * @author Mark Paluch - * @since 1.0.0 - */ -class DtoInstantiatingConverter implements Converter { - - private final Class targetType; - - private final MappingContext, ? extends PersistentProperty> context; - - private final EntityInstantiator instantiator; - - /** - * Create a new {@link Converter} to instantiate DTOs. - * @param dtoType target type - * @param context mapping context to be used - * @param entityInstantiators the instantiators to use for object creation - */ - public DtoInstantiatingConverter(Class dtoType, - MappingContext, ? extends PersistentProperty> context, - EntityInstantiators entityInstantiators) { - - this.targetType = dtoType; - this.context = context; - this.instantiator = entityInstantiators.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); - } - - @SuppressWarnings("unchecked") - @Override - public T convert(Object source) { - - if (targetType.isInterface()) { - return (T) source; - } - - PersistentEntity sourceEntity = this.context.getRequiredPersistentEntity(source.getClass()); - - PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); - PersistentEntity entity = this.context.getRequiredPersistentEntity(this.targetType); - PreferredConstructor> constructor = entity.getPersistenceConstructor(); - - @SuppressWarnings({"rawtypes", "unchecked"}) - Object dto = this.instantiator.createInstance(entity, new ParameterValueProvider() { - - @Override - public Object getParameterValue(Parameter parameter) { - return sourceAccessor.getProperty( - sourceEntity.getRequiredPersistentProperty(parameter.getName())); - } - }); - - PersistentPropertyAccessor dtoAccessor = entity.getPropertyAccessor(dto); - - entity.doWithProperties((SimplePropertyHandler) property -> { - - if (constructor.isConstructorParameter(property)) { - return; - } - - dtoAccessor.setProperty(property, - sourceAccessor.getProperty(sourceEntity.getRequiredPersistentProperty(property.getName()))); - }); - - return (T) dto; - } - -} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoMappingContext.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoMappingContext.java deleted file mode 100644 index b98855968..000000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/DtoMappingContext.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2002-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.graphql.data.querydsl; - -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.context.AbstractMappingContext; -import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; -import org.springframework.data.mapping.model.BasicPersistentEntity; -import org.springframework.data.mapping.model.Property; -import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.util.TypeInformation; - -/** - * Lightweight {@link org.springframework.data.mapping.context.MappingContext} - * to provide class metadata for entity to DTO mapping. - * - * @author Mark Paluch - * @since 1.0.0 - */ -class DtoMappingContext extends AbstractMappingContext, - DtoMappingContext.DtoPersistentProperty> { - - @Override - protected boolean shouldCreatePersistentEntityFor(TypeInformation type) { - // No Java std lib type introspection to not interfere with encapsulation. - // We do not want to get into the business of materializing Java types. - if (type.getType().getName().startsWith("java.") || type.getType().getName().startsWith("javax.")) { - return false; - } - return super.shouldCreatePersistentEntityFor(type); - } - - @Override - protected DtoPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - return new DtoPersistentEntity<>(typeInformation); - } - - @Override - protected DtoPersistentProperty createPersistentProperty( - Property property, DtoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { - - return new DtoPersistentProperty(property, owner, simpleTypeHolder); - } - - static class DtoPersistentEntity extends BasicPersistentEntity { - - public DtoPersistentEntity(TypeInformation information) { - super(information); - } - - } - - static class DtoPersistentProperty extends AnnotationBasedPersistentProperty { - - public DtoPersistentProperty( - Property property, PersistentEntity owner, - SimpleTypeHolder simpleTypeHolder) { - - super(property, owner, simpleTypeHolder); - } - - @Override - protected Association createAssociation() { - return null; - } - - } - -} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java index 5eb9d0b39..73beb1baa 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.function.Function; -import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Predicate; import graphql.schema.DataFetcher; @@ -47,9 +46,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.data.mapping.model.EntityInstantiators; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.domain.Sort; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.querydsl.SimpleEntityPathResolver; @@ -60,8 +57,9 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; import org.springframework.graphql.data.GraphQlRepository; import org.springframework.lang.Nullable; @@ -120,11 +118,12 @@ public abstract class QuerydslDataFetcher { private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder( DefaultConversionService.getSharedInstance(), SimpleEntityPathResolver.INSTANCE); - private final TypeInformation domainType; + // visible to subtypes in the same package + final TypeInformation domainType; private final QuerydslBinderCustomizer> customizer; - QuerydslDataFetcher(ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer) { + QuerydslDataFetcher(TypeInformation domainType, QuerydslBinderCustomizer> customizer) { this.customizer = customizer; this.domainType = domainType; } @@ -140,10 +139,13 @@ public abstract class QuerydslDataFetcher { public static Builder builder(QuerydslPredicateExecutor executor) { Class repositoryInterface = getRepositoryInterface(executor); DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + Class domainType = (Class) metadata.getDomainType(); return new Builder<>(executor, - (ClassTypeInformation) ClassTypeInformation.from(metadata.getDomainType()), - (bindings, root) -> {}, Function.identity()); + ClassTypeInformation.from(domainType), + domainType, + Sort.unsorted(), + (bindings, root) -> {}); } /** @@ -157,10 +159,13 @@ public static Builder builder(QuerydslPredicateExecutor executor) { public static ReactiveBuilder builder(ReactiveQuerydslPredicateExecutor executor) { Class repositoryInterface = getRepositoryInterface(executor); DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + Class domainType = (Class) metadata.getDomainType(); return new ReactiveBuilder<>(executor, - (ClassTypeInformation) ClassTypeInformation.from(metadata.getDomainType()), - (bindings, root) -> {}, Function.identity()); + ClassTypeInformation.from(domainType), + domainType, + Sort.unsorted(), + (bindings, root) -> {}); } /** @@ -192,32 +197,7 @@ protected Predicate buildPredicate(DataFetchingEnvironment environment) { parameters.put(entry.getKey(), Collections.singletonList(entry.getValue())); } - Predicate predicate = BUILDER.getPredicate(this.domainType, (MultiValueMap) parameters, bindings); - - // Temporary workaround for this fix in Spring Data: - // https://github.com/spring-projects/spring-data-commons/issues/2396 - - if (predicate == null) { - predicate = new BooleanBuilder(); - } - - return predicate; - } - - private static Function createProjection(Class projectionType) { - // TODO: SpelAwareProxyProjectionFactory, DtoMappingContext, and EntityInstantiators - // should be reused to avoid duplicate class metadata. - Assert.notNull(projectionType, "Projection type must not be null"); - - if (projectionType.isInterface()) { - ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); - return element -> projectionFactory.createProjection(projectionType, element); - } - - DtoInstantiatingConverter converter = new DtoInstantiatingConverter<>(projectionType, - new DtoMappingContext(), new EntityInstantiators()); - - return converter::convert; + return BUILDER.getPredicate(this.domainType, (MultiValueMap) parameters, bindings); } private static Class getRepositoryInterface(Object executor) { @@ -251,18 +231,22 @@ public static class Builder { private final ClassTypeInformation domainType; - private final QuerydslBinderCustomizer> customizer; + private final Class resultType; - private final Function resultConverter; + private final Sort sort; + + private final QuerydslBinderCustomizer> customizer; Builder(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, - Function resultConverter) { + Class resultType, + Sort sort, + QuerydslBinderCustomizer> customizer) { this.executor = executor; this.domainType = domainType; + this.resultType = resultType; + this.sort = sort; this.customizer = customizer; - this.resultConverter = resultConverter; } /** @@ -278,7 +262,19 @@ public static class Builder { public

Builder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); return new Builder<>( - this.executor, this.domainType, this.customizer, createProjection(projectionType)); + this.executor, this.domainType, projectionType, this.sort, this.customizer); + } + + /** + * Apply a {@link Sort} order. + * @param sort the default sort order + * @return a new {@link Builder} instance with all previously configured + * options and {@code Sort} applied + */ + public Builder sortBy(Sort sort) { + Assert.notNull(sort, "Sort must not be null"); + return new Builder<>( + this.executor, this.domainType, this.resultType, sort, customizer); } /** @@ -291,7 +287,7 @@ public

Builder projectAs(Class

projectionType) { public Builder customizer(QuerydslBinderCustomizer> customizer) { Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); return new Builder<>( - this.executor, this.domainType, customizer, this.resultConverter); + this.executor, this.domainType, this.resultType, this.sort, customizer); } /** @@ -300,7 +296,7 @@ public Builder customizer(QuerydslBinderCustomizer */ public DataFetcher single() { return new SingleEntityFetcher<>( - this.executor, this.domainType, this.customizer, this.resultConverter); + this.executor, this.domainType, this.resultType, this.sort, this.customizer); } /** @@ -309,7 +305,7 @@ public DataFetcher single() { */ public DataFetcher> many() { return new ManyEntityFetcher<>( - this.executor, this.domainType, this.customizer, this.resultConverter); + this.executor, this.domainType, this.resultType, this.sort, this.customizer); } } @@ -325,21 +321,25 @@ public static class ReactiveBuilder { private final ReactiveQuerydslPredicateExecutor executor; - private final ClassTypeInformation domainType; + private final TypeInformation domainType; - private final QuerydslBinderCustomizer> customizer; + private final Class resultType; - private final Function resultConverter; + private final Sort sort; + + private final QuerydslBinderCustomizer> customizer; ReactiveBuilder(ReactiveQuerydslPredicateExecutor executor, - ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, - Function resultConverter) { + TypeInformation domainType, + Class resultType, + Sort sort, + QuerydslBinderCustomizer> customizer) { this.executor = executor; this.domainType = domainType; + this.resultType = resultType; + this.sort = sort; this.customizer = customizer; - this.resultConverter = resultConverter; } /** @@ -355,7 +355,19 @@ public static class ReactiveBuilder { public

ReactiveBuilder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); return new ReactiveBuilder<>( - this.executor, this.domainType, this.customizer, createProjection(projectionType)); + this.executor, this.domainType, projectionType, this.sort, this.customizer); + } + + /** + * Apply a {@link Sort} order. + * @param sort the default sort order + * @return a new {@link Builder} instance with all previously configured + * options and {@code Sort} applied + */ + public ReactiveBuilder sortBy(Sort sort) { + Assert.notNull(sort, "Sort must not be null"); + return new ReactiveBuilder<>( + this.executor, this.domainType, this.resultType, sort, customizer); } /** @@ -368,7 +380,7 @@ public

ReactiveBuilder projectAs(Class

projectionType) { public ReactiveBuilder customizer(QuerydslBinderCustomizer> customizer) { Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); return new ReactiveBuilder<>( - this.executor, this.domainType, customizer, this.resultConverter); + this.executor, this.domainType, this.resultType, this.sort, customizer); } /** @@ -377,7 +389,7 @@ public ReactiveBuilder customizer(QuerydslBinderCustomizer> single() { return new ReactiveSingleEntityFetcher<>( - this.executor, this.domainType, this.customizer, this.resultConverter); + this.executor, this.domainType, this.resultType, this.sort, this.customizer); } /** @@ -386,7 +398,7 @@ public DataFetcher> single() { */ public DataFetcher> many() { return new ReactiveManyEntityFetcher<>( - this.executor, this.domainType, this.customizer, this.resultConverter); + this.executor, this.domainType, this.resultType, this.sort, this.customizer); } } @@ -395,24 +407,39 @@ private static class SingleEntityFetcher extends QuerydslDataFetcher im private final QuerydslPredicateExecutor executor; - private final Function resultConverter; + private final Class resultType; + + private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) SingleEntityFetcher(QuerydslPredicateExecutor executor, - ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, - Function resultConverter) { + TypeInformation domainType, + Class resultType, + Sort sort, + QuerydslBinderCustomizer> customizer) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; - this.resultConverter = resultConverter; + this.resultType = resultType; + this.sort = sort; } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "unchecked"}) public R get(DataFetchingEnvironment environment) { - Predicate predicate = buildPredicate(environment); - return this.executor.findOne(predicate).map(this.resultConverter).orElse(null); + return this.executor.findBy(buildPredicate(environment), q -> { + FetchableFluentQuery queryToUse = (FetchableFluentQuery) q; + + if(this.sort.isSorted()){ + queryToUse = queryToUse.sortBy(this.sort); + } + + if(!this.resultType.equals(this.domainType.getType())){ + queryToUse = queryToUse.as(this.resultType); + } + + return queryToUse.first(); + }).orElse(null); } } @@ -421,22 +448,38 @@ private static class ManyEntityFetcher extends QuerydslDataFetcher impl private final QuerydslPredicateExecutor executor; - private final Function resultConverter; + private final Class resultType; + + private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) ManyEntityFetcher(QuerydslPredicateExecutor executor, - ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, - Function resultConverter) { + TypeInformation domainType, + Class resultType, + Sort sort, + QuerydslBinderCustomizer> customizer) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; - this.resultConverter = resultConverter; + this.resultType = resultType; + this.sort = sort; } @Override + @SuppressWarnings("unchecked") public Iterable get(DataFetchingEnvironment environment) { - Predicate predicate = buildPredicate(environment); - return Streamable.of(this.executor.findAll(predicate)).map(this.resultConverter).toList(); + return this.executor.findBy(buildPredicate(environment), q -> { + FetchableFluentQuery queryToUse = (FetchableFluentQuery) q; + + if(this.sort.isSorted()){ + queryToUse = queryToUse.sortBy(this.sort); + } + + if(!this.resultType.equals(this.domainType.getType())){ + queryToUse = queryToUse.as(this.resultType); + } + + return queryToUse.all(); + }); } } @@ -445,22 +488,39 @@ private static class ReactiveSingleEntityFetcher extends QuerydslDataFetch private final ReactiveQuerydslPredicateExecutor executor; - private final Function resultConverter; + private final Class resultType; + + private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) ReactiveSingleEntityFetcher(ReactiveQuerydslPredicateExecutor executor, - ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, - Function resultConverter) { + TypeInformation domainType, + Class resultType, + Sort sort, + QuerydslBinderCustomizer> customizer) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; - this.resultConverter = resultConverter; + this.resultType = resultType; + this.sort = sort; } @Override + @SuppressWarnings("unchecked") public Mono get(DataFetchingEnvironment environment) { - return this.executor.findOne(buildPredicate(environment)).map(this.resultConverter); + return this.executor.findBy(buildPredicate(environment), q -> { + FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) q; + + if(this.sort.isSorted()){ + queryToUse = queryToUse.sortBy(this.sort); + } + + if(!this.resultType.equals(this.domainType.getType())){ + queryToUse = queryToUse.as(this.resultType); + } + + return queryToUse.first(); + }); } } @@ -469,22 +529,39 @@ private static class ReactiveManyEntityFetcher extends QuerydslDataFetcher private final ReactiveQuerydslPredicateExecutor executor; - private final Function resultConverter; + private final Class resultType; + + private final Sort sort; @SuppressWarnings({"unchecked", "rawtypes"}) ReactiveManyEntityFetcher(ReactiveQuerydslPredicateExecutor executor, - ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, - Function resultConverter) { + TypeInformation domainType, + Class resultType, + Sort sort, + QuerydslBinderCustomizer> customizer) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; - this.resultConverter = resultConverter; + this.resultType = resultType; + this.sort = sort; } @Override + @SuppressWarnings("unchecked") public Flux get(DataFetchingEnvironment environment) { - return this.executor.findAll(buildPredicate(environment)).map(this.resultConverter); + return this.executor.findBy(buildPredicate(environment), q -> { + FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) q; + + if(this.sort.isSorted()){ + queryToUse = queryToUse.sortBy(this.sort); + } + + if(!this.resultType.equals(this.domainType.getType())){ + queryToUse = queryToUse.as(this.resultType); + } + + return queryToUse.all(); + }); } } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java index 2ff2c1408..956e45380 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java @@ -16,9 +16,11 @@ package org.springframework.graphql.data.querydsl; +import org.springframework.data.annotation.Id; + public class Book { - Long id; + @Id Long id; String name; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java index 49ce15b4e..7f797388d 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java @@ -33,9 +33,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; +import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; +import org.springframework.data.map.MapKeyValueAdapter; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.Repository; import org.springframework.graphql.data.GraphQlRepository; import org.springframework.graphql.execution.ExecutionGraphQlService; @@ -57,16 +61,18 @@ */ class QuerydslDataFetcherTests { + private KeyValueRepositoryFactory repositoryFactory = new KeyValueRepositoryFactory(new KeyValueTemplate(new MapKeyValueAdapter())); + private MockRepository mockRepository = repositoryFactory.getRepository(MockRepository.class); + @Test void shouldFetchSingleItems() { - MockRepository mockRepository = mock(MockRepository.class); Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); + mockRepository.save(book); BiConsumer, QuerydslPredicateExecutor> tester = (wiringConfigurer, executor) -> { WebGraphQlHandler handler = initWebGraphQlHandler(wiringConfigurer, executor, null); - WebOutput output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block(); + WebOutput output = handler.handleRequest(input("{ bookById(id: 42) {name}}")).block(); // TODO: getData interferes with method overrides assertThat((Object) output.getData()).isEqualTo( @@ -85,10 +91,9 @@ void shouldFetchSingleItems() { @Test void shouldFetchMultipleItems() { - MockRepository mockRepository = mock(MockRepository.class); Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); Book book2 = new Book(53L, "Breaking Bad", "Heisenberg"); - when(mockRepository.findAll(any(Predicate.class))).thenReturn(Arrays.asList(book1, book2)); + mockRepository.saveAll(Arrays.asList(book1, book2)); BiConsumer, QuerydslPredicateExecutor> tester = (wiringConfigurer, executor) -> { @@ -97,8 +102,8 @@ void shouldFetchMultipleItems() { assertThat((Object) output.getData()).isEqualTo( Collections.singletonMap("books", Arrays.asList( - Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), - Collections.singletonMap("name", "Breaking Bad")))); + Collections.singletonMap("name", "Breaking Bad"), + Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy")))); }; // explicit wiring @@ -114,7 +119,7 @@ void shouldFetchMultipleItems() { void shouldFavorExplicitWiring() { MockRepository mockRepository = mock(MockRepository.class); Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); + when(mockRepository.findBy(any(), any())).thenReturn(Optional.of(book)); // 1) Automatic registration only WebGraphQlHandler handler = initWebGraphQlHandler(null, mockRepository, null); @@ -136,9 +141,8 @@ void shouldFavorExplicitWiring() { @Test void shouldFetchSingleItemsWithInterfaceProjection() { - MockRepository mockRepository = mock(MockRepository.class); Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); + mockRepository.save(book); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder .dataFetcher("bookById", QuerydslDataFetcher @@ -146,7 +150,7 @@ void shouldFetchSingleItemsWithInterfaceProjection() { .projectAs(BookProjection.class) .single())); - WebOutput output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block(); + WebOutput output = handler.handleRequest(input("{ bookById(id: 42) {name}}")).block(); assertThat((Object) output.getData()).isEqualTo( Collections.singletonMap("bookById", @@ -155,9 +159,8 @@ void shouldFetchSingleItemsWithInterfaceProjection() { @Test void shouldFetchSingleItemsWithDtoProjection() { - MockRepository mockRepository = mock(MockRepository.class); Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); + mockRepository.save(book); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder .dataFetcher("bookById", QuerydslDataFetcher @@ -165,7 +168,7 @@ void shouldFetchSingleItemsWithDtoProjection() { .projectAs(BookDto.class) .single())); - WebOutput output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block(); + WebOutput output = handler.handleRequest(input("{ bookById(id: 42) {name}}")).block(); assertThat((Object) output.getData()).isEqualTo( Collections.singletonMap("bookById", @@ -187,7 +190,7 @@ void shouldConstructPredicateProperly() { ArgumentCaptor predicateCaptor = ArgumentCaptor.forClass(Predicate.class); - verify(mockRepository).findAll(predicateCaptor.capture()); + verify(mockRepository).findBy(predicateCaptor.capture(), any()); Predicate predicate = predicateCaptor.getValue(); assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H").and(QBook.book.author.eq("Doug"))); @@ -197,7 +200,7 @@ void shouldConstructPredicateProperly() { void shouldReactivelyFetchSingleItems() { ReactiveMockRepository mockRepository = mock(ReactiveMockRepository.class); Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - when(mockRepository.findOne(any())).thenReturn(Mono.just(book)); + when(mockRepository.findBy(any(), any())).thenReturn(Mono.just(book)); BiConsumer, ReactiveQuerydslPredicateExecutor> tester = (wiringConfigurer, executor) -> { @@ -224,7 +227,7 @@ void shouldReactivelyFetchMultipleItems() { ReactiveMockRepository mockRepository = mock(ReactiveMockRepository.class); Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); Book book2 = new Book(53L, "Breaking Bad", "Heisenberg"); - when(mockRepository.findAll((Predicate) any())).thenReturn(Flux.just(book1, book2)); + when(mockRepository.findBy(any(), any())).thenReturn(Flux.just(book1, book2)); BiConsumer, ReactiveQuerydslPredicateExecutor> tester = (wiringConfigurer, executor) -> { @@ -248,7 +251,7 @@ void shouldReactivelyFetchMultipleItems() { @GraphQlRepository - interface MockRepository extends Repository, QuerydslPredicateExecutor { + interface MockRepository extends CrudRepository, QuerydslPredicateExecutor { } From a370cfc20d69e70e5b9cc006ab4cfe68051f2323 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Oct 2021 14:09:38 +0200 Subject: [PATCH 2/2] Limit field projection according to field selection. We now request only property paths from the repository query that are requested through a GraphQL query if the result type is not a projection. That reduces the amount of data being retrieved from the underlying data store. --- .../data/querydsl/PropertySelection.java | 196 ++++++++++++++++++ .../data/querydsl/QuerydslDataFetcher.java | 55 +++-- .../graphql/data/querydsl/Book.java | 9 +- .../querydsl/QuerydslDataFetcherTests.java | 24 +-- 4 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/PropertySelection.java diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/PropertySelection.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/PropertySelection.java new file mode 100644 index 000000000..6615d0291 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/PropertySelection.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.data.querydsl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.SelectedField; + +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.util.TypeInformation; + +/** + * Utility to compute {@link PropertyPath property paths} from + * a {@link DataFetchingFieldSelectionSet field selection} considering an underlying + * Java type. + *

+ * Property paths are created for each selected field that corresponds with a property + * on the underlying type. Nested properties are represented with nested paths + * if the nesting can be resolved to a concrete type, otherwise the nested path + * is considered to be a composite property without further inspection. + * + * @author Mark Paluch + * @since 1.0.0 + */ +class PropertySelection { + + private final List propertyPaths; + + private PropertySelection(List propertyPaths) { + this.propertyPaths = propertyPaths; + } + + /** + * Create a property selection for the given {@link TypeInformation type} and + * {@link DataFetchingFieldSelectionSet}. + * + * @param typeInformation the type to inspect + * @param selectionSet the field selection to apply + * @return a property selection holding all selectable property paths. + */ + public static PropertySelection create(TypeInformation typeInformation, + DataFetchingFieldSelectionSet selectionSet) { + return create(typeInformation, new DataFetchingFieldSelection(selectionSet)); + } + + private static PropertySelection create(TypeInformation typeInformation, FieldSelection selection) { + List propertyPaths = collectPropertyPaths(typeInformation, + selection, + path -> PropertyPath.from(path, typeInformation)); + return new PropertySelection(propertyPaths); + } + + private static List collectPropertyPaths(TypeInformation typeInformation, + FieldSelection selection, Function propertyPathFactory) { + List propertyPaths = new ArrayList<>(); + + for (SelectedField selectedField : selection) { + + String propertyName = selectedField.getName(); + TypeInformation property = typeInformation.getProperty(propertyName); + + if (property == null) { + continue; + } + + PropertyPath propertyPath = propertyPathFactory.apply(propertyName); + FieldSelection nestedSelection = selection.select(selectedField); + + List pathsToAdd = Collections.singletonList(propertyPath); + + if (!nestedSelection.isEmpty() && property.getActualType() != null) { + List nestedPaths = collectPropertyPaths(property.getRequiredActualType(), + nestedSelection, propertyPath::nested); + + if (!nestedPaths.isEmpty()) { + pathsToAdd = nestedPaths; + } + } + + propertyPaths.addAll(pathsToAdd); + } + + return propertyPaths; + } + + /** + * @return the property paths as list. + */ + public List toList() { + return this.propertyPaths.stream().map(PropertyPath::toDotPath) + .collect(Collectors.toList()); + } + + /** + * Hierarchical representation of selected fields. Allows traversing the + * object graph with nested fields. + */ + interface FieldSelection extends Iterable { + + /** + * @return {@code true} if the field selection is empty + */ + boolean isEmpty(); + + /** + * Obtain the field selection (nested fields) for a given {@code field}. + * @param field the field for which nested fields should be obtained + * @return the field selection. Can be empty. + */ + FieldSelection select(SelectedField field); + + } + + static class DataFetchingFieldSelection implements FieldSelection { + + private final List selectedFields; + private final List allFields; + + DataFetchingFieldSelection(DataFetchingFieldSelectionSet selectionSet) { + this.selectedFields = selectionSet.getImmediateFields(); + this.allFields = selectionSet.getFields(); + } + + private DataFetchingFieldSelection(List selectedFields, + List allFields) { + this.selectedFields = selectedFields; + this.allFields = allFields; + } + + @Override + public boolean isEmpty() { + return selectedFields.isEmpty(); + } + + @Override + public FieldSelection select(SelectedField field) { + List selectedFields = new ArrayList<>(); + + for (SelectedField selectedField : allFields) { + if (field.equals(selectedField.getParentField())) { + selectedFields.add(selectedField); + } + } + + return (selectedFields.isEmpty() ? EmptyFieldSelection.INSTANCE + : new DataFetchingFieldSelection(selectedFields, allFields)); + } + + @Override + public Iterator iterator() { + return this.selectedFields.iterator(); + } + + } + + enum EmptyFieldSelection implements FieldSelection { + + INSTANCE; + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public FieldSelection select(SelectedField field) { + return INSTANCE; + } + + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java index 73beb1baa..48b85f763 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcher.java @@ -17,6 +17,7 @@ package org.springframework.graphql.data.querydsl; import java.lang.reflect.Type; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -25,18 +26,7 @@ import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Predicate; -import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLCodeRegistry; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLNamedOutputType; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeVisitor; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.PropertyDataFetcher; +import graphql.schema.*; import graphql.util.TraversalControl; import graphql.util.TraverserContext; import reactor.core.publisher.Flux; @@ -118,8 +108,7 @@ public abstract class QuerydslDataFetcher { private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder( DefaultConversionService.getSharedInstance(), SimpleEntityPathResolver.INSTANCE); - // visible to subtypes in the same package - final TypeInformation domainType; + private final TypeInformation domainType; private final QuerydslBinderCustomizer> customizer; @@ -185,6 +174,20 @@ public static GraphQLTypeVisitor registrationTypeVisitor( return new RegistrationTypeVisitor(executors, reactiveExecutors); } + protected boolean requiresProjection(Class resultType) { + return !resultType.equals(this.domainType.getType()); + } + protected Collection buildPropertyPaths(DataFetchingFieldSelectionSet selection, + Class resultType){ + // Compute selection only for non-projections + if(resultType.equals(this.domainType.getType()) + || this.domainType.getType().isAssignableFrom(resultType) + || this.domainType.isSubTypeOf(resultType)) { + return PropertySelection.create(this.domainType, selection).toList(); + } + return Collections.emptyList(); + } + @SuppressWarnings({"unchecked", "rawtypes"}) protected Predicate buildPredicate(DataFetchingEnvironment environment) { MultiValueMap parameters = new LinkedMultiValueMap<>(); @@ -430,12 +433,14 @@ public R get(DataFetchingEnvironment environment) { return this.executor.findBy(buildPredicate(environment), q -> { FetchableFluentQuery queryToUse = (FetchableFluentQuery) q; - if(this.sort.isSorted()){ + if (this.sort.isSorted()){ queryToUse = queryToUse.sortBy(this.sort); } - if(!this.resultType.equals(this.domainType.getType())){ + if (requiresProjection(this.resultType)){ queryToUse = queryToUse.as(this.resultType); + } else { + queryToUse = queryToUse.project(buildPropertyPaths(environment.getSelectionSet(), this.resultType)); } return queryToUse.first(); @@ -470,12 +475,14 @@ public Iterable get(DataFetchingEnvironment environment) { return this.executor.findBy(buildPredicate(environment), q -> { FetchableFluentQuery queryToUse = (FetchableFluentQuery) q; - if(this.sort.isSorted()){ + if (this.sort.isSorted()){ queryToUse = queryToUse.sortBy(this.sort); } - if(!this.resultType.equals(this.domainType.getType())){ + if (requiresProjection(this.resultType)){ queryToUse = queryToUse.as(this.resultType); + } else { + queryToUse = queryToUse.project(buildPropertyPaths(environment.getSelectionSet(), this.resultType)); } return queryToUse.all(); @@ -511,12 +518,14 @@ public Mono get(DataFetchingEnvironment environment) { return this.executor.findBy(buildPredicate(environment), q -> { FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) q; - if(this.sort.isSorted()){ + if (this.sort.isSorted()){ queryToUse = queryToUse.sortBy(this.sort); } - if(!this.resultType.equals(this.domainType.getType())){ + if (requiresProjection(this.resultType)){ queryToUse = queryToUse.as(this.resultType); + } else { + queryToUse = queryToUse.project(buildPropertyPaths(environment.getSelectionSet(), this.resultType)); } return queryToUse.first(); @@ -552,12 +561,14 @@ public Flux get(DataFetchingEnvironment environment) { return this.executor.findBy(buildPredicate(environment), q -> { FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) q; - if(this.sort.isSorted()){ + if (this.sort.isSorted()){ queryToUse = queryToUse.sortBy(this.sort); } - if(!this.resultType.equals(this.domainType.getType())){ + if (requiresProjection(this.resultType)){ queryToUse = queryToUse.as(this.resultType); + } else { + queryToUse = queryToUse.project(buildPropertyPaths(environment.getSelectionSet(), this.resultType)); } return queryToUse.all(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java index 956e45380..f7ad96bfc 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/Book.java @@ -17,6 +17,7 @@ package org.springframework.graphql.data.querydsl; import org.springframework.data.annotation.Id; +import org.springframework.graphql.Author; public class Book { @@ -24,12 +25,12 @@ public class Book { String name; - String author; + Author author; public Book() { } - public Book(Long id, String name, String author) { + public Book(Long id, String name, Author author) { this.id = id; this.name = name; this.author = author; @@ -51,11 +52,11 @@ public void setName(String name) { this.name = name; } - public String getAuthor() { + public Author getAuthor() { return this.author; } - public void setAuthor(String author) { + public void setAuthor(Author author) { this.author = author; } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java index 7f797388d..e3a936005 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querydsl/QuerydslDataFetcherTests.java @@ -41,6 +41,7 @@ import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.Repository; +import org.springframework.graphql.Author; import org.springframework.graphql.data.GraphQlRepository; import org.springframework.graphql.execution.ExecutionGraphQlService; import org.springframework.graphql.execution.GraphQlSource; @@ -66,7 +67,7 @@ class QuerydslDataFetcherTests { @Test void shouldFetchSingleItems() { - Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); + Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); mockRepository.save(book); BiConsumer, QuerydslPredicateExecutor> tester = @@ -91,8 +92,8 @@ void shouldFetchSingleItems() { @Test void shouldFetchMultipleItems() { - Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - Book book2 = new Book(53L, "Breaking Bad", "Heisenberg"); + Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); + Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg")); mockRepository.saveAll(Arrays.asList(book1, book2)); BiConsumer, QuerydslPredicateExecutor> tester = @@ -118,7 +119,7 @@ void shouldFetchMultipleItems() { @Test void shouldFavorExplicitWiring() { MockRepository mockRepository = mock(MockRepository.class); - Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); + Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); when(mockRepository.findBy(any(), any())).thenReturn(Optional.of(book)); // 1) Automatic registration only @@ -130,7 +131,7 @@ void shouldFavorExplicitWiring() { // 2) Automatic registration and explicit wiring handler = initWebGraphQlHandler( - builder -> builder.dataFetcher("bookById", env -> new Book(53L, "Breaking Bad", "Heisenberg")), + builder -> builder.dataFetcher("bookById", env -> new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"))), mockRepository, null); output = handler.handleRequest(input("{ bookById(id: 1) {name}}")).block(); @@ -141,7 +142,7 @@ void shouldFavorExplicitWiring() { @Test void shouldFetchSingleItemsWithInterfaceProjection() { - Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); + Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); mockRepository.save(book); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder @@ -159,7 +160,7 @@ void shouldFetchSingleItemsWithInterfaceProjection() { @Test void shouldFetchSingleItemsWithDtoProjection() { - Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); + Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); mockRepository.save(book); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder @@ -188,7 +189,6 @@ void shouldConstructPredicateProperly() { handler.handleRequest(input("{ books(name: \"H\", author: \"Doug\") {name}}")).block(); - ArgumentCaptor predicateCaptor = ArgumentCaptor.forClass(Predicate.class); verify(mockRepository).findBy(predicateCaptor.capture(), any()); @@ -199,7 +199,7 @@ void shouldConstructPredicateProperly() { @Test void shouldReactivelyFetchSingleItems() { ReactiveMockRepository mockRepository = mock(ReactiveMockRepository.class); - Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); + Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); when(mockRepository.findBy(any(), any())).thenReturn(Mono.just(book)); BiConsumer, ReactiveQuerydslPredicateExecutor> tester = @@ -225,8 +225,8 @@ void shouldReactivelyFetchSingleItems() { @Test void shouldReactivelyFetchMultipleItems() { ReactiveMockRepository mockRepository = mock(ReactiveMockRepository.class); - Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); - Book book2 = new Book(53L, "Breaking Bad", "Heisenberg"); + Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")); + Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg")); when(mockRepository.findBy(any(), any())).thenReturn(Flux.just(book1, book2)); BiConsumer, ReactiveQuerydslPredicateExecutor> tester = @@ -304,7 +304,7 @@ private WebInput input(String query) { interface BookProjection { - @Value("#{target.name + ' by ' + target.author}") + @Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}") String getName(); }