From 2bd32511083d5aa052da869d36c3646253fa2025 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 1 Apr 2021 12:16:34 +0200 Subject: [PATCH 1/3] Add Querydsl integration Spring Data repositories that support Querydsl are now supported as DataFetchers returning single objects and iterables including projection support. --- build.gradle | 3 +- samples/webmvc-http/build.gradle | 15 +- .../repository/ArtifactRepositories.java | 3 +- .../ArtifactRepositoryDataWiring.java | 8 +- spring-graphql/build.gradle | 6 + .../data/DtoInstantiatingConverter.java | 106 +++++++ .../graphql/data/DtoMappingContext.java | 82 ++++++ .../data/QuerydslDataFetcherSupport.java | 260 ++++++++++++++++++ .../graphql/data/package-info.java | 25 ++ .../springframework/graphql/data/Book.java | 60 ++++ .../springframework/graphql/data/QBook.java | 47 ++++ .../data/QuerydslDataFetcherSupportTests.java | 204 ++++++++++++++ .../src/test/resources/books/schema.graphqls | 1 + 13 files changed, 815 insertions(+), 5 deletions(-) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/DtoInstantiatingConverter.java create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/package-info.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/Book.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/QBook.java create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java diff --git a/build.gradle b/build.gradle index 4e427c411..f614aa74f 100644 --- a/build.gradle +++ b/build.gradle @@ -30,12 +30,13 @@ configure(moduleProjects) { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - + dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.3" mavenBom "io.projectreactor:reactor-bom:2020.0.7" mavenBom "org.springframework:spring-framework-bom:5.3.7" + mavenBom "org.springframework.data:spring-data-bom:2021.0.1" mavenBom "org.junit:junit-bom:5.7.2" } dependencies { diff --git a/samples/webmvc-http/build.gradle b/samples/webmvc-http/build.gradle index 299812d6a..7509c4e08 100644 --- a/samples/webmvc-http/build.gradle +++ b/samples/webmvc-http/build.gradle @@ -18,7 +18,20 @@ dependencies { testImplementation project(':spring-graphql-test') testImplementation 'org.springframework:spring-webflux' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + implementation( + "com.querydsl:querydsl-core:4.4.0", + "com.querydsl:querydsl-jpa:4.4.0" + ) + annotationProcessor "com.querydsl:querydsl-apt:4.4.0:jpa", + "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final", + "javax.annotation:javax.annotation-api:1.3.2" +} + +compileJava { + options.annotationProcessorPath = configurations.annotationProcessor } + test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java index 44cd614e4..72aafe2a2 100644 --- a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java +++ b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java @@ -1,7 +1,8 @@ package io.spring.sample.graphql.repository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.CrudRepository; -public interface ArtifactRepositories extends CrudRepository { +public interface ArtifactRepositories extends CrudRepository, QuerydslPredicateExecutor { } diff --git a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java index 2b119fc49..f22ab1ee1 100644 --- a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java +++ b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java @@ -1,7 +1,9 @@ package io.spring.sample.graphql.repository; import graphql.schema.idl.RuntimeWiring; + import org.springframework.graphql.boot.RuntimeWiringCustomizer; +import org.springframework.graphql.data.QuerydslDataFetcherSupport; import org.springframework.stereotype.Component; @Component @@ -16,8 +18,10 @@ public ArtifactRepositoryDataWiring(ArtifactRepositories repositories) { @Override public void customize(RuntimeWiring.Builder builder) { builder.type("Query", - typeWiring -> typeWiring.dataFetcher("artifactRepositories", env -> this.repositories.findAll()) - .dataFetcher("artifactRepository", env -> this.repositories.findById(env.getArgument("id")))); + typeWiring -> typeWiring.dataFetcher("artifactRepositories", QuerydslDataFetcherSupport + .builder(repositories).many()) + .dataFetcher("artifactRepository", QuerydslDataFetcherSupport + .builder(repositories).single())); } } diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index eac17b212..ec37410d3 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -11,13 +11,19 @@ dependencies { compileOnly 'org.springframework:spring-websocket' compileOnly 'javax.servlet:javax.servlet-api:4.0.1' + compileOnly 'com.querydsl:querydsl-core:4.4.0' + compileOnly 'org.springframework.data:spring-data-commons' + testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core:3.11.1' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.springframework:spring-webflux' testImplementation 'org.springframework:spring-webmvc' testImplementation 'org.springframework:spring-websocket' testImplementation 'org.springframework:spring-test' + testImplementation 'org.springframework.data:spring-data-commons' + testImplementation 'com.querydsl:querydsl-core:4.4.0' testImplementation 'javax.servlet:javax.servlet-api:4.0.1' testImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/DtoInstantiatingConverter.java b/spring-graphql/src/main/java/org/springframework/graphql/data/DtoInstantiatingConverter.java new file mode 100644 index 000000000..3c7d957c5 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/DtoInstantiatingConverter.java @@ -0,0 +1,106 @@ +/* + * 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; + +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 = context + .getRequiredPersistentEntity(source.getClass()); + + PersistentPropertyAccessor sourceAccessor = sourceEntity + .getPropertyAccessor(source); + PersistentEntity targetEntity = context + .getRequiredPersistentEntity(targetType); + PreferredConstructor> constructor = targetEntity + .getPersistenceConstructor(); + + @SuppressWarnings({"rawtypes", "unchecked"}) + Object dto = instantiator + .createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity + .getRequiredPersistentProperty(parameter.getName())); + } + }); + + PersistentPropertyAccessor dtoAccessor = targetEntity + .getPropertyAccessor(dto); + + targetEntity.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/DtoMappingContext.java b/spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java new file mode 100644 index 000000000..73a7c0780 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java @@ -0,0 +1,82 @@ +/* + * 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; + +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/QuerydslDataFetcherSupport.java b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java new file mode 100644 index 000000000..7126ed9e5 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java @@ -0,0 +1,260 @@ +/* + * 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; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.Predicate; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; + +import org.springframework.core.ResolvableType; +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.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.querydsl.binding.QuerydslBindings; +import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Utility to implement {@link DataFetcher} based on Querydsl {@link Predicate} through {@link QuerydslPredicateExecutor}. + * Actual instances can be created through a {@link #builder(QuerydslPredicateExecutor) builder} to query for + * {@link Builder#single()} or {@link Builder#many()} objects. + * Example: + *
+ * interface BookRepository extends Repository<Book, String>, QuerydslPredicateExecutor<Book>{}
+ *
+ * BookRepository repository = …;
+ * TypeRuntimeWiring wiring = …;
+ *
+ * wiring.dataFetcher("books", QuerydslDataFetcherSupport.builder(repository).many())
+ *       .dataFetcher("book", QuerydslDataFetcherSupport.builder(repositories).single());
+ * 
+ * + * @param returned result type + * @author Mark Paluch + * @since 1.0.0 + * @see QuerydslPredicateExecutor + * @see Predicate + * @see QuerydslBinderCustomizer + */ +public abstract class QuerydslDataFetcherSupport { + + private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder(DefaultConversionService + .getSharedInstance(), SimpleEntityPathResolver.INSTANCE); + + private final TypeInformation domainType; + + private final QuerydslBinderCustomizer> customizer; + + QuerydslDataFetcherSupport(QuerydslPredicateExecutor executor, QuerydslBinderCustomizer> customizer) { + + this.customizer = customizer; + + Class repositoryInterface = getRepositoryInterface(executor); + + DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + domainType = ClassTypeInformation.from(metadata.getDomainType()); + } + + private static Class getRepositoryInterface(QuerydslPredicateExecutor executor) { + + Type[] genericInterfaces = executor.getClass().getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + + ResolvableType resolvableType = ResolvableType.forType(genericInterface); + + if (MergedAnnotations.from(resolvableType.getRawClass()) + .isPresent(NoRepositoryBean.class)) { + continue; + } + + if (Repository.class.isAssignableFrom(resolvableType.getRawClass())) { + return resolvableType.getRawClass(); + } + } + + throw new IllegalArgumentException(String + .format("Cannot resolve repository interface from %s", executor)); + } + + /** + * Create a new {@link Builder} accepting {@link QuerydslPredicateExecutor}. + * @param executor the repository object to use + * @param result type + * @return a new builder + */ + public static Builder builder(QuerydslPredicateExecutor executor) { + return new Builder<>(executor, (bindings, root) -> { + }, Function.identity()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + Predicate buildPredicate(DataFetchingEnvironment environment) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + QuerydslBindings bindings = new QuerydslBindings(); + + EntityPath path = SimpleEntityPathResolver.INSTANCE + .createPath(domainType.getType()); + + customizer.customize(bindings, path); + + for (Map.Entry entry : environment.getArguments().entrySet()) { + parameters.put(entry.getKey(), Collections.singletonList(entry.getValue())); + } + + return BUILDER + .getPredicate(domainType, (MultiValueMap) parameters, bindings); + } + + /** + * Builder for a Querydsl-based {@link DataFetcher}. Note that builder instances are immutable and return a new + * instance of the builder when calling configuration methods. + * @param domain type + * @param result type + */ + public static class Builder { + + private final QuerydslPredicateExecutor executor; + + private final QuerydslBinderCustomizer> customizer; + + private final Function resultConverter; + + Builder(QuerydslPredicateExecutor executor, QuerydslBinderCustomizer> customizer, + Function resultConverter) { + this.executor = executor; + this.customizer = customizer; + this.resultConverter = resultConverter; + } + + /** + * Project results returned from the {@link QuerydslPredicateExecutor} into the target + * {@code projectionType}. Projection types can be either interfaces declaring getters + * for properties to expose or regular classes outside the entity type hierarchy for + * DTO projection. + * @param projectionType projection type + * @return a new {@link Builder} instance with all previously configured options and {@code projectionType} + * applied + */ + public

Builder projectAs(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 new Builder<>(executor, customizer, element -> projectionFactory + .createProjection(projectionType, element)); + } + + DtoInstantiatingConverter

converter = new DtoInstantiatingConverter<>(projectionType, + new DtoMappingContext(), new EntityInstantiators()); + return new Builder<>(executor, customizer, converter::convert); + } + + /** + * Apply a {@link QuerydslBinderCustomizer}. + * @param customizer the customizer to customize bindings for the actual query + * @return a new {@link Builder} instance with all previously configured options and + * {@code QuerydslBinderCustomizer} applied + */ + public Builder customizer(QuerydslBinderCustomizer> customizer) { + Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); + return new Builder<>(executor, customizer, resultConverter); + } + + /** + * Build a {@link DataFetcher} to fetch single object instances. + * @return a {@link DataFetcher} based on Querydsl to fetch one object + */ + public DataFetcher single() { + return new SingleEntityQuerydslDataFetcher<>(executor, customizer, resultConverter); + } + + /** + * Build a {@link DataFetcher} to fetch many object instances. + * @return a {@link DataFetcher} based on Querydsl to fetch many objects + */ + public DataFetcher> many() { + return new ManyEntityQuerydslDataFetcher<>(executor, customizer, resultConverter); + } + + } + + static class ManyEntityQuerydslDataFetcher extends QuerydslDataFetcherSupport implements DataFetcher> { + + private final QuerydslPredicateExecutor executor; + + private final Function resultConverter; + + + @SuppressWarnings({"unchecked", "rawtypes"}) + ManyEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, + QuerydslBinderCustomizer> customizer, Function resultConverter) { + super(executor, (QuerydslBinderCustomizer) customizer); + this.executor = executor; + this.resultConverter = resultConverter; + } + + @Override + public Iterable get(DataFetchingEnvironment environment) { + return Streamable.of(executor.findAll(buildPredicate(environment))) + .map(resultConverter).toList(); + } + + } + + static class SingleEntityQuerydslDataFetcher extends QuerydslDataFetcherSupport implements DataFetcher { + + private final QuerydslPredicateExecutor executor; + + private final Function resultConverter; + + @SuppressWarnings({"unchecked", "rawtypes"}) + SingleEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, + QuerydslBinderCustomizer> customizer, Function resultConverter) { + super(executor, (QuerydslBinderCustomizer) customizer); + this.executor = executor; + this.resultConverter = resultConverter; + } + + @Override + public R get(DataFetchingEnvironment environment) { + return executor.findOne(buildPredicate(environment)).map(resultConverter) + .orElse(null); + } + + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/data/package-info.java new file mode 100644 index 000000000..bcf088223 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-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. + */ + +/** + * Support for integrating Spring Data data fetchers. + */ +@NonNullApi +@NonNullFields +package org.springframework.graphql.data; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/Book.java b/spring-graphql/src/test/java/org/springframework/graphql/data/Book.java new file mode 100644 index 000000000..6be2d6a98 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/Book.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-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; + +public class Book { + + Long id; + + String name; + + String author; + + public Book() { + } + + public Book(Long id, String name, String author) { + this.id = id; + this.name = name; + this.author = author; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAuthor() { + return this.author; + } + + public void setAuthor(String author) { + this.author = author; + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/QBook.java b/spring-graphql/src/test/java/org/springframework/graphql/data/QBook.java new file mode 100644 index 000000000..cccf1f516 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/QBook.java @@ -0,0 +1,47 @@ +/* + * 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; + +import com.querydsl.core.types.Path; +import com.querydsl.core.types.PathMetadata; +import com.querydsl.core.types.PathMetadataFactory; +import com.querydsl.core.types.dsl.EntityPathBase; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.StringPath; + +/** + * Generated by Querydsl. + */ +public class QBook extends EntityPathBase { + private static final long serialVersionUID = 1773522017L; + public static final QBook book = new QBook("book"); + public final StringPath author = this.createString("author"); + public final NumberPath id = this.createNumber("id", Long.class); + public final StringPath name = this.createString("name"); + + public QBook(String variable) { + super(Book.class, PathMetadataFactory.forVariable(variable)); + } + + public QBook(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QBook(PathMetadata metadata) { + super(Book.class, metadata); + } +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java new file mode 100644 index 000000000..21febd711 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java @@ -0,0 +1,204 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Consumer; + +import com.querydsl.core.types.Predicate; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.TypeRuntimeWiring; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.repository.Repository; +import org.springframework.graphql.execution.ExecutionGraphQlService; +import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.web.WebInput; +import org.springframework.graphql.web.WebOutput; +import org.springframework.http.HttpHeaders; + +/** + * Unit tests for {@link QuerydslDataFetcherSupport}. + */ +class QuerydslDataFetcherSupportTests { + + @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)); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("bookById", QuerydslDataFetcherSupport + .builder(mockRepository) + .single())); + + WebOutput output = handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block(); + + // TODO: getData interferes with method overries + assertThat((Object) output.getData()) + .isEqualTo(Collections.singletonMap("bookById", Collections + .singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + } + + @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((Predicate) null)) + .thenReturn(Arrays.asList(book1, book2)); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("books", QuerydslDataFetcherSupport + .builder(mockRepository) + .many())); + + WebOutput output = handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ books {name}}"), "1")).block(); + + assertThat((Object) output.getData()) + .isEqualTo(Collections.singletonMap("books", Arrays.asList(Collections + .singletonMap("name", "Hitchhiker's Guide to the Galaxy"), Collections + .singletonMap("name", "Breaking Bad")))); + } + + @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)); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("bookById", QuerydslDataFetcherSupport + .builder(mockRepository) + .projectAs(BookProjection.class) + .single())); + + WebOutput output = handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block(); + + assertThat((Object) output.getData()) + .isEqualTo(Collections.singletonMap("bookById", Collections + .singletonMap("name", "Hitchhiker's Guide to the Galaxy by Douglas Adams"))); + } + + @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)); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("bookById", QuerydslDataFetcherSupport + .builder(mockRepository) + .projectAs(BookDto.class) + .single())); + + WebOutput output = handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block(); + + assertThat((Object) output.getData()) + .isEqualTo(Collections.singletonMap("bookById", Collections + .singletonMap("name", "The book is: Hitchhiker's Guide to the Galaxy"))); + } + + @Test + void shouldConstructPredicateProperly() { + MockRepository mockRepository = mock(MockRepository.class); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("books", QuerydslDataFetcherSupport + .builder(mockRepository) + .customizer((QuerydslBinderCustomizer) (bindings, book) -> bindings.bind(book.name) + .firstOptional((path, value) -> value.map(path::startsWith))) + .many())); + + handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ books(name: \"H\", author: \"Doug\") {name}}"), "1")).block(); + + + ArgumentCaptor predicateCaptor = ArgumentCaptor.forClass(Predicate.class); + verify(mockRepository).findAll(predicateCaptor.capture()); + + Predicate predicate = predicateCaptor.getValue(); + + assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H") + .and(QBook.book.author.eq("Doug"))); + } + + interface MockRepository extends Repository, QuerydslPredicateExecutor { + + } + + static WebGraphQlHandler initWebGraphQlHandler(Consumer configurer) { + return WebGraphQlHandler + .builder(new ExecutionGraphQlService(graphQlSource(configurer))) + .build(); + } + + private static GraphQlSource graphQlSource(Consumer configurer) { + RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring(); + TypeRuntimeWiring.Builder wiringBuilder = TypeRuntimeWiring + .newTypeWiring("Query"); + configurer.accept(wiringBuilder); + builder.type(wiringBuilder); + return GraphQlSource.builder() + .schemaResource(new ClassPathResource("books/schema.graphqls")) + .runtimeWiring(builder.build()) + .build(); + } + + interface BookProjection { + + @Value("#{target.name + ' by ' + target.author}") + String getName(); + + } + + static class BookDto { + + private final String name; + + public BookDto(String name) { + this.name = name; + } + + public String getName() { + return "The book is: " + name; + } + } + +} diff --git a/spring-graphql/src/test/resources/books/schema.graphqls b/spring-graphql/src/test/resources/books/schema.graphqls index cb787d52a..b6920e1cf 100644 --- a/spring-graphql/src/test/resources/books/schema.graphqls +++ b/spring-graphql/src/test/resources/books/schema.graphqls @@ -1,5 +1,6 @@ type Query { bookById(id: ID): Book + books(id: ID, name: String, author: String): [Book] } type Book { From c79a8aeb6ba3b5abde96be4d038e87a3dc9eccaf Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 23 Jun 2021 14:43:15 +0200 Subject: [PATCH 2/3] Revise QuerydslDataFetcher Rename QuerydslDataFetcherSupport to QuerydslDataFetcher. Add reactive Querydsl support. --- .../ArtifactRepositoryDataWiring.java | 6 +- .../graphql/data/QuerydslDataFetcher.java | 398 ++++++++++++++++++ .../data/QuerydslDataFetcherSupport.java | 260 ------------ ...sts.java => QuerydslDataFetcherTests.java} | 68 ++- 4 files changed, 461 insertions(+), 271 deletions(-) create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java delete mode 100644 spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java rename spring-graphql/src/test/java/org/springframework/graphql/data/{QuerydslDataFetcherSupportTests.java => QuerydslDataFetcherTests.java} (74%) diff --git a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java index f22ab1ee1..e45882f86 100644 --- a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java +++ b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java @@ -3,7 +3,7 @@ import graphql.schema.idl.RuntimeWiring; import org.springframework.graphql.boot.RuntimeWiringCustomizer; -import org.springframework.graphql.data.QuerydslDataFetcherSupport; +import org.springframework.graphql.data.QuerydslDataFetcher; import org.springframework.stereotype.Component; @Component @@ -18,9 +18,9 @@ public ArtifactRepositoryDataWiring(ArtifactRepositories repositories) { @Override public void customize(RuntimeWiring.Builder builder) { builder.type("Query", - typeWiring -> typeWiring.dataFetcher("artifactRepositories", QuerydslDataFetcherSupport + typeWiring -> typeWiring.dataFetcher("artifactRepositories", QuerydslDataFetcher .builder(repositories).many()) - .dataFetcher("artifactRepository", QuerydslDataFetcherSupport + .dataFetcher("artifactRepository", QuerydslDataFetcher .builder(repositories).single())); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java new file mode 100644 index 000000000..9dc4d4717 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java @@ -0,0 +1,398 @@ +/* + * 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; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.Predicate; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +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.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.querydsl.binding.QuerydslBindings; +import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Utility to implement {@link DataFetcher} based on Querydsl {@link Predicate} through {@link QuerydslPredicateExecutor}. + * Actual instances can be created through a {@link #builder(QuerydslPredicateExecutor) builder} to query for + * {@link QuerydslDataFetcherBuilder#single()} or {@link QuerydslDataFetcherBuilder#many()} objects. + * Example: + *

+ * interface BookRepository extends Repository<Book, String>, QuerydslPredicateExecutor<Book>{}
+ *
+ * BookRepository repository = …;
+ * TypeRuntimeWiring wiring = …;
+ *
+ * wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
+ *       .dataFetcher("book", QuerydslDataFetcher.builder(repositories).single());
+ * 
+ * + * @param returned result type + * @author Mark Paluch + * @since 1.0.0 + * @see QuerydslPredicateExecutor + * @see Predicate + * @see QuerydslBinderCustomizer + */ +public abstract class QuerydslDataFetcher { + + private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder(DefaultConversionService + .getSharedInstance(), SimpleEntityPathResolver.INSTANCE); + + private final TypeInformation domainType; + + private final QuerydslBinderCustomizer> customizer; + + QuerydslDataFetcher(ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer) { + this.customizer = customizer; + this.domainType = domainType; + } + + /** + * Create a new {@link QuerydslDataFetcherBuilder} accepting {@link QuerydslPredicateExecutor}. + * @param executor the repository object to use + * @param result type + * @return a new builder + */ + @SuppressWarnings("unchecked") + public static QuerydslDataFetcherBuilder builder(QuerydslPredicateExecutor executor) { + + Class repositoryInterface = getRepositoryInterface(executor); + DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + + return new QuerydslDataFetcherBuilder<>(executor, (ClassTypeInformation) ClassTypeInformation + .from(metadata.getDomainType()), (bindings, root) -> { + }, Function.identity()); + } + + /** + * Create a new {@link ReactiveQuerydslDataFetcherBuilder} accepting {@link ReactiveQuerydslPredicateExecutor}. + * @param executor the repository object to use + * @param result type + * @return a new builder + */ + @SuppressWarnings("unchecked") + public static ReactiveQuerydslDataFetcherBuilder builder(ReactiveQuerydslPredicateExecutor executor) { + + Class repositoryInterface = getRepositoryInterface(executor); + DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + + return new ReactiveQuerydslDataFetcherBuilder<>(executor, (ClassTypeInformation) ClassTypeInformation + .from(metadata.getDomainType()), (bindings, root) -> { + }, Function.identity()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + Predicate buildPredicate(DataFetchingEnvironment environment) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + QuerydslBindings bindings = new QuerydslBindings(); + + EntityPath path = SimpleEntityPathResolver.INSTANCE + .createPath(domainType.getType()); + + customizer.customize(bindings, path); + + for (Map.Entry entry : environment.getArguments().entrySet()) { + parameters.put(entry.getKey(), Collections.singletonList(entry.getValue())); + } + + return BUILDER + .getPredicate(domainType, (MultiValueMap) parameters, bindings); + } + + private static Function createProjectionFunction(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; + } + + private static Class getRepositoryInterface(Object executor) { + + Type[] genericInterfaces = executor.getClass().getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + + ResolvableType resolvableType = ResolvableType.forType(genericInterface); + + if (resolvableType.getRawClass() == null || MergedAnnotations + .from(resolvableType.getRawClass()) + .isPresent(NoRepositoryBean.class)) { + continue; + } + + if (Repository.class.isAssignableFrom(resolvableType.getRawClass())) { + return resolvableType.getRawClass(); + } + } + + throw new IllegalArgumentException(String + .format("Cannot resolve repository interface from %s", executor)); + } + + /** + * Builder for a Querydsl-based {@link DataFetcher}. Note that builder instances are immutable and return a new + * instance of the builder when calling configuration methods. + * @param domain type + * @param result type + */ + public static class QuerydslDataFetcherBuilder { + + private final QuerydslPredicateExecutor executor; + + private final ClassTypeInformation domainType; + + private final QuerydslBinderCustomizer> customizer; + + private final Function resultConverter; + + QuerydslDataFetcherBuilder(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer, + Function resultConverter) { + this.executor = executor; + this.domainType = domainType; + this.customizer = customizer; + this.resultConverter = resultConverter; + } + + /** + * Project results returned from the {@link QuerydslPredicateExecutor} into the target + * {@code projectionType}. Projection types can be either interfaces declaring getters + * for properties to expose or regular classes outside the entity type hierarchy for + * DTO projection. + * @param projectionType projection type + * @return a new {@link QuerydslDataFetcherBuilder} instance with all previously configured options and {@code projectionType} + * applied + */ + public

QuerydslDataFetcherBuilder projectAs(Class

projectionType) { + Assert.notNull(projectionType, "Projection type must not be null"); + return new QuerydslDataFetcherBuilder<>(executor, domainType, customizer, createProjectionFunction(projectionType)); + } + + /** + * Apply a {@link QuerydslBinderCustomizer}. + * @param customizer the customizer to customize bindings for the actual query + * @return a new {@link QuerydslDataFetcherBuilder} instance with all previously configured options and + * {@code QuerydslBinderCustomizer} applied + */ + public QuerydslDataFetcherBuilder customizer(QuerydslBinderCustomizer> customizer) { + Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); + return new QuerydslDataFetcherBuilder<>(executor, domainType, customizer, resultConverter); + } + + /** + * Build a {@link DataFetcher} to fetch single object instances. + * @return a {@link DataFetcher} based on Querydsl to fetch one object + */ + public DataFetcher single() { + return new SingleEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + } + + /** + * Build a {@link DataFetcher} to fetch many object instances. + * @return a {@link DataFetcher} based on Querydsl to fetch many objects + */ + public DataFetcher> many() { + return new ManyEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + } + + } + + /** + * Builder for a reactive Querydsl-based {@link DataFetcher}. Note that builder instances are immutable and return a new + * instance of the builder when calling configuration methods. + * @param domain type + * @param result type + */ + public static class ReactiveQuerydslDataFetcherBuilder { + + private final ReactiveQuerydslPredicateExecutor executor; + + private final ClassTypeInformation domainType; + + private final QuerydslBinderCustomizer> customizer; + + private final Function resultConverter; + + ReactiveQuerydslDataFetcherBuilder(ReactiveQuerydslPredicateExecutor executor, ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer, + Function resultConverter) { + this.executor = executor; + this.domainType = domainType; + this.customizer = customizer; + this.resultConverter = resultConverter; + } + + /** + * Project results returned from the {@link QuerydslPredicateExecutor} into the target + * {@code projectionType}. Projection types can be either interfaces declaring getters + * for properties to expose or regular classes outside the entity type hierarchy for + * DTO projection. + * @param projectionType projection type + * @return a new {@link ReactiveQuerydslDataFetcherBuilder} instance with all previously configured options and {@code projectionType} + * applied + */ + public

ReactiveQuerydslDataFetcherBuilder projectAs(Class

projectionType) { + Assert.notNull(projectionType, "Projection type must not be null"); + return new ReactiveQuerydslDataFetcherBuilder<>(executor, domainType, customizer, createProjectionFunction(projectionType)); + } + + /** + * Apply a {@link QuerydslBinderCustomizer}. + * @param customizer the customizer to customize bindings for the actual query + * @return a new {@link ReactiveQuerydslDataFetcherBuilder} instance with all previously configured options and + * {@code QuerydslBinderCustomizer} applied + */ + public ReactiveQuerydslDataFetcherBuilder customizer(QuerydslBinderCustomizer> customizer) { + Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); + return new ReactiveQuerydslDataFetcherBuilder<>(executor, domainType, customizer, resultConverter); + } + + /** + * Build a {@link DataFetcher} to fetch single object instances through {@link Mono}. + * @return a {@link DataFetcher} based on Querydsl to fetch one object + */ + public DataFetcher> single() { + return new ReactiveSingleEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + } + + /** + * Build a {@link DataFetcher} to fetch many object instances through {@link Flux}. + * @return a {@link DataFetcher} based on Querydsl to fetch many objects + */ + public DataFetcher> many() { + return new ReactiveManyEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + } + + } + + static class SingleEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher { + + private final QuerydslPredicateExecutor executor; + + private final Function resultConverter; + + @SuppressWarnings({"unchecked", "rawtypes"}) + SingleEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, Function resultConverter) { + super(domainType, (QuerydslBinderCustomizer) customizer); + this.executor = executor; + this.resultConverter = resultConverter; + } + + @Override + public R get(DataFetchingEnvironment environment) { + return executor.findOne(buildPredicate(environment)).map(resultConverter) + .orElse(null); + } + + } + + static class ManyEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher> { + + private final QuerydslPredicateExecutor executor; + + private final Function resultConverter; + + @SuppressWarnings({"unchecked", "rawtypes"}) + ManyEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, Function resultConverter) { + super(domainType, (QuerydslBinderCustomizer) customizer); + this.executor = executor; + this.resultConverter = resultConverter; + } + + @Override + public Iterable get(DataFetchingEnvironment environment) { + return Streamable.of(executor.findAll(buildPredicate(environment))) + .map(resultConverter).toList(); + } + + } + + static class ReactiveSingleEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher> { + + private final ReactiveQuerydslPredicateExecutor executor; + + private final Function resultConverter; + + @SuppressWarnings({"unchecked", "rawtypes"}) + ReactiveSingleEntityQuerydslDataFetcher(ReactiveQuerydslPredicateExecutor executor, ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, Function resultConverter) { + super(domainType, (QuerydslBinderCustomizer) customizer); + this.executor = executor; + this.resultConverter = resultConverter; + } + + @Override + public Mono get(DataFetchingEnvironment environment) { + return executor.findOne(buildPredicate(environment)).map(resultConverter); + } + + } + + static class ReactiveManyEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher> { + + private final ReactiveQuerydslPredicateExecutor executor; + + private final Function resultConverter; + + @SuppressWarnings({"unchecked", "rawtypes"}) + ReactiveManyEntityQuerydslDataFetcher(ReactiveQuerydslPredicateExecutor executor, ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, Function resultConverter) { + super(domainType, (QuerydslBinderCustomizer) customizer); + this.executor = executor; + this.resultConverter = resultConverter; + } + + @Override + public Flux get(DataFetchingEnvironment environment) { + return executor.findAll(buildPredicate(environment)).map(resultConverter); + } + + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java deleted file mode 100644 index 7126ed9e5..000000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcherSupport.java +++ /dev/null @@ -1,260 +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; - -import java.lang.reflect.Type; -import java.util.Collections; -import java.util.Map; -import java.util.function.Function; - -import com.querydsl.core.types.EntityPath; -import com.querydsl.core.types.Predicate; -import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; - -import org.springframework.core.ResolvableType; -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.querydsl.QuerydslPredicateExecutor; -import org.springframework.data.querydsl.SimpleEntityPathResolver; -import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; -import org.springframework.data.querydsl.binding.QuerydslBindings; -import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; -import org.springframework.data.repository.NoRepositoryBean; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.Streamable; -import org.springframework.data.util.TypeInformation; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -/** - * Utility to implement {@link DataFetcher} based on Querydsl {@link Predicate} through {@link QuerydslPredicateExecutor}. - * Actual instances can be created through a {@link #builder(QuerydslPredicateExecutor) builder} to query for - * {@link Builder#single()} or {@link Builder#many()} objects. - * Example: - *

- * interface BookRepository extends Repository<Book, String>, QuerydslPredicateExecutor<Book>{}
- *
- * BookRepository repository = …;
- * TypeRuntimeWiring wiring = …;
- *
- * wiring.dataFetcher("books", QuerydslDataFetcherSupport.builder(repository).many())
- *       .dataFetcher("book", QuerydslDataFetcherSupport.builder(repositories).single());
- * 
- * - * @param returned result type - * @author Mark Paluch - * @since 1.0.0 - * @see QuerydslPredicateExecutor - * @see Predicate - * @see QuerydslBinderCustomizer - */ -public abstract class QuerydslDataFetcherSupport { - - private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder(DefaultConversionService - .getSharedInstance(), SimpleEntityPathResolver.INSTANCE); - - private final TypeInformation domainType; - - private final QuerydslBinderCustomizer> customizer; - - QuerydslDataFetcherSupport(QuerydslPredicateExecutor executor, QuerydslBinderCustomizer> customizer) { - - this.customizer = customizer; - - Class repositoryInterface = getRepositoryInterface(executor); - - DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); - domainType = ClassTypeInformation.from(metadata.getDomainType()); - } - - private static Class getRepositoryInterface(QuerydslPredicateExecutor executor) { - - Type[] genericInterfaces = executor.getClass().getGenericInterfaces(); - for (Type genericInterface : genericInterfaces) { - - ResolvableType resolvableType = ResolvableType.forType(genericInterface); - - if (MergedAnnotations.from(resolvableType.getRawClass()) - .isPresent(NoRepositoryBean.class)) { - continue; - } - - if (Repository.class.isAssignableFrom(resolvableType.getRawClass())) { - return resolvableType.getRawClass(); - } - } - - throw new IllegalArgumentException(String - .format("Cannot resolve repository interface from %s", executor)); - } - - /** - * Create a new {@link Builder} accepting {@link QuerydslPredicateExecutor}. - * @param executor the repository object to use - * @param result type - * @return a new builder - */ - public static Builder builder(QuerydslPredicateExecutor executor) { - return new Builder<>(executor, (bindings, root) -> { - }, Function.identity()); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - Predicate buildPredicate(DataFetchingEnvironment environment) { - MultiValueMap parameters = new LinkedMultiValueMap<>(); - QuerydslBindings bindings = new QuerydslBindings(); - - EntityPath path = SimpleEntityPathResolver.INSTANCE - .createPath(domainType.getType()); - - customizer.customize(bindings, path); - - for (Map.Entry entry : environment.getArguments().entrySet()) { - parameters.put(entry.getKey(), Collections.singletonList(entry.getValue())); - } - - return BUILDER - .getPredicate(domainType, (MultiValueMap) parameters, bindings); - } - - /** - * Builder for a Querydsl-based {@link DataFetcher}. Note that builder instances are immutable and return a new - * instance of the builder when calling configuration methods. - * @param domain type - * @param result type - */ - public static class Builder { - - private final QuerydslPredicateExecutor executor; - - private final QuerydslBinderCustomizer> customizer; - - private final Function resultConverter; - - Builder(QuerydslPredicateExecutor executor, QuerydslBinderCustomizer> customizer, - Function resultConverter) { - this.executor = executor; - this.customizer = customizer; - this.resultConverter = resultConverter; - } - - /** - * Project results returned from the {@link QuerydslPredicateExecutor} into the target - * {@code projectionType}. Projection types can be either interfaces declaring getters - * for properties to expose or regular classes outside the entity type hierarchy for - * DTO projection. - * @param projectionType projection type - * @return a new {@link Builder} instance with all previously configured options and {@code projectionType} - * applied - */ - public

Builder projectAs(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 new Builder<>(executor, customizer, element -> projectionFactory - .createProjection(projectionType, element)); - } - - DtoInstantiatingConverter

converter = new DtoInstantiatingConverter<>(projectionType, - new DtoMappingContext(), new EntityInstantiators()); - return new Builder<>(executor, customizer, converter::convert); - } - - /** - * Apply a {@link QuerydslBinderCustomizer}. - * @param customizer the customizer to customize bindings for the actual query - * @return a new {@link Builder} instance with all previously configured options and - * {@code QuerydslBinderCustomizer} applied - */ - public Builder customizer(QuerydslBinderCustomizer> customizer) { - Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); - return new Builder<>(executor, customizer, resultConverter); - } - - /** - * Build a {@link DataFetcher} to fetch single object instances. - * @return a {@link DataFetcher} based on Querydsl to fetch one object - */ - public DataFetcher single() { - return new SingleEntityQuerydslDataFetcher<>(executor, customizer, resultConverter); - } - - /** - * Build a {@link DataFetcher} to fetch many object instances. - * @return a {@link DataFetcher} based on Querydsl to fetch many objects - */ - public DataFetcher> many() { - return new ManyEntityQuerydslDataFetcher<>(executor, customizer, resultConverter); - } - - } - - static class ManyEntityQuerydslDataFetcher extends QuerydslDataFetcherSupport implements DataFetcher> { - - private final QuerydslPredicateExecutor executor; - - private final Function resultConverter; - - - @SuppressWarnings({"unchecked", "rawtypes"}) - ManyEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, - QuerydslBinderCustomizer> customizer, Function resultConverter) { - super(executor, (QuerydslBinderCustomizer) customizer); - this.executor = executor; - this.resultConverter = resultConverter; - } - - @Override - public Iterable get(DataFetchingEnvironment environment) { - return Streamable.of(executor.findAll(buildPredicate(environment))) - .map(resultConverter).toList(); - } - - } - - static class SingleEntityQuerydslDataFetcher extends QuerydslDataFetcherSupport implements DataFetcher { - - private final QuerydslPredicateExecutor executor; - - private final Function resultConverter; - - @SuppressWarnings({"unchecked", "rawtypes"}) - SingleEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, - QuerydslBinderCustomizer> customizer, Function resultConverter) { - super(executor, (QuerydslBinderCustomizer) customizer); - this.executor = executor; - this.resultConverter = resultConverter; - } - - @Override - public R get(DataFetchingEnvironment environment) { - return executor.findOne(buildPredicate(environment)).map(resultConverter) - .orElse(null); - } - - } - -} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java similarity index 74% rename from spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java rename to spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java index 21febd711..85231ab3a 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherSupportTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java @@ -30,10 +30,13 @@ import graphql.schema.idl.TypeRuntimeWiring; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; import org.springframework.data.repository.Repository; import org.springframework.graphql.execution.ExecutionGraphQlService; @@ -44,9 +47,9 @@ import org.springframework.http.HttpHeaders; /** - * Unit tests for {@link QuerydslDataFetcherSupport}. + * Unit tests for {@link QuerydslDataFetcher}. */ -class QuerydslDataFetcherSupportTests { +class QuerydslDataFetcherTests { @Test void shouldFetchSingleItems() { @@ -55,7 +58,7 @@ void shouldFetchSingleItems() { when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("bookById", QuerydslDataFetcherSupport + .dataFetcher("bookById", QuerydslDataFetcher .builder(mockRepository) .single())); @@ -63,7 +66,7 @@ void shouldFetchSingleItems() { URI.create("http://abc.org"), new HttpHeaders(), Collections .singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block(); - // TODO: getData interferes with method overries + // TODO: getData interferes with method overrides assertThat((Object) output.getData()) .isEqualTo(Collections.singletonMap("bookById", Collections .singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); @@ -78,7 +81,7 @@ void shouldFetchMultipleItems() { .thenReturn(Arrays.asList(book1, book2)); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("books", QuerydslDataFetcherSupport + .dataFetcher("books", QuerydslDataFetcher .builder(mockRepository) .many())); @@ -99,7 +102,7 @@ void shouldFetchSingleItemsWithInterfaceProjection() { when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("bookById", QuerydslDataFetcherSupport + .dataFetcher("bookById", QuerydslDataFetcher .builder(mockRepository) .projectAs(BookProjection.class) .single())); @@ -120,7 +123,7 @@ void shouldFetchSingleItemsWithDtoProjection() { when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("bookById", QuerydslDataFetcherSupport + .dataFetcher("bookById", QuerydslDataFetcher .builder(mockRepository) .projectAs(BookDto.class) .single())); @@ -139,7 +142,7 @@ void shouldConstructPredicateProperly() { MockRepository mockRepository = mock(MockRepository.class); WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("books", QuerydslDataFetcherSupport + .dataFetcher("books", QuerydslDataFetcher .builder(mockRepository) .customizer((QuerydslBinderCustomizer) (bindings, book) -> bindings.bind(book.name) .firstOptional((path, value) -> value.map(path::startsWith))) @@ -159,10 +162,58 @@ void shouldConstructPredicateProperly() { .and(QBook.book.author.eq("Doug"))); } + @Test + 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)); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("bookById", QuerydslDataFetcher + .builder(mockRepository) + .single())); + + WebOutput output = handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block(); + + // TODO: getData interferes with method overries + assertThat((Object) output.getData()) + .isEqualTo(Collections.singletonMap("bookById", Collections + .singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + } + + @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"); + when(mockRepository.findAll((Predicate) null)) + .thenReturn(Flux.just(book1, book2)); + + WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder + .dataFetcher("books", QuerydslDataFetcher + .builder(mockRepository) + .many())); + + WebOutput output = handler.handle(new WebInput( + URI.create("http://abc.org"), new HttpHeaders(), Collections + .singletonMap("query", "{ books {name}}"), "1")).block(); + + assertThat((Object) output.getData()) + .isEqualTo(Collections.singletonMap("books", Arrays.asList(Collections + .singletonMap("name", "Hitchhiker's Guide to the Galaxy"), Collections + .singletonMap("name", "Breaking Bad")))); + } + interface MockRepository extends Repository, QuerydslPredicateExecutor { } + interface ReactiveMockRepository extends Repository, ReactiveQuerydslPredicateExecutor { + + } + static WebGraphQlHandler initWebGraphQlHandler(Consumer configurer) { return WebGraphQlHandler .builder(new ExecutionGraphQlService(graphQlSource(configurer))) @@ -199,6 +250,7 @@ public BookDto(String name) { public String getName() { return "The book is: " + name; } + } } From 78aa8f5179375eaf818e52e1abd747fb6f8efa22 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Jun 2021 15:34:09 +0200 Subject: [PATCH 3/3] Reformat code and shorten class names --- .../graphql/data/DtoMappingContext.java | 8 +- .../graphql/data/QuerydslDataFetcher.java | 179 +++++++++++------- 2 files changed, 116 insertions(+), 71 deletions(-) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java b/spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java index 73a7c0780..42c5695f4 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/DtoMappingContext.java @@ -26,8 +26,8 @@ import org.springframework.data.util.TypeInformation; /** - * Lightweight {@link org.springframework.data.mapping.context.MappingContext} to provide class metadata - * for entity to DTO mapping. + * Lightweight {@link org.springframework.data.mapping.context.MappingContext} + * to provide class metadata for entity to DTO mapping. * * @author Mark Paluch * @since 1.0.0 @@ -37,8 +37,8 @@ class DtoMappingContext extends AbstractMappingContext 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. + // 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; diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java index 9dc4d4717..8a412dab4 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java @@ -51,71 +51,96 @@ import org.springframework.util.MultiValueMap; /** - * Utility to implement {@link DataFetcher} based on Querydsl {@link Predicate} through {@link QuerydslPredicateExecutor}. - * Actual instances can be created through a {@link #builder(QuerydslPredicateExecutor) builder} to query for - * {@link QuerydslDataFetcherBuilder#single()} or {@link QuerydslDataFetcherBuilder#many()} objects. - * Example: + * Entrypoint to create {@link DataFetcher} using repositories through Querydsl. + * Exposes builders accepting {@link QuerydslPredicateExecutor} or + * {@link ReactiveQuerydslPredicateExecutor} that support customization of bindings + * and interface- and DTO projections. Instances can be created through a + * {@link #builder(QuerydslPredicateExecutor) builder} to query for + * {@link Builder#single()} or {@link Builder#many()} objects. + *

Example: *

- * interface BookRepository extends Repository<Book, String>, QuerydslPredicateExecutor<Book>{}
+ * interface BookRepository extends Repository<Book, String>,
+ *                              QuerydslPredicateExecutor<Book>{}
  *
  * BookRepository repository = …;
  * TypeRuntimeWiring wiring = …;
  *
  * wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
- *       .dataFetcher("book", QuerydslDataFetcher.builder(repositories).single());
+ *       .dataFetcher("book", QuerydslDataFetcher.builder(repository).single());
+ * 
+ * + *

+ * {@link DataFetcher} returning reactive types such as {@link Mono} and {@link Flux} + * can be constructed from a {@link ReactiveQuerydslPredicateExecutor} using + * {@link #builder(ReactiveQuerydslPredicateExecutor) builder}. + *

For example: + *

+ * interface BookRepository extends Repository<Book, String>,
+ *                              ReactiveQuerydslPredicateExecutor<Book>{}
+ *
+ * BookRepository repository = …;
+ * TypeRuntimeWiring wiring = …;
+ *
+ * wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
+ *       .dataFetcher("book", QuerydslDataFetcher.builder(repository).single());
  * 
* * @param returned result type * @author Mark Paluch * @since 1.0.0 * @see QuerydslPredicateExecutor + * @see ReactiveQuerydslPredicateExecutor * @see Predicate * @see QuerydslBinderCustomizer */ public abstract class QuerydslDataFetcher { - private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder(DefaultConversionService - .getSharedInstance(), SimpleEntityPathResolver.INSTANCE); + private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder( + DefaultConversionService + .getSharedInstance(), SimpleEntityPathResolver.INSTANCE); private final TypeInformation domainType; private final QuerydslBinderCustomizer> customizer; - QuerydslDataFetcher(ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer) { + QuerydslDataFetcher(ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer) { this.customizer = customizer; this.domainType = domainType; } /** - * Create a new {@link QuerydslDataFetcherBuilder} accepting {@link QuerydslPredicateExecutor}. + * Create a new {@link Builder} accepting {@link QuerydslPredicateExecutor} + * to build a {@link DataFetcher}. * @param executor the repository object to use * @param result type * @return a new builder */ @SuppressWarnings("unchecked") - public static QuerydslDataFetcherBuilder builder(QuerydslPredicateExecutor executor) { + public static Builder builder(QuerydslPredicateExecutor executor) { Class repositoryInterface = getRepositoryInterface(executor); DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); - return new QuerydslDataFetcherBuilder<>(executor, (ClassTypeInformation) ClassTypeInformation + return new Builder<>(executor, (ClassTypeInformation) ClassTypeInformation .from(metadata.getDomainType()), (bindings, root) -> { }, Function.identity()); } /** - * Create a new {@link ReactiveQuerydslDataFetcherBuilder} accepting {@link ReactiveQuerydslPredicateExecutor}. + * Create a new {@link ReactiveBuilder} accepting + * {@link ReactiveQuerydslPredicateExecutor} to build a reactive {@link DataFetcher}. * @param executor the repository object to use * @param result type * @return a new builder */ @SuppressWarnings("unchecked") - public static ReactiveQuerydslDataFetcherBuilder builder(ReactiveQuerydslPredicateExecutor executor) { + public static ReactiveBuilder builder(ReactiveQuerydslPredicateExecutor executor) { Class repositoryInterface = getRepositoryInterface(executor); DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); - return new ReactiveQuerydslDataFetcherBuilder<>(executor, (ClassTypeInformation) ClassTypeInformation + return new ReactiveBuilder<>(executor, (ClassTypeInformation) ClassTypeInformation .from(metadata.getDomainType()), (bindings, root) -> { }, Function.identity()); } @@ -134,12 +159,12 @@ Predicate buildPredicate(DataFetchingEnvironment environment) { parameters.put(entry.getKey(), Collections.singletonList(entry.getValue())); } - return BUILDER - .getPredicate(domainType, (MultiValueMap) parameters, bindings); + return BUILDER.getPredicate(domainType, (MultiValueMap) parameters, bindings); } - private static Function createProjectionFunction(Class projectionType) { - // TODO: SpelAwareProxyProjectionFactory, DtoMappingContext, and EntityInstantiators should be reused to avoid duplicate class metadata. + 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()) { @@ -155,6 +180,8 @@ private static Function createProjectionFunction(Class projectio private static Class getRepositoryInterface(Object executor) { + Assert.isInstanceOf(Repository.class, executor); + Type[] genericInterfaces = executor.getClass().getGenericInterfaces(); for (Type genericInterface : genericInterfaces) { @@ -176,12 +203,13 @@ private static Class getRepositoryInterface(Object executor) { } /** - * Builder for a Querydsl-based {@link DataFetcher}. Note that builder instances are immutable and return a new - * instance of the builder when calling configuration methods. + * Builder for a Querydsl-based {@link DataFetcher}. Note that builder + * instances are immutable and return a new instance of the builder + * when calling configuration methods. * @param domain type * @param result type */ - public static class QuerydslDataFetcherBuilder { + public static class Builder { private final QuerydslPredicateExecutor executor; @@ -191,7 +219,8 @@ public static class QuerydslDataFetcherBuilder { private final Function resultConverter; - QuerydslDataFetcherBuilder(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer, + Builder(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, Function resultConverter) { this.executor = executor; this.domainType = domainType; @@ -200,28 +229,30 @@ public static class QuerydslDataFetcherBuilder { } /** - * Project results returned from the {@link QuerydslPredicateExecutor} into the target - * {@code projectionType}. Projection types can be either interfaces declaring getters - * for properties to expose or regular classes outside the entity type hierarchy for + * Project results returned from the {@link QuerydslPredicateExecutor} + * into the target {@code projectionType}. Projection types can be + * either interfaces declaring getters for properties to expose or + * regular classes outside the entity type hierarchy for * DTO projection. * @param projectionType projection type - * @return a new {@link QuerydslDataFetcherBuilder} instance with all previously configured options and {@code projectionType} - * applied + * @return a new {@link Builder} instance with all previously + * configured options and {@code projectionType} applied */ - public

QuerydslDataFetcherBuilder projectAs(Class

projectionType) { + public

Builder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); - return new QuerydslDataFetcherBuilder<>(executor, domainType, customizer, createProjectionFunction(projectionType)); + return new Builder<>(executor, domainType, customizer, createProjection(projectionType)); } /** * Apply a {@link QuerydslBinderCustomizer}. - * @param customizer the customizer to customize bindings for the actual query - * @return a new {@link QuerydslDataFetcherBuilder} instance with all previously configured options and - * {@code QuerydslBinderCustomizer} applied + * @param customizer the customizer to customize bindings for the + * actual query + * @return a new {@link Builder} instance with all previously configured + * options and {@code QuerydslBinderCustomizer} applied */ - public QuerydslDataFetcherBuilder customizer(QuerydslBinderCustomizer> customizer) { + public Builder customizer(QuerydslBinderCustomizer> customizer) { Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); - return new QuerydslDataFetcherBuilder<>(executor, domainType, customizer, resultConverter); + return new Builder<>(executor, domainType, customizer, resultConverter); } /** @@ -229,7 +260,7 @@ public QuerydslDataFetcherBuilder customizer(QuerydslBinderCustomizer single() { - return new SingleEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + return new SingleEntityFetcher<>(executor, domainType, customizer, resultConverter); } /** @@ -237,18 +268,19 @@ public DataFetcher single() { * @return a {@link DataFetcher} based on Querydsl to fetch many objects */ public DataFetcher> many() { - return new ManyEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + return new ManyEntityFetcher<>(executor, domainType, customizer, resultConverter); } } /** - * Builder for a reactive Querydsl-based {@link DataFetcher}. Note that builder instances are immutable and return a new - * instance of the builder when calling configuration methods. + * Builder for a reactive Querydsl-based {@link DataFetcher}. Note that builder + * instances are immutable and return a new instance of the builder when + * calling configuration methods. * @param domain type * @param result type */ - public static class ReactiveQuerydslDataFetcherBuilder { + public static class ReactiveBuilder { private final ReactiveQuerydslPredicateExecutor executor; @@ -258,7 +290,9 @@ public static class ReactiveQuerydslDataFetcherBuilder { private final Function resultConverter; - ReactiveQuerydslDataFetcherBuilder(ReactiveQuerydslPredicateExecutor executor, ClassTypeInformation domainType, QuerydslBinderCustomizer> customizer, + ReactiveBuilder(ReactiveQuerydslPredicateExecutor executor, + ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, Function resultConverter) { this.executor = executor; this.domainType = domainType; @@ -267,28 +301,30 @@ public static class ReactiveQuerydslDataFetcherBuilder { } /** - * Project results returned from the {@link QuerydslPredicateExecutor} into the target - * {@code projectionType}. Projection types can be either interfaces declaring getters - * for properties to expose or regular classes outside the entity type hierarchy for + * Project results returned from the {@link QuerydslPredicateExecutor} + * into the target {@code projectionType}. Projection types can be + * either interfaces declaring getters for properties to expose or + * regular classes outside the entity type hierarchy for * DTO projection. * @param projectionType projection type - * @return a new {@link ReactiveQuerydslDataFetcherBuilder} instance with all previously configured options and {@code projectionType} - * applied + * @return a new {@link Builder} instance with all previously + * configured options and {@code projectionType} applied */ - public

ReactiveQuerydslDataFetcherBuilder projectAs(Class

projectionType) { + public

ReactiveBuilder projectAs(Class

projectionType) { Assert.notNull(projectionType, "Projection type must not be null"); - return new ReactiveQuerydslDataFetcherBuilder<>(executor, domainType, customizer, createProjectionFunction(projectionType)); + return new ReactiveBuilder<>(executor, domainType, customizer, createProjection(projectionType)); } /** * Apply a {@link QuerydslBinderCustomizer}. - * @param customizer the customizer to customize bindings for the actual query - * @return a new {@link ReactiveQuerydslDataFetcherBuilder} instance with all previously configured options and - * {@code QuerydslBinderCustomizer} applied + * @param customizer the customizer to customize bindings for the + * actual query + * @return a new {@link Builder} instance with all previously configured + * options and {@code QuerydslBinderCustomizer} applied */ - public ReactiveQuerydslDataFetcherBuilder customizer(QuerydslBinderCustomizer> customizer) { + public ReactiveBuilder customizer(QuerydslBinderCustomizer> customizer) { Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null"); - return new ReactiveQuerydslDataFetcherBuilder<>(executor, domainType, customizer, resultConverter); + return new ReactiveBuilder<>(executor, domainType, customizer, resultConverter); } /** @@ -296,7 +332,7 @@ public ReactiveQuerydslDataFetcherBuilder customizer(QuerydslBinderCustomi * @return a {@link DataFetcher} based on Querydsl to fetch one object */ public DataFetcher> single() { - return new ReactiveSingleEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + return new ReactiveSingleEntityFetcher<>(executor, domainType, customizer, resultConverter); } /** @@ -304,20 +340,22 @@ public DataFetcher> single() { * @return a {@link DataFetcher} based on Querydsl to fetch many objects */ public DataFetcher> many() { - return new ReactiveManyEntityQuerydslDataFetcher<>(executor, domainType, customizer, resultConverter); + return new ReactiveManyEntityFetcher<>(executor, domainType, customizer, resultConverter); } } - static class SingleEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher { + static class SingleEntityFetcher extends QuerydslDataFetcher implements DataFetcher { private final QuerydslPredicateExecutor executor; private final Function resultConverter; @SuppressWarnings({"unchecked", "rawtypes"}) - SingleEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, Function resultConverter) { + SingleEntityFetcher(QuerydslPredicateExecutor executor, + ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, + Function resultConverter) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; this.resultConverter = resultConverter; @@ -331,15 +369,18 @@ public R get(DataFetchingEnvironment environment) { } - static class ManyEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher> { + static class ManyEntityFetcher extends QuerydslDataFetcher + implements DataFetcher> { private final QuerydslPredicateExecutor executor; private final Function resultConverter; @SuppressWarnings({"unchecked", "rawtypes"}) - ManyEntityQuerydslDataFetcher(QuerydslPredicateExecutor executor, ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, Function resultConverter) { + ManyEntityFetcher(QuerydslPredicateExecutor executor, + ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, + Function resultConverter) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; this.resultConverter = resultConverter; @@ -353,15 +394,17 @@ public Iterable get(DataFetchingEnvironment environment) { } - static class ReactiveSingleEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher> { + static class ReactiveSingleEntityFetcher extends QuerydslDataFetcher implements DataFetcher> { private final ReactiveQuerydslPredicateExecutor executor; private final Function resultConverter; @SuppressWarnings({"unchecked", "rawtypes"}) - ReactiveSingleEntityQuerydslDataFetcher(ReactiveQuerydslPredicateExecutor executor, ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, Function resultConverter) { + ReactiveSingleEntityFetcher(ReactiveQuerydslPredicateExecutor executor, + ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, + Function resultConverter) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; this.resultConverter = resultConverter; @@ -374,15 +417,17 @@ public Mono get(DataFetchingEnvironment environment) { } - static class ReactiveManyEntityQuerydslDataFetcher extends QuerydslDataFetcher implements DataFetcher> { + static class ReactiveManyEntityFetcher extends QuerydslDataFetcher implements DataFetcher> { private final ReactiveQuerydslPredicateExecutor executor; private final Function resultConverter; @SuppressWarnings({"unchecked", "rawtypes"}) - ReactiveManyEntityQuerydslDataFetcher(ReactiveQuerydslPredicateExecutor executor, ClassTypeInformation domainType, - QuerydslBinderCustomizer> customizer, Function resultConverter) { + ReactiveManyEntityFetcher(ReactiveQuerydslPredicateExecutor executor, + ClassTypeInformation domainType, + QuerydslBinderCustomizer> customizer, + Function resultConverter) { super(domainType, (QuerydslBinderCustomizer) customizer); this.executor = executor; this.resultConverter = resultConverter;