Skip to content

Commit f19c603

Browse files
authored
Fix auto-configuration to apply a shared container entity manager instance for GraphQLJPASchemaBuilder (#380)
* Fix intermittent incorrect query results due to Hibernate proxies * make query result list streams and batch loaders thread safe * Apply shared entity manager auto-configuration for GraphqlSchemaBuilder * Refactor transactional execution delegate strategy auto-configuration * revert enable result stream in GraphQLJpaQueryFactory * Refine execution strategy auto-configuration for mulitple datasources * Add ChainedTransactionManager bean to multiple-datasources tests
1 parent ceed90a commit f19c603

File tree

19 files changed

+416
-64
lines changed

19 files changed

+416
-64
lines changed

autoconfigure/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
</description>
1414

1515
<dependencies>
16+
<dependency>
17+
<groupId>org.springframework</groupId>
18+
<artifactId>spring-orm</artifactId>
19+
</dependency>
1620
<dependency>
1721
<groupId>com.introproventures</groupId>
1822
<artifactId>graphql-jpa-query-scalars</artifactId>

autoconfigure/src/main/java/com/introproventures/graphql/jpa/query/autoconfigure/GraphQLJpaQueryAutoConfiguration.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutorContextFactory;
2323
import graphql.GraphQL;
2424
import graphql.GraphQLContext;
25-
import graphql.execution.ExecutionStrategy;
2625
import graphql.execution.instrumentation.Instrumentation;
2726
import graphql.schema.GraphQLSchema;
2827
import graphql.schema.visibility.GraphqlFieldVisibility;
@@ -60,9 +59,9 @@ public GraphQLExecutorContextFactory graphQLJpaExecutorContextFactory(
6059
ObjectProvider<Supplier<GraphqlFieldVisibility>> graphqlFieldVisibility,
6160
ObjectProvider<Supplier<Instrumentation>> instrumentation,
6261
ObjectProvider<Supplier<GraphQLContext>> graphqlContext,
63-
ObjectProvider<Supplier<ExecutionStrategy>> queryExecutionStrategy,
64-
ObjectProvider<Supplier<ExecutionStrategy>> mutationExecutionStrategy,
65-
ObjectProvider<Supplier<ExecutionStrategy>> subscriptionExecutionStrategy
62+
ObjectProvider<QueryExecutionStrategyProvider> queryExecutionStrategy,
63+
ObjectProvider<MutationExecutionStrategyProvider> mutationExecutionStrategy,
64+
ObjectProvider<SubscriptionExecutionStrategyProvider> subscriptionExecutionStrategy
6665
) {
6766
GraphQLJpaExecutorContextFactory bean = new GraphQLJpaExecutorContextFactory();
6867

autoconfigure/src/main/java/com/introproventures/graphql/jpa/query/autoconfigure/GraphQLJpaQueryGraphQlSourceAutoConfiguration.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import graphql.execution.instrumentation.Instrumentation;
2020
import graphql.schema.GraphQLSchema;
2121
import java.util.function.Consumer;
22-
import java.util.stream.Collectors;
2322
import org.springframework.beans.factory.ListableBeanFactory;
2423
import org.springframework.beans.factory.ObjectProvider;
2524
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -45,6 +44,28 @@
4544
@EnableConfigurationProperties(GraphQlProperties.class)
4645
public class GraphQLJpaQueryGraphQlSourceAutoConfiguration {
4746

47+
@Bean
48+
@ConditionalOnBean(GraphQLSchema.class)
49+
Consumer<GraphQL.Builder> graphQlExecutionStrategyConfigurer(
50+
ObjectProvider<QueryExecutionStrategyProvider> queryExecutionStrategy,
51+
ObjectProvider<MutationExecutionStrategyProvider> mutationExecutionStrategy,
52+
ObjectProvider<SubscriptionExecutionStrategyProvider> subscriptionExecutionStrategy
53+
) {
54+
return builder -> {
55+
queryExecutionStrategy.ifAvailable(it -> builder.queryExecutionStrategy(it.get()));
56+
mutationExecutionStrategy.ifAvailable(it -> builder.mutationExecutionStrategy(it.get()));
57+
subscriptionExecutionStrategy.ifAvailable(it -> builder.subscriptionExecutionStrategy(it.get()));
58+
};
59+
}
60+
61+
@Bean
62+
@ConditionalOnGraphQlSchema
63+
GraphQlSourceBuilderCustomizer graphQlSourceBuilderExecutionStrategyCustomizer(
64+
Consumer<GraphQL.Builder> graphQlExecutionStrategyConfigurer
65+
) {
66+
return builder -> builder.configureGraphQl(graphQlExecutionStrategyConfigurer);
67+
}
68+
4869
@Bean
4970
@ConditionalOnGraphQlSchema
5071
GraphQLSchemaConfigurer graphQlSourceSchemaConfigurer(
@@ -86,9 +107,9 @@ public GraphQlSource graphQlSource(
86107
GraphQlSource.Builder<?> builder = GraphQlSource.builder(graphQLSchema);
87108

88109
builder
89-
.exceptionResolvers(exceptionResolvers.orderedStream().collect(Collectors.toList()))
90-
.subscriptionExceptionResolvers(subscriptionExceptionResolvers.orderedStream().collect(Collectors.toList()))
91-
.instrumentation(instrumentations.orderedStream().collect(Collectors.toList()));
110+
.exceptionResolvers(exceptionResolvers.orderedStream().toList())
111+
.subscriptionExceptionResolvers(subscriptionExceptionResolvers.orderedStream().toList())
112+
.instrumentation(instrumentations.orderedStream().toList());
92113

93114
configurers.orderedStream().forEach(builder::configureGraphQl);
94115

autoconfigure/src/main/java/com/introproventures/graphql/jpa/query/autoconfigure/GraphQLSchemaBuilderAutoConfiguration.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.introproventures.graphql.jpa.query.autoconfigure;
22

3+
import static com.introproventures.graphql.jpa.query.autoconfigure.TransactionalDelegateExecutionStrategy.Builder.newTransactionalExecutionStrategy;
4+
35
import com.introproventures.graphql.jpa.query.schema.GraphQLSchemaBuilder;
46
import com.introproventures.graphql.jpa.query.schema.RestrictedKeysProvider;
57
import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder;
68
import graphql.GraphQL;
9+
import jakarta.persistence.EntityManager;
710
import jakarta.persistence.EntityManagerFactory;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
813
import org.springframework.beans.factory.ObjectProvider;
914
import org.springframework.boot.autoconfigure.AutoConfiguration;
1015
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -14,6 +19,10 @@
1419
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
1520
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1621
import org.springframework.context.annotation.Bean;
22+
import org.springframework.orm.jpa.SharedEntityManagerCreator;
23+
import org.springframework.transaction.PlatformTransactionManager;
24+
import org.springframework.transaction.TransactionDefinition;
25+
import org.springframework.transaction.support.TransactionTemplate;
1726

1827
@AutoConfiguration(
1928
before = { GraphQLSchemaAutoConfiguration.class, GraphQLJpaQueryGraphQlSourceAutoConfiguration.class },
@@ -24,15 +33,69 @@
2433
@ConditionalOnProperty(name = "spring.graphql.jpa.query.enabled", havingValue = "true", matchIfMissing = true)
2534
public class GraphQLSchemaBuilderAutoConfiguration {
2635

36+
private static final Logger log = LoggerFactory.getLogger(GraphQLSchemaBuilderAutoConfiguration.class);
37+
38+
@Bean
39+
@ConditionalOnMissingBean(GraphQLSchemaTransactionTemplate.class)
40+
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
41+
GraphQLSchemaTransactionTemplate graphQLSchemaTransactionTemplate(PlatformTransactionManager transactionManager) {
42+
return () -> new TransactionTemplate(transactionManager);
43+
}
44+
45+
@Bean
46+
@ConditionalOnMissingBean(QueryExecutionStrategyProvider.class)
47+
@ConditionalOnSingleCandidate(GraphQLSchemaTransactionTemplate.class)
48+
QueryExecutionStrategyProvider queryExecutionStrategy(
49+
GraphQLSchemaTransactionTemplate graphQLSchemaTransactionTemplate
50+
) {
51+
var transactionTemplate = graphQLSchemaTransactionTemplate.get();
52+
transactionTemplate.setReadOnly(true);
53+
54+
return () -> newTransactionalExecutionStrategy(transactionTemplate).build();
55+
}
56+
57+
@Bean
58+
@ConditionalOnMissingBean(MutationExecutionStrategyProvider.class)
59+
@ConditionalOnSingleCandidate(GraphQLSchemaTransactionTemplate.class)
60+
MutationExecutionStrategyProvider mutationExecutionStrategy(
61+
GraphQLSchemaTransactionTemplate graphQLSchemaTransactionTemplate
62+
) {
63+
var transactionTemplate = graphQLSchemaTransactionTemplate.get();
64+
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
65+
66+
return () -> newTransactionalExecutionStrategy(transactionTemplate).build();
67+
}
68+
69+
@Bean
70+
@ConditionalOnMissingBean(SubscriptionExecutionStrategyProvider.class)
71+
@ConditionalOnSingleCandidate(GraphQLSchemaTransactionTemplate.class)
72+
SubscriptionExecutionStrategyProvider subscriptionExecutionStrategy(
73+
GraphQLSchemaTransactionTemplate graphQLSchemaTransactionTemplate
74+
) {
75+
var transactionTemplate = graphQLSchemaTransactionTemplate.get();
76+
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
77+
78+
return () -> newTransactionalExecutionStrategy(transactionTemplate).build();
79+
}
80+
81+
@Bean
82+
@ConditionalOnMissingBean(GraphQLSchemaEntityManager.class)
83+
@ConditionalOnSingleCandidate(EntityManagerFactory.class)
84+
GraphQLSchemaEntityManager graphQLSchemaEntityManager(EntityManagerFactory entityManagerFactory) {
85+
return () -> SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory);
86+
}
87+
2788
@Bean
2889
@ConditionalOnMissingBean
2990
@ConditionalOnSingleCandidate(EntityManagerFactory.class)
3091
GraphQLJpaSchemaBuilder defaultGraphQLJpaSchemaBuilder(
31-
EntityManagerFactory entityManagerFactory,
92+
GraphQLSchemaEntityManager graphQLSchemaEntityManager,
3293
GraphQLJpaQueryProperties properties,
3394
ObjectProvider<RestrictedKeysProvider> restrictedKeysProvider
3495
) {
35-
GraphQLJpaSchemaBuilder builder = new GraphQLJpaSchemaBuilder(entityManagerFactory.createEntityManager());
96+
final EntityManager entityManager = graphQLSchemaEntityManager.get();
97+
98+
GraphQLJpaSchemaBuilder builder = new GraphQLJpaSchemaBuilder(entityManager);
3699

37100
builder
38101
.name(properties.getName())
@@ -46,6 +109,8 @@ GraphQLJpaSchemaBuilder defaultGraphQLJpaSchemaBuilder(
46109

47110
restrictedKeysProvider.ifAvailable(builder::restrictedKeysProvider);
48111

112+
log.warn("Configured {} for {} GraphQL schema", entityManager, properties.getName());
113+
49114
return builder;
50115
}
51116

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.introproventures.graphql.jpa.query.autoconfigure;
2+
3+
import jakarta.persistence.EntityManager;
4+
import java.util.function.Supplier;
5+
6+
public interface GraphQLSchemaEntityManager extends Supplier<EntityManager> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.introproventures.graphql.jpa.query.autoconfigure;
2+
3+
import java.util.function.Supplier;
4+
import org.springframework.transaction.support.TransactionTemplate;
5+
6+
public interface GraphQLSchemaTransactionTemplate extends Supplier<TransactionTemplate> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.introproventures.graphql.jpa.query.autoconfigure;
2+
3+
import graphql.execution.ExecutionStrategy;
4+
import java.util.function.Supplier;
5+
6+
public interface MutationExecutionStrategyProvider extends Supplier<ExecutionStrategy> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.introproventures.graphql.jpa.query.autoconfigure;
2+
3+
import graphql.execution.ExecutionStrategy;
4+
import java.util.function.Supplier;
5+
6+
public interface QueryExecutionStrategyProvider extends Supplier<ExecutionStrategy> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.introproventures.graphql.jpa.query.autoconfigure;
2+
3+
import graphql.execution.ExecutionStrategy;
4+
import java.util.function.Supplier;
5+
6+
public interface SubscriptionExecutionStrategyProvider extends Supplier<ExecutionStrategy> {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.introproventures.graphql.jpa.query.autoconfigure;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.execution.AsyncExecutionStrategy;
5+
import graphql.execution.ExecutionContext;
6+
import graphql.execution.ExecutionStrategy;
7+
import graphql.execution.ExecutionStrategyParameters;
8+
import graphql.execution.NonNullableFieldWasNullException;
9+
import java.util.concurrent.CompletableFuture;
10+
import java.util.concurrent.Executor;
11+
import java.util.concurrent.Executors;
12+
import java.util.function.Supplier;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import org.springframework.transaction.support.TransactionSynchronizationManager;
16+
import org.springframework.transaction.support.TransactionTemplate;
17+
18+
public class TransactionalDelegateExecutionStrategy extends ExecutionStrategy {
19+
20+
private static final Logger log = LoggerFactory.getLogger(TransactionalDelegateExecutionStrategy.class);
21+
22+
private final TransactionTemplate transactionTemplate;
23+
24+
private final ExecutionStrategy delegate;
25+
26+
private final Supplier<Executor> executor;
27+
28+
public TransactionalDelegateExecutionStrategy(
29+
TransactionTemplate transactionTemplate,
30+
ExecutionStrategy delegate,
31+
Supplier<Executor> executor
32+
) {
33+
this.transactionTemplate = transactionTemplate;
34+
this.delegate = delegate;
35+
this.executor = executor;
36+
}
37+
38+
@Override
39+
public CompletableFuture<ExecutionResult> execute(
40+
ExecutionContext executionContext,
41+
ExecutionStrategyParameters parameters
42+
) throws NonNullableFieldWasNullException {
43+
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
44+
if (log.isTraceEnabled()) {
45+
log.trace(
46+
"Start root execution request {} running on {}",
47+
executionContext.getExecutionId(),
48+
Thread.currentThread()
49+
);
50+
}
51+
return CompletableFuture.supplyAsync(
52+
() -> {
53+
return transactionTemplate.execute(status -> {
54+
if (log.isTraceEnabled()) {
55+
log.trace(
56+
"Begin transaction for {} on {}",
57+
executionContext.getExecutionId(),
58+
Thread.currentThread()
59+
);
60+
}
61+
try {
62+
if (log.isTraceEnabled()) {
63+
log.trace(
64+
"Execute request for {} on {}",
65+
executionContext.getExecutionId(),
66+
Thread.currentThread()
67+
);
68+
}
69+
70+
return delegate.execute(executionContext, parameters).join();
71+
} finally {
72+
if (log.isTraceEnabled()) {
73+
log.trace(
74+
"End transaction for {} on {}",
75+
executionContext.getExecutionId(),
76+
Thread.currentThread()
77+
);
78+
}
79+
}
80+
});
81+
},
82+
executor.get()
83+
);
84+
} else {
85+
if (log.isTraceEnabled()) {
86+
log.trace(
87+
"Execute request {} for {} on {}",
88+
executionContext.getExecutionId(),
89+
parameters.getField().getName(),
90+
Thread.currentThread()
91+
);
92+
}
93+
return delegate.execute(executionContext, parameters);
94+
}
95+
}
96+
97+
public static final class Builder {
98+
99+
private TransactionTemplate transactionTemplate;
100+
private Supplier<Executor> executor = Executors::newCachedThreadPool;
101+
private ExecutionStrategy delegate = new AsyncExecutionStrategy();
102+
103+
private Builder() {}
104+
105+
public static Builder newTransactionalExecutionStrategy(TransactionTemplate transactionTemplate) {
106+
return new Builder().transactionTemplate(transactionTemplate);
107+
}
108+
109+
public Builder transactionTemplate(TransactionTemplate transactionTemplate) {
110+
this.transactionTemplate = transactionTemplate;
111+
return this;
112+
}
113+
114+
public Builder delegate(ExecutionStrategy delegate) {
115+
this.delegate = delegate;
116+
return this;
117+
}
118+
119+
public Builder executor(Supplier<Executor> executor) {
120+
this.executor = executor;
121+
return this;
122+
}
123+
124+
public TransactionalDelegateExecutionStrategy build() {
125+
return new TransactionalDelegateExecutionStrategy(transactionTemplate, delegate, executor);
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)