Skip to content

Commit 9da39bb

Browse files
committed
Introduce DataFetcher builder customizers for Querydsl and Query by Example repositories
We now provide Builder customizers to customize DataFetchers created out of Querydsl and Query by Example repositories by letting repositories implement customizer interfaces.
1 parent 2f27cc7 commit 9da39bb

File tree

7 files changed

+210
-36
lines changed

7 files changed

+210
-36
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -967,11 +967,10 @@ obtained from `QuerydslDataFetcher`. The
967967
detects `@GraphQlRepository` beans and uses them to initialize the
968968
`RuntimeWiringConfigurer` with.
969969

970-
Auto-registration does not support <<data-querybyexample-customizations, customizations>>.
971-
If you need that, you'll need to use `QueryByExampleDataFetcher` to build and
972-
register the `DataFetcher` manually through a
973-
<<execution-graphqlsource-runtimewiring-configurer>>.
974-
970+
Auto-registration applies <<data-querybyexample-customizations, customizations>>
971+
by calling `customize(Builder)` on the repository instance if your repository
972+
implements `QuerydslBuilderCustomizer` respectively
973+
`ReactiveQuerydslBuilderCustomizer`.
975974

976975

977976
[[data-querybyexample]]
@@ -1081,10 +1080,10 @@ obtained from `QueryByExampleDataFetcher`. The
10811080
detects `@GraphQlRepository` beans and uses them to initialize the
10821081
`RuntimeWiringConfigurer` with.
10831082

1084-
Auto-registration does not support <<data-querybyexample-customizations, customizations>>.
1085-
If you need that, you'll need to use `QueryByExampleDataFetcher` to build and
1086-
register the `DataFetcher` manually through a
1087-
<<execution-graphqlsource-runtimewiring-configurer>>.
1083+
Auto-registration applies <<data-querybyexample-customizations, customizations>>
1084+
by calling `customize(Builder)` on the repository instance if your repository
1085+
implements `QueryByExampleBuilderCustomizer` respectively
1086+
`ReactiveQueryByExampleBuilderCustomizer`.
10881087

10891088

10901089

spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,16 @@ public static RuntimeWiringConfigurer autoRegistrationConfigurer(
178178
for (QueryByExampleExecutor<?> executor : executors) {
179179
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
180180
if (typeName != null) {
181-
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
181+
Builder<?, ?> builder = customize(executor, builder(executor));
182+
factories.put(typeName, single -> single ? builder.single() : builder.many());
182183
}
183184
}
184185

185186
for (ReactiveQueryByExampleExecutor<?> executor : reactiveExecutors) {
186187
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
187188
if (typeName != null) {
188-
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
189+
ReactiveBuilder<?, ?> builder = customize(executor, builder(executor));
190+
factories.put(typeName, single -> single ? builder.single() : builder.many());
189191
}
190192
}
191193

@@ -218,21 +220,40 @@ public static GraphQLTypeVisitor autoRegistrationTypeVisitor(
218220
for (QueryByExampleExecutor<?> executor : executors) {
219221
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
220222
if (typeName != null) {
221-
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
223+
Builder<?, ?> builder = customize(executor, builder(executor));
224+
factories.put(typeName, single -> single ? builder.single() : builder.many());
222225
}
223226
}
224227

225228
for (ReactiveQueryByExampleExecutor<?> executor : reactiveExecutors) {
226229
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
227230
if (typeName != null) {
228-
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
231+
ReactiveBuilder<?, ?> builder = customize(executor, builder(executor));
232+
factories.put(typeName, single -> single ? builder.single() : builder.many());
229233
}
230234
}
231235

232236
return new AutoRegistrationTypeVisitor(factories);
233237
}
234238

235239

240+
@SuppressWarnings({"unchecked", "rawtypes"})
241+
private static Builder customize(QueryByExampleExecutor<?> executor, Builder builder) {
242+
if(executor instanceof QueryByExampleBuilderCustomizer<?> customizer){
243+
return customizer.customize(builder);
244+
}
245+
return builder;
246+
}
247+
248+
@SuppressWarnings({"unchecked", "rawtypes"})
249+
private static ReactiveBuilder customize(ReactiveQueryByExampleExecutor<?> executor, ReactiveBuilder builder) {
250+
if(executor instanceof ReactiveQueryByExampleBuilderCustomizer<?> customizer){
251+
return customizer.customize(builder);
252+
}
253+
return builder;
254+
}
255+
256+
236257
/**
237258
* Builder for a Query by Example-based {@link DataFetcher}. Note that builder
238259
* instances are immutable and return a new instance of the builder
@@ -304,6 +325,23 @@ public DataFetcher<Iterable<R>> many() {
304325

305326
}
306327

328+
/**
329+
* Callback interface that can be used to customize QueryByExampleDataFetcher {@link Builder} to
330+
* change its configuration.
331+
*
332+
* @param <T>
333+
* @since 1.2.0
334+
*/
335+
public interface QueryByExampleBuilderCustomizer<T> {
336+
337+
/**
338+
* Callback to customize a {@link Builder} instance.
339+
* @param builder builder to customize
340+
*/
341+
Builder<T, ?> customize(Builder<T, ?> builder);
342+
343+
}
344+
307345

308346
/**
309347
* Builder for a reactive Query by Example-based {@link DataFetcher}.
@@ -379,6 +417,23 @@ public DataFetcher<Flux<R>> many() {
379417

380418
}
381419

420+
/**
421+
* Callback interface that can be used to customize QueryByExampleDataFetcher {@link ReactiveBuilder} to
422+
* change its configuration.
423+
*
424+
* @param <T>
425+
* @since 1.2.0
426+
*/
427+
public interface ReactiveQueryByExampleBuilderCustomizer<T> {
428+
429+
/**
430+
* Callback to customize a {@link ReactiveBuilder} instance.
431+
* @param builder builder to customize
432+
*/
433+
ReactiveBuilder<T, ?> customize(ReactiveBuilder<T, ?> builder);
434+
435+
}
436+
382437

383438
private static class SingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<R> {
384439

spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,15 @@ public static RuntimeWiringConfigurer autoRegistrationConfigurer(
205205
for (QuerydslPredicateExecutor<?> executor : executors) {
206206
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
207207
if (typeName != null) {
208-
Builder<?, ?> builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
208+
Builder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
209209
factories.put(typeName, single -> single ? builder.single() : builder.many());
210210
}
211211
}
212212

213213
for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
214214
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
215215
if (typeName != null) {
216-
ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
216+
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
217217
factories.put(typeName, single -> single ? builder.single() : builder.many());
218218
}
219219
}
@@ -253,22 +253,38 @@ public static GraphQLTypeVisitor autoRegistrationTypeVisitor(
253253
for (QuerydslPredicateExecutor<?> executor : executors) {
254254
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
255255
if (typeName != null) {
256-
Builder<?, ?> builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
256+
Builder<?, ?> builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
257257
factories.put(typeName, single -> single ? builder.single() : builder.many());
258258
}
259259
}
260260

261261
for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
262262
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
263263
if (typeName != null) {
264-
ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
264+
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
265265
factories.put(typeName, single -> single ? builder.single() : builder.many());
266266
}
267267
}
268268

269269
return new AutoRegistrationTypeVisitor(factories);
270270
}
271271

272+
@SuppressWarnings({"unchecked", "rawtypes"})
273+
private static Builder customize(QuerydslPredicateExecutor<?> executor, Builder builder) {
274+
if(executor instanceof QuerydslBuilderCustomizer<?> customizer){
275+
return customizer.customize(builder);
276+
}
277+
return builder;
278+
}
279+
280+
@SuppressWarnings({"unchecked", "rawtypes"})
281+
private static ReactiveBuilder customize(ReactiveQuerydslPredicateExecutor<?> executor, ReactiveBuilder builder) {
282+
if(executor instanceof ReactiveQuerydslBuilderCustomizer<?> customizer){
283+
return customizer.customize(builder);
284+
}
285+
return builder;
286+
}
287+
272288
@SuppressWarnings("rawtypes")
273289
private static QuerydslBinderCustomizer customizer(Object executor) {
274290
return (executor instanceof QuerydslBinderCustomizer<?> ?
@@ -377,6 +393,24 @@ public DataFetcher<Iterable<R>> many() {
377393
}
378394

379395

396+
/**
397+
* Callback interface that can be used to customize QuerydslDataFetcher {@link Builder} to
398+
* change its configuration.
399+
*
400+
* @param <T>
401+
* @since 1.2.0
402+
*/
403+
public interface QuerydslBuilderCustomizer<T> {
404+
405+
/**
406+
* Callback to customize a {@link Builder} instance.
407+
* @param builder builder to customize
408+
*/
409+
Builder<T, ?> customize(Builder<T, ?> builder);
410+
411+
}
412+
413+
380414
/**
381415
* Builder for a reactive Querydsl-based {@link DataFetcher}. Note that builder
382416
* instances are immutable and return a new instance of the builder when
@@ -480,6 +514,24 @@ public DataFetcher<Flux<R>> many() {
480514
}
481515

482516

517+
/**
518+
* Callback interface that can be used to customize QuerydslDataFetcher {@link ReactiveBuilder} to
519+
* change its configuration.
520+
*
521+
* @param <T>
522+
* @since 1.2.0
523+
*/
524+
public interface ReactiveQuerydslBuilderCustomizer<T> {
525+
526+
/**
527+
* Callback to customize a {@link ReactiveBuilder} instance.
528+
* @param builder builder to customize
529+
*/
530+
ReactiveBuilder<T, ?> customize(ReactiveBuilder<T, ?> builder);
531+
532+
}
533+
534+
483535
private static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<R> {
484536

485537
private final QuerydslPredicateExecutor<T> executor;
@@ -659,5 +711,4 @@ public Flux<R> get(DataFetchingEnvironment env) {
659711

660712
}
661713

662-
663714
}

spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
import org.springframework.graphql.GraphQlSetup;
4848
import org.springframework.graphql.ResponseHelper;
4949
import org.springframework.graphql.data.GraphQlRepository;
50+
import org.springframework.graphql.data.query.QuerydslDataFetcher.Builder;
51+
import org.springframework.graphql.data.query.QuerydslDataFetcher.QuerydslBuilderCustomizer;
5052
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
5153
import org.springframework.graphql.server.WebGraphQlRequest;
5254
import org.springframework.graphql.server.WebGraphQlHandler;
@@ -213,16 +215,21 @@ void shouldFetchSingleItemsWithInterfaceProjection() {
213215

214216
@Test
215217
void shouldFetchSingleItemsWithDtoProjection() {
218+
MockWithBuilderCustomizerRepository mockWithCustomizerRepository = repositoryFactory.getRepository(MockWithBuilderCustomizerRepository.class);
216219
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
217-
mockRepository.save(book);
220+
mockWithCustomizerRepository.save(book);
218221

219-
DataFetcher<?> fetcher = QuerydslDataFetcher.builder(mockRepository).projectAs(BookDto.class).single();
220-
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
222+
Consumer<GraphQlSetup> tester = graphQlSetup -> {
223+
WebGraphQlRequest request = request("{ bookById(id: 42) {name}}");
224+
Mono<WebGraphQlResponse> responseMono = graphQlSetup.toWebGraphQlHandler().handleRequest(request);
221225

222-
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
226+
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
223227

224-
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
225-
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
228+
assertThat(actualBook.getName()).isEqualTo("The book is: " + book.getName());
229+
};
230+
231+
// explicit wiring
232+
tester.accept(initGraphQlSetup(mockWithCustomizerRepository, null));
226233
}
227234

228235
@Test
@@ -275,11 +282,11 @@ static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
275282
}
276283

277284
static GraphQlSetup graphQlSetup(@Nullable QuerydslPredicateExecutor<?> executor) {
278-
return initGraphQlSetup(executor, null);
285+
return initGraphQlSetup(executor, null);
279286
}
280287

281288
static GraphQlSetup graphQlSetup(@Nullable ReactiveQuerydslPredicateExecutor<?> executor) {
282-
return initGraphQlSetup(null, executor);
289+
return initGraphQlSetup(null, executor);
283290
}
284291

285292
private static GraphQlSetup initGraphQlSetup(
@@ -303,6 +310,15 @@ private WebGraphQlRequest request(String query) {
303310
interface MockRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book> {
304311
}
305312

313+
@GraphQlRepository
314+
interface MockWithBuilderCustomizerRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book>, QuerydslBuilderCustomizer<Book> {
315+
316+
@Override
317+
default Builder<Book, ?> customize(Builder<Book, ?> builder) {
318+
return builder.projectAs(BookDto.class);
319+
}
320+
}
321+
306322

307323
@GraphQlRepository
308324
interface MockWithCustomizerRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book>,
@@ -328,6 +344,7 @@ interface BookProjection {
328344

329345
}
330346

347+
331348
static class BookDto {
332349

333350
private final String name;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.query.jpa;
18+
19+
import org.springframework.beans.factory.annotation.Value;
20+
21+
interface BookProjection {
22+
23+
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
24+
String getName();
25+
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.data.query.jpa;
18+
19+
import org.springframework.data.jpa.repository.JpaRepository;
20+
import org.springframework.graphql.data.GraphQlRepository;
21+
import org.springframework.graphql.data.query.QueryByExampleDataFetcher.Builder;
22+
import org.springframework.graphql.data.query.QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer;
23+
import org.springframework.graphql.data.query.jpa.QueryByExampleDataFetcherJpaTests.BookDto;
24+
25+
@GraphQlRepository
26+
public interface ProjectingBookJpaRepository extends JpaRepository<Book, Long>, QueryByExampleBuilderCustomizer<Book> {
27+
28+
@Override
29+
default Builder<Book, ?> customize(Builder<Book, ?> builder){
30+
return builder.projectAs(BookProjection.class);
31+
}
32+
}

0 commit comments

Comments
 (0)