diff --git a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc index fdc9b408..ae9c97af 100644 --- a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc @@ -312,7 +312,8 @@ In Spring GraphQL you can register a `PreparsedDocumentProvider` through GraphQlSource.Builder builder = ... // Create provider -PreparsedDocumentProvider provider = ... +PreparsedDocumentProvider provider = + new ApolloPersistedQuerySupport(new InMemoryPersistedQueryCache(Collections.emptyMap())); builder.schemaResources(..) .configureRuntimeWiring(..) @@ -323,6 +324,38 @@ The xref:request-execution.adoc#execution.graphqlsource[GraphQlSource section] e + +[[execution.thread-model]] +== Thread Model + +Most GraphQL requests benefit from concurrent execution in fetching nested fields. This is +why most applications today rely on GraphQL Java's `AsyncExecutionStrategy`, which allows +data fetchers to return `CompletionStage` and to execute concurrently rather than serially. + +Java 21 and virtual threads add an important ability to use more threads efficiently, but +it is still necessary to execute concurrently rather than serially in order for request +execution to complete more quickly. + +Spring for GraphQL supports: + +- <>, and those are +adapted to `CompletionStage` as expected by `AsyncExecutionStrategy`. +- `CompletionStage` as return value. +- Controller methods that are Kotlin coroutine methods. +- xref:controllers.adoc#controllers.schema-mapping[@SchemaMapping] and +xref:controllers.adoc#controllers.schema-mapping[@BatchMapping] methods can return +`Callable` that is submitted to an `Executor` such as the Spring Framework +`VirtualThreadTaskExecutor`. To enable this, you must configure an `Executor` on +`AnnotatedControllerConfigurer`. + +Spring for GraphQL runs on either Spring MVC or WebFlux as the transport. Spring MVC +uses async request execution, unless the resulting `CompletableFuture` is done +immediately after the GraphQL Java engine returns, which would be the case if the +request is simple enough and did not require asynchronous data fetching. + + + + [[execution.reactive-datafetcher]] == Reactive `DataFetcher` @@ -342,9 +375,10 @@ xref:request-execution.adoc#execution.context.webflux[WebFlux Context]. == Context Propagation Spring for GraphQL provides support to transparently propagate context from the -xref:transports.adoc#server.transports.http[HTTP], through GraphQL Java, and to `DataFetcher` and other components it -invokes. This includes both `ThreadLocal` context from the Spring MVC request handling -thread and Reactor `Context` from the WebFlux processing pipeline. +xref:transports.adoc#server.transports.http[HTTP] transport, through GraphQL Java, and to +`DataFetcher` and other components it invokes. This includes both `ThreadLocal` context +from the Spring MVC request handling thread and Reactor `Context` from the WebFlux +processing pipeline. [[execution.context.webmvc]] diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/support/SerializableGraphQlRequest.java b/spring-graphql/src/main/java/org/springframework/graphql/server/support/SerializableGraphQlRequest.java index 7f71ea9d..d73fb3bc 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/support/SerializableGraphQlRequest.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/support/SerializableGraphQlRequest.java @@ -17,6 +17,8 @@ package org.springframework.graphql.server.support; import java.util.Map; +import graphql.execution.preparsed.persisted.PersistedQuerySupport; + import org.springframework.graphql.GraphQlRequest; import org.springframework.lang.Nullable; import org.springframework.web.server.ServerWebInputException; @@ -85,6 +87,9 @@ public class SerializableGraphQlRequest implements GraphQlRequest { @Override public String getDocument() { if (this.query == null) { + if (this.extensions != null && this.extensions.get("persistedQuery") != null) { + return PersistedQuerySupport.PERSISTED_QUERY_MARKER; + } throw new ServerWebInputException("No 'query'"); } return this.query; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java index 77221827..e98c4964 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java @@ -22,10 +22,11 @@ import java.util.List; import java.util.Locale; import java.util.UUID; -import jakarta.servlet.ServletException; - import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import graphql.execution.preparsed.persisted.ApolloPersistedQuerySupport; +import graphql.execution.preparsed.persisted.InMemoryPersistedQueryCache; +import jakarta.servlet.ServletException; import org.junit.jupiter.api.Test; import org.springframework.context.i18n.LocaleContextHolder; @@ -109,6 +110,45 @@ public class GraphQlHttpHandlerTests { assertThatNoException().isThrownBy(() -> UUID.fromString(id)); } + @Test + void persistedQuery() throws Exception { + + ApolloPersistedQuerySupport documentProvider = + new ApolloPersistedQuerySupport(new InMemoryPersistedQueryCache(Collections.emptyMap())); + + GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }") + .configureGraphQl(builder -> builder.preparsedDocumentProvider(documentProvider)) + .toHttpHandler(); + + String document = """ + { + "query" : "{__typename}", + "extensions": { + "persistedQuery": { + "version":1, + "sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38" + } + } + }"""; + + MockHttpServletResponse servletResponse = handleRequest(createServletRequest(document, "*/*"), handler); + assertThat(servletResponse.getContentAsString()).isEqualTo("{\"data\":{\"__typename\":\"Query\"}}"); + + document = """ + { + "extensions":{ + "persistedQuery":{ + "version":1, + "sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38" + } + } + }"""; + + servletResponse = handleRequest(createServletRequest(document, "*/*"), handler); + assertThat(servletResponse.getContentAsString()).isEqualTo("{\"data\":{\"__typename\":\"Query\"}}"); + } + + private MockHttpServletRequest createServletRequest(String query, String accept) { MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/"); servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE); diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java index 679130fd..cd329c65 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; +import java.util.function.Consumer; import graphql.GraphQL; import graphql.execution.instrumentation.Instrumentation; @@ -146,6 +147,11 @@ public class GraphQlSetup implements GraphQlServiceSetup { return this; } + public GraphQlSetup configureGraphQl(Consumer configurer) { + this.graphQlSourceBuilder.configureGraphQl(configurer); + return this; + } + public GraphQL toGraphQl() { return this.graphQlSourceBuilder.build().graphQl(); }