Add builder customizers for Querydsl and QBE DataFetcher

We now provide Builder customizers to customize DataFetchers
created out of Querydsl and Query by Example repositories
by letting repositories implement customizer interfaces.

See gh-559
This commit is contained in:
Mark Paluch
2022-11-24 15:14:42 +01:00
committed by rstoyanchev
parent 573e89987f
commit ca0d0b46f9
7 changed files with 214 additions and 36 deletions

View File

@@ -967,11 +967,10 @@ obtained from `QuerydslDataFetcher`. The
detects `@GraphQlRepository` beans and uses them to initialize the
`RuntimeWiringConfigurer` with.
Auto-registration does not support <<data-querybyexample-customizations, customizations>>.
If you need that, you'll need to use `QueryByExampleDataFetcher` to build and
register the `DataFetcher` manually through a
<<execution-graphqlsource-runtimewiring-configurer>>.
Auto-registration applies <<data-querybyexample-customizations, customizations>>
by calling `customize(Builder)` on the repository instance if your repository
implements `QuerydslBuilderCustomizer` or `ReactiveQuerydslBuilderCustomizer`
respectively.
[[data-querybyexample]]
@@ -1081,10 +1080,10 @@ obtained from `QueryByExampleDataFetcher`. The
detects `@GraphQlRepository` beans and uses them to initialize the
`RuntimeWiringConfigurer` with.
Auto-registration does not support <<data-querybyexample-customizations, customizations>>.
If you need that, you'll need to use `QueryByExampleDataFetcher` to build and
register the `DataFetcher` manually through a
<<execution-graphqlsource-runtimewiring-configurer>>.
Auto-registration applies <<data-querybyexample-customizations, customizations>>
by calling `customize(Builder)` on the repository instance if your repository
implements `QueryByExampleBuilderCustomizer` or
`ReactiveQueryByExampleBuilderCustomizer` respectively.

View File

@@ -178,14 +178,16 @@ public abstract class QueryByExampleDataFetcher<T> {
for (QueryByExampleExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
Builder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
for (ReactiveQueryByExampleExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
ReactiveBuilder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
@@ -218,14 +220,16 @@ public abstract class QueryByExampleDataFetcher<T> {
for (QueryByExampleExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
Builder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
for (ReactiveQueryByExampleExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
ReactiveBuilder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
@@ -233,6 +237,23 @@ public abstract class QueryByExampleDataFetcher<T> {
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static Builder customize(QueryByExampleExecutor<?> executor, Builder builder) {
if(executor instanceof QueryByExampleBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static ReactiveBuilder customize(ReactiveQueryByExampleExecutor<?> executor, ReactiveBuilder builder) {
if(executor instanceof ReactiveQueryByExampleBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}
/**
* Builder for a Query by Example-based {@link DataFetcher}. Note that builder
* instances are immutable and return a new instance of the builder
@@ -304,6 +325,24 @@ public abstract class QueryByExampleDataFetcher<T> {
}
/**
* Callback interface that can be used to customize QueryByExampleDataFetcher {@link Builder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface QueryByExampleBuilderCustomizer<T> {
/**
* Callback to customize a {@link Builder} instance.
* @param builder builder to customize
*/
Builder<T, ?> customize(Builder<T, ?> builder);
}
/**
* Builder for a reactive Query by Example-based {@link DataFetcher}.
@@ -379,6 +418,24 @@ public abstract class QueryByExampleDataFetcher<T> {
}
/**
* Callback interface that can be used to customize QueryByExampleDataFetcher {@link ReactiveBuilder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface ReactiveQueryByExampleBuilderCustomizer<T> {
/**
* Callback to customize a {@link ReactiveBuilder} instance.
* @param builder builder to customize
*/
ReactiveBuilder<T, ?> customize(ReactiveBuilder<T, ?> builder);
}
private static class SingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<R> {

View File

@@ -205,7 +205,7 @@ public abstract class QuerydslDataFetcher<T> {
for (QuerydslPredicateExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
Builder<?, ?> builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
Builder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
@@ -213,7 +213,7 @@ public abstract class QuerydslDataFetcher<T> {
for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
@@ -253,7 +253,7 @@ public abstract class QuerydslDataFetcher<T> {
for (QuerydslPredicateExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
Builder<?, ?> builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
Builder<?, ?> builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
@@ -261,7 +261,7 @@ public abstract class QuerydslDataFetcher<T> {
for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
@@ -269,6 +269,22 @@ public abstract class QuerydslDataFetcher<T> {
return new AutoRegistrationTypeVisitor(factories);
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static Builder customize(QuerydslPredicateExecutor<?> executor, Builder builder) {
if(executor instanceof QuerydslBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static ReactiveBuilder customize(ReactiveQuerydslPredicateExecutor<?> executor, ReactiveBuilder builder) {
if(executor instanceof ReactiveQuerydslBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}
@SuppressWarnings("rawtypes")
private static QuerydslBinderCustomizer customizer(Object executor) {
return (executor instanceof QuerydslBinderCustomizer<?> ?
@@ -377,6 +393,25 @@ public abstract class QuerydslDataFetcher<T> {
}
/**
* Callback interface that can be used to customize QuerydslDataFetcher {@link Builder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface QuerydslBuilderCustomizer<T> {
/**
* Callback to customize a {@link Builder} instance.
* @param builder builder to customize
*/
Builder<T, ?> customize(Builder<T, ?> builder);
}
/**
* Builder for a reactive Querydsl-based {@link DataFetcher}. Note that builder
* instances are immutable and return a new instance of the builder when
@@ -480,6 +515,25 @@ public abstract class QuerydslDataFetcher<T> {
}
/**
* Callback interface that can be used to customize QuerydslDataFetcher {@link ReactiveBuilder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface ReactiveQuerydslBuilderCustomizer<T> {
/**
* Callback to customize a {@link ReactiveBuilder} instance.
* @param builder builder to customize
*/
ReactiveBuilder<T, ?> customize(ReactiveBuilder<T, ?> builder);
}
private static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<R> {
private final QuerydslPredicateExecutor<T> executor;
@@ -659,5 +713,4 @@ public abstract class QuerydslDataFetcher<T> {
}
}

View File

@@ -47,6 +47,8 @@ import org.springframework.graphql.BookSource;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.query.QuerydslDataFetcher.Builder;
import org.springframework.graphql.data.query.QuerydslDataFetcher.QuerydslBuilderCustomizer;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlHandler;
@@ -213,16 +215,21 @@ class QuerydslDataFetcherTests {
@Test
void shouldFetchSingleItemsWithDtoProjection() {
MockWithBuilderCustomizerRepository mockWithCustomizerRepository = repositoryFactory.getRepository(MockWithBuilderCustomizerRepository.class);
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
mockRepository.save(book);
mockWithCustomizerRepository.save(book);
DataFetcher<?> fetcher = QuerydslDataFetcher.builder(mockRepository).projectAs(BookDto.class).single();
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
Consumer<GraphQlSetup> tester = graphQlSetup -> {
WebGraphQlRequest request = request("{ bookById(id: 42) {name}}");
Mono<WebGraphQlResponse> responseMono = graphQlSetup.toWebGraphQlHandler().handleRequest(request);
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
assertThat(actualBook.getName()).isEqualTo("The book is: " + book.getName());
};
// explicit wiring
tester.accept(initGraphQlSetup(mockWithCustomizerRepository, null));
}
@Test
@@ -275,11 +282,11 @@ class QuerydslDataFetcherTests {
}
static GraphQlSetup graphQlSetup(@Nullable QuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(executor, null);
return initGraphQlSetup(executor, null);
}
static GraphQlSetup graphQlSetup(@Nullable ReactiveQuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(null, executor);
return initGraphQlSetup(null, executor);
}
private static GraphQlSetup initGraphQlSetup(
@@ -303,6 +310,15 @@ class QuerydslDataFetcherTests {
interface MockRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book> {
}
@GraphQlRepository
interface MockWithBuilderCustomizerRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book>, QuerydslBuilderCustomizer<Book> {
@Override
default Builder<Book, ?> customize(Builder<Book, ?> builder) {
return builder.projectAs(BookDto.class);
}
}
@GraphQlRepository
interface MockWithCustomizerRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book>,
@@ -328,6 +344,7 @@ class QuerydslDataFetcherTests {
}
static class BookDto {
private final String name;

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2002-2022 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.query.jpa;
import org.springframework.beans.factory.annotation.Value;
interface BookProjection {
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
String getName();
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2002-2022 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.query.jpa;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher.Builder;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer;
import org.springframework.graphql.data.query.jpa.QueryByExampleDataFetcherJpaTests.BookDto;
@GraphQlRepository
public interface ProjectingBookJpaRepository extends JpaRepository<Book, Long>, QueryByExampleBuilderCustomizer<Book> {
@Override
default Builder<Book, ?> customize(Builder<Book, ?> builder){
return builder.projectAs(BookProjection.class);
}
}

View File

@@ -72,6 +72,9 @@ class QueryByExampleDataFetcherJpaTests {
@Autowired
private BookJpaRepository repository;
@Autowired
private ProjectingBookJpaRepository projectingRepository;
@Test
void shouldFetchSingleItems() {
@@ -147,8 +150,7 @@ class QueryByExampleDataFetcherJpaTests {
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
repository.save(book);
DataFetcher<?> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookProjection.class).single();
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
WebGraphQlHandler handler = graphQlSetup(projectingRepository).toWebGraphQlHandler();
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
@@ -194,14 +196,6 @@ class QueryByExampleDataFetcherJpaTests {
}
interface BookProjection {
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
String getName();
}
static class BookDto {
private final String name;