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:
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user