spring-projects / spring-projects/spring-graphql
Spring @Transactional support for Virtual Thread
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.6k
- Forks
- 336
- PR merge metrics
- No merged PRs in 30d
Description
As described in #448, when Spring GraphQL receives JPA entities from a DataFetcher, attempting to touch FetchType.LAZY fields triggers:
LazyInitializationException: Cannot lazily initialize collection of role '???' (no session)
Following the latest documentation(transaction-management), I tried enabling "global transaction" with this configuration:
@Bean
public GraphQlSourceBuilderCustomizer customizer(
FederationSchemaFactory factory,
ObjectProvider<DataFetcherExceptionResolver> resolvers) {
final var exceptionHandler =
DataFetcherExceptionResolver.createExceptionHandler(resolvers.stream().toList());
return schemaBuilder ->
schemaBuilder
.configureGraphQl(
gqlBuilder ->
gqlBuilder
.queryExecutionStrategy(new AsyncSerialExecutionStrategy(exceptionHandler))
.mutationExecutionStrategy(new AsyncSerialExecutionStrategy(exceptionHandler)));
}
@Component
@RequiredArgsConstructor
public static class GraphQLTransactionalInstrumentation extends SimplePerformantInstrumentation {
private final PlatformTransactionManager txManager;
@Override
public @Nullable InstrumentationContext<ExecutionResult> beginExecuteOperation(
InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
final var tx = txManager.getTransaction(null);
return new SimpleInstrumentationContext<>() {
@Override
public void onCompleted(@Nullable ExecutionResult result, @Nullable Throwable t) {
if (t == null && (result == null || result.getErrors().isEmpty())) {
txManager.commit(tx);
} else {
txManager.rollback(tx);
}
}
};
}
}
However, this only works when spring.threads.virtual.enabled = false.
Upon investigation, I found that with virtual threads, AnnotatedControllerConfigurer attempts to configure SchemaMappingDataFetcher/BatchLoaderHandlerMethod with invokeAsync = true. If this succeeds, field resolution always runs in the executor, resulting in a transaction-less context (transactions don't auto-propagate to new threads).
To override this, I tried a BeanPostProcessor to modify shouldInvokeAsync() behavior:
@Component
public static class AnnotatedControllerConfigurerPostProcessor
implements BeanPostProcessor, Ordered {
@Override
public @Nullable Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof AnnotatedControllerConfigurer configurer) {
configurer.setBlockingMethodPredicate(ignored -> false);
}
return bean;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
While this works, is there a more robust way to configure this without tightly coupling to Spring GraphQL's internals?
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the transaction-management documentation and inspect AnnotatedControllerConfigurer, SchemaMappingDataFetcher, and BatchLoaderHandlerMethod where invokeAsync and shouldInvokeAsync are configured. Determine whether a supported configuration point can keep field resolution in the transaction context with virtual threads, then verify that lazy JPA fields work without coupling to internal implementation details.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- graphql, java, spring
- Domain
- api, backend, backend-api-design
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100