Spring Data repositories that support Querydsl

Spring Data repositories that support Querydsl are now supported
as DataFetchers returning single objects and iterables including
projection support.

See gh-59
This commit is contained in:
Mark Paluch
2021-06-24 15:34:09 +02:00
committed by Rossen Stoyanchev
parent ab99dc2892
commit 2d53fdc73f
13 changed files with 1050 additions and 5 deletions

View File

@@ -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 {

View File

@@ -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()
}
}

View File

@@ -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<ArtifactRepository, String> {
public interface ArtifactRepositories extends CrudRepository<ArtifactRepository, String>, QuerydslPredicateExecutor<ArtifactRepository> {
}

View File

@@ -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.QuerydslDataFetcher;
import org.springframework.stereotype.Component;
@Component
@@ -16,8 +18,10 @@ public class ArtifactRepositoryDataWiring implements RuntimeWiringCustomizer {
@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", QuerydslDataFetcher
.builder(repositories).many())
.dataFetcher("artifactRepository", QuerydslDataFetcher
.builder(repositories).single()));
}
}

View File

@@ -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'

View File

@@ -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<T> implements Converter<Object, T> {
private final Class<T> targetType;
private final MappingContext<? extends PersistentEntity<?, ?>, ? 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<T> dtoType,
MappingContext<? extends PersistentEntity<?, ?>, ? 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<?, ? extends PersistentProperty<?>> 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;
}
}

View File

@@ -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.DtoPersistentEntity<?>,
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 <T> DtoPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
return new DtoPersistentEntity<>(typeInformation);
}
@Override
protected DtoPersistentProperty createPersistentProperty(Property property, DtoPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {
return new DtoPersistentProperty(property, owner, simpleTypeHolder);
}
static class DtoPersistentEntity<T> extends BasicPersistentEntity<T, DtoPersistentProperty> {
public DtoPersistentEntity(TypeInformation<T> information) {
super(information);
}
}
static class DtoPersistentProperty extends AnnotationBasedPersistentProperty<DtoPersistentProperty> {
public DtoPersistentProperty(Property property, PersistentEntity<?, DtoPersistentProperty> owner,
SimpleTypeHolder simpleTypeHolder) {
super(property, owner, simpleTypeHolder);
}
@Override
protected Association<DtoPersistentProperty> createAssociation() {
return null;
}
}
}

View File

@@ -0,0 +1,443 @@
/*
* 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;
/**
* 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.
* <p>Example:
* <pre class="code">
* interface BookRepository extends Repository&lt;Book, String&gt;,
* QuerydslPredicateExecutor&lt;Book&gt;{}
*
* BookRepository repository = …;
* TypeRuntimeWiring wiring = …;
*
* wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
* .dataFetcher("book", QuerydslDataFetcher.builder(repository).single());
* </pre>
*
* <p>
* {@link DataFetcher} returning reactive types such as {@link Mono} and {@link Flux}
* can be constructed from a {@link ReactiveQuerydslPredicateExecutor} using
* {@link #builder(ReactiveQuerydslPredicateExecutor) builder}.
* <p>For example:
* <pre class="code">
* interface BookRepository extends Repository&lt;Book, String&gt;,
* ReactiveQuerydslPredicateExecutor&lt;Book&gt;{}
*
* BookRepository repository = …;
* TypeRuntimeWiring wiring = …;
*
* wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
* .dataFetcher("book", QuerydslDataFetcher.builder(repository).single());
* </pre>
*
* @param <T> returned result type
* @author Mark Paluch
* @since 1.0.0
* @see QuerydslPredicateExecutor
* @see ReactiveQuerydslPredicateExecutor
* @see Predicate
* @see QuerydslBinderCustomizer
*/
public abstract class QuerydslDataFetcher<T> {
private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder(
DefaultConversionService
.getSharedInstance(), SimpleEntityPathResolver.INSTANCE);
private final TypeInformation<T> domainType;
private final QuerydslBinderCustomizer<EntityPath<?>> customizer;
QuerydslDataFetcher(ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<EntityPath<?>> customizer) {
this.customizer = customizer;
this.domainType = domainType;
}
/**
* Create a new {@link Builder} accepting {@link QuerydslPredicateExecutor}
* to build a {@link DataFetcher}.
* @param executor the repository object to use
* @param <T> result type
* @return a new builder
*/
@SuppressWarnings("unchecked")
public static <T> Builder<T, T> builder(QuerydslPredicateExecutor<T> executor) {
Class<?> repositoryInterface = getRepositoryInterface(executor);
DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
return new Builder<>(executor, (ClassTypeInformation<T>) ClassTypeInformation
.from(metadata.getDomainType()), (bindings, root) -> {
}, Function.identity());
}
/**
* Create a new {@link ReactiveBuilder} accepting
* {@link ReactiveQuerydslPredicateExecutor} to build a reactive {@link DataFetcher}.
* @param executor the repository object to use
* @param <T> result type
* @return a new builder
*/
@SuppressWarnings("unchecked")
public static <T> ReactiveBuilder<T, T> builder(ReactiveQuerydslPredicateExecutor<T> executor) {
Class<?> repositoryInterface = getRepositoryInterface(executor);
DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
return new ReactiveBuilder<>(executor, (ClassTypeInformation<T>) ClassTypeInformation
.from(metadata.getDomainType()), (bindings, root) -> {
}, Function.identity());
}
@SuppressWarnings({"unchecked", "rawtypes"})
Predicate buildPredicate(DataFetchingEnvironment environment) {
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
QuerydslBindings bindings = new QuerydslBindings();
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE
.createPath(domainType.getType());
customizer.customize(bindings, path);
for (Map.Entry<String, Object> entry : environment.getArguments().entrySet()) {
parameters.put(entry.getKey(), Collections.singletonList(entry.getValue()));
}
return BUILDER.getPredicate(domainType, (MultiValueMap) parameters, bindings);
}
private static <S, T> Function<S, T> createProjection(Class<T> 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<T> converter = new DtoInstantiatingConverter<>(projectionType,
new DtoMappingContext(), new EntityInstantiators());
return converter::convert;
}
private static Class<?> getRepositoryInterface(Object executor) {
Assert.isInstanceOf(Repository.class, 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 <T> domain type
* @param <R> result type
*/
public static class Builder<T, R> {
private final QuerydslPredicateExecutor<T> executor;
private final ClassTypeInformation<T> domainType;
private final QuerydslBinderCustomizer<? extends EntityPath<T>> customizer;
private final Function<T, R> resultConverter;
Builder(QuerydslPredicateExecutor<T> executor, ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
Function<T, R> 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 Builder} instance with all previously
* configured options and {@code projectionType} applied
*/
public <P> Builder<T, P> projectAs(Class<P> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null");
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 Builder} instance with all previously configured
* options and {@code QuerydslBinderCustomizer} applied
*/
public Builder<T, R> customizer(QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null");
return new Builder<>(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<R> single() {
return new SingleEntityFetcher<>(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<Iterable<R>> many() {
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.
* @param <T> domain type
* @param <R> result type
*/
public static class ReactiveBuilder<T, R> {
private final ReactiveQuerydslPredicateExecutor<T> executor;
private final ClassTypeInformation<T> domainType;
private final QuerydslBinderCustomizer<? extends EntityPath<T>> customizer;
private final Function<T, R> resultConverter;
ReactiveBuilder(ReactiveQuerydslPredicateExecutor<T> executor,
ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
Function<T, R> 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 Builder} instance with all previously
* configured options and {@code projectionType} applied
*/
public <P> ReactiveBuilder<T, P> projectAs(Class<P> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null");
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 Builder} instance with all previously configured
* options and {@code QuerydslBinderCustomizer} applied
*/
public ReactiveBuilder<T, R> customizer(QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null");
return new ReactiveBuilder<>(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<Mono<R>> single() {
return new ReactiveSingleEntityFetcher<>(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<Flux<R>> many() {
return new ReactiveManyEntityFetcher<>(executor, domainType, customizer, resultConverter);
}
}
static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<R> {
private final QuerydslPredicateExecutor<T> executor;
private final Function<T, R> resultConverter;
@SuppressWarnings({"unchecked", "rawtypes"})
SingleEntityFetcher(QuerydslPredicateExecutor<T> executor,
ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
Function<T, R> 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 ManyEntityFetcher<T, R> extends QuerydslDataFetcher<T>
implements DataFetcher<Iterable<R>> {
private final QuerydslPredicateExecutor<T> executor;
private final Function<T, R> resultConverter;
@SuppressWarnings({"unchecked", "rawtypes"})
ManyEntityFetcher(QuerydslPredicateExecutor<T> executor,
ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
Function<T, R> resultConverter) {
super(domainType, (QuerydslBinderCustomizer) customizer);
this.executor = executor;
this.resultConverter = resultConverter;
}
@Override
public Iterable<R> get(DataFetchingEnvironment environment) {
return Streamable.of(executor.findAll(buildPredicate(environment)))
.map(resultConverter).toList();
}
}
static class ReactiveSingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Mono<R>> {
private final ReactiveQuerydslPredicateExecutor<T> executor;
private final Function<T, R> resultConverter;
@SuppressWarnings({"unchecked", "rawtypes"})
ReactiveSingleEntityFetcher(ReactiveQuerydslPredicateExecutor<T> executor,
ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
Function<T, R> resultConverter) {
super(domainType, (QuerydslBinderCustomizer) customizer);
this.executor = executor;
this.resultConverter = resultConverter;
}
@Override
public Mono<R> get(DataFetchingEnvironment environment) {
return executor.findOne(buildPredicate(environment)).map(resultConverter);
}
}
static class ReactiveManyEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Flux<R>> {
private final ReactiveQuerydslPredicateExecutor<T> executor;
private final Function<T, R> resultConverter;
@SuppressWarnings({"unchecked", "rawtypes"})
ReactiveManyEntityFetcher(ReactiveQuerydslPredicateExecutor<T> executor,
ClassTypeInformation<T> domainType,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
Function<T, R> resultConverter) {
super(domainType, (QuerydslBinderCustomizer) customizer);
this.executor = executor;
this.resultConverter = resultConverter;
}
@Override
public Flux<R> get(DataFetchingEnvironment environment) {
return executor.findAll(buildPredicate(environment)).map(resultConverter);
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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<Book> {
private static final long serialVersionUID = 1773522017L;
public static final QBook book = new QBook("book");
public final StringPath author = this.createString("author");
public final NumberPath<Long> 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<? extends Book> path) {
super(path.getType(), path.getMetadata());
}
public QBook(PathMetadata metadata) {
super(Book.class, metadata);
}
}

View File

@@ -0,0 +1,256 @@
/*
* 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 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;
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 QuerydslDataFetcher}.
*/
class QuerydslDataFetcherTests {
@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", 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 overrides
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", 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"))));
}
@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", QuerydslDataFetcher
.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", QuerydslDataFetcher
.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", QuerydslDataFetcher
.builder(mockRepository)
.customizer((QuerydslBinderCustomizer<QBook>) (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<Predicate> 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")));
}
@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<Book, Long>, QuerydslPredicateExecutor<Book> {
}
interface ReactiveMockRepository extends Repository<Book, Long>, ReactiveQuerydslPredicateExecutor<Book> {
}
static WebGraphQlHandler initWebGraphQlHandler(Consumer<TypeRuntimeWiring.Builder> configurer) {
return WebGraphQlHandler
.builder(new ExecutionGraphQlService(graphQlSource(configurer)))
.build();
}
private static GraphQlSource graphQlSource(Consumer<TypeRuntimeWiring.Builder> 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;
}
}
}

View File

@@ -1,5 +1,6 @@
type Query {
bookById(id: ID): Book
books(id: ID, name: String, author: String): [Book]
}
type Book {