From 7fea8730db7ca9860c7980abd44b58bf9722aa8f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Oct 2021 09:11:41 +0200 Subject: [PATCH] Migrate QuerydslDataFetcher to use fluent Querydsl API Remove supporting classes for external projection as projections are handled by Spring Data directly. See gh-168 --- 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 fba83701..67407134 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 7bfeb2b4..4edee3f3 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 b4cc4550..28995b2c 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 ef977bef..00000000 --- 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 b9885596..00000000 --- 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 5eb9d0b3..73beb1ba 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.List; 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.ResolvableType; 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.NoRepositoryBean; 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 abstract class QuerydslDataFetcher { 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 @@ public abstract class QuerydslDataFetcher { 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 abstract class QuerydslDataFetcher { private final ClassTypeInformation domainType; + private final Class resultType; + + private final Sort sort; + private final QuerydslBinderCustomizer> customizer; - private final Function resultConverter; - 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 abstract class QuerydslDataFetcher { 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 abstract class QuerydslDataFetcher { 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 abstract class QuerydslDataFetcher { */ 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 abstract class QuerydslDataFetcher { */ 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 abstract class QuerydslDataFetcher { private final ReactiveQuerydslPredicateExecutor executor; - private final ClassTypeInformation domainType; + private final TypeInformation domainType; + + private final Class resultType; + + private final Sort sort; private final QuerydslBinderCustomizer> customizer; - private final Function resultConverter; - 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 abstract class QuerydslDataFetcher { 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 abstract class QuerydslDataFetcher { 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 abstract class QuerydslDataFetcher { */ public DataFetcher> 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 abstract class QuerydslDataFetcher { */ 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 @@ public abstract class QuerydslDataFetcher { 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 @@ public abstract class QuerydslDataFetcher { 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 @@ public abstract class QuerydslDataFetcher { 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 @@ public abstract class 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 2ff2c140..956e4538 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 49ce15b4..7f797388 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 reactor.core.publisher.Mono; 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 @@ import static org.mockito.Mockito.when; */ 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 @@ class QuerydslDataFetcherTests { @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 @@ class QuerydslDataFetcherTests { 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 @@ class QuerydslDataFetcherTests { 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 @@ class QuerydslDataFetcherTests { @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 @@ class QuerydslDataFetcherTests { .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 @@ class QuerydslDataFetcherTests { @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 @@ class QuerydslDataFetcherTests { .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 @@ class QuerydslDataFetcherTests { 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 @@ class QuerydslDataFetcherTests { 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 @@ class QuerydslDataFetcherTests { 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 @@ class QuerydslDataFetcherTests { @GraphQlRepository - interface MockRepository extends Repository, QuerydslPredicateExecutor { + interface MockRepository extends CrudRepository, QuerydslPredicateExecutor { }