-
Notifications
You must be signed in to change notification settings - Fork 328
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?