Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit cc055bd

Browse files
feat: added option to use a transactional query invoker
This may prevent LazyInitializationException when working with JPA entities across multiple resolvers (thus making individual resolvers Transactional is not sufficient). By default, this feature is disabled. It can be enabled in application.properties/yaml with: graphql.query-invoker.transactional=true
1 parent 1d15482 commit cc055bd

File tree

8 files changed

+193
-3
lines changed

8 files changed

+193
-3
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ graphql:
173173
# if you want to @ExceptionHandler annotation for custom GraphQLErrors
174174
exception-handlers-enabled: true
175175
contextSetting: PER_REQUEST_WITH_INSTRUMENTATION
176+
query-invoker:
177+
# use a transactional query invoker; useful when working with JPA entities across multiple resolvers to
178+
# prevent LazyInitializationException; false by default
179+
transactional: true
176180
```
177181
178182
By default a global CORS filter is enabled for `/graphql/**` context.

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ LIB_SPRING_BOOT_VER = 2.1.6.RELEASE
4444
LIB_GRAPHQL_SERVLET_VER = 8.0.0
4545
LIB_GRAPHQL_JAVA_TOOLS_VER = 5.7.1
4646
LIB_COMMONS_IO_VER = 2.6
47+
LIB_TRANSACTIONS_API_VERSION=1.3
4748
kotlin.version=1.3.31
4849

4950
GRADLE_WRAPPER_VER = 4.10.3

graphql-spring-boot-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323

2424
compile "org.springframework.boot:spring-boot-autoconfigure:$LIB_SPRING_BOOT_VER"
2525
compile "org.springframework.boot:spring-boot-starter-websocket:$LIB_SPRING_BOOT_VER"
26+
compile "javax.transaction:javax.transaction-api:$LIB_TRANSACTIONS_API_VERSION"
2627
compile "org.springframework.boot:spring-boot-starter-actuator:$LIB_SPRING_BOOT_VER"
2728
compile "com.graphql-java-kickstart:graphql-java-servlet:$LIB_GRAPHQL_SERVLET_VER"
2829
compile "com.graphql-java-kickstart:graphql-java-tools:$LIB_GRAPHQL_JAVA_TOOLS_VER"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.oembedler.moon.graphql.boot;
2+
3+
import lombok.Data;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Data
8+
@Configuration
9+
@ConfigurationProperties(prefix = "graphql.query-invoker")
10+
public class GraphQLQueryInvokerProperties {
11+
12+
private boolean transactional;
13+
}

graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLWebAutoConfiguration.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
@Conditional(OnSchemaOrSchemaProviderBean.class)
8787
@ConditionalOnProperty(value = "graphql.servlet.enabled", havingValue = "true", matchIfMissing = true)
8888
@AutoConfigureAfter({GraphQLJavaToolsAutoConfiguration.class, JacksonAutoConfiguration.class})
89-
@EnableConfigurationProperties({GraphQLServletProperties.class})
89+
@EnableConfigurationProperties({GraphQLServletProperties.class, GraphQLQueryInvokerProperties.class})
9090
public class GraphQLWebAutoConfiguration {
9191

9292

@@ -213,7 +213,10 @@ public GraphQLInvocationInputFactory invocationInputFactory(GraphQLSchemaProvide
213213

214214
@Bean
215215
@ConditionalOnMissingBean
216-
public GraphQLQueryInvoker queryInvoker(ExecutionStrategyProvider executionStrategyProvider) {
216+
public GraphQLQueryInvoker queryInvoker(
217+
ExecutionStrategyProvider executionStrategyProvider,
218+
GraphQLQueryInvokerProperties graphQLQueryInvokerProperties
219+
) {
217220
GraphQLQueryInvoker.Builder builder = GraphQLQueryInvoker.newBuilder()
218221
.withExecutionStrategyProvider(executionStrategyProvider);
219222

@@ -228,7 +231,13 @@ public GraphQLQueryInvoker queryInvoker(ExecutionStrategyProvider executionStrat
228231
builder.withPreparsedDocumentProvider(preparsedDocumentProvider);
229232
}
230233

231-
return builder.build();
234+
GraphQLQueryInvoker queryInvoker = builder.build();
235+
236+
if (graphQLQueryInvokerProperties.isTransactional()) {
237+
queryInvoker = new TransactionalGraphQLQueryInvokerWrapper(queryInvoker);
238+
log.info("Using transactional query invoker.");
239+
}
240+
return queryInvoker;
232241
}
233242

234243
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.oembedler.moon.graphql.boot;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.servlet.context.ContextSetting;
5+
import graphql.servlet.core.GraphQLQueryInvoker;
6+
import graphql.servlet.input.GraphQLSingleInvocationInput;
7+
import lombok.Getter;
8+
import org.springframework.lang.NonNull;
9+
10+
import javax.transaction.Transactional;
11+
import java.util.List;
12+
import java.util.Objects;
13+
14+
/**
15+
* This is a transactional wrapper for the default {@link GraphQLQueryInvoker} bean. The primary purpose of this class
16+
* is to prevent LazyInitializationException when working with nested
17+
* {@link com.coxautodev.graphql.tools.GraphQLResolver resolvers} and JPA entities. In these cases making the
18+
* individual resolvers {@link Transactional} may not be sufficient.
19+
*
20+
* Other than making the whole query invocation transactional, this wrapper does not change the behaviour of the
21+
* wrapped invoker, and will simply delegate all queries to it.
22+
*
23+
* To enable the transactional wrapper, set the {@code graphql.query-invoker.transactional} property to {@code true}
24+
* in application.properties/yaml.
25+
*/
26+
@Getter
27+
@Transactional
28+
public class TransactionalGraphQLQueryInvokerWrapper extends GraphQLQueryInvoker {
29+
//GraphQLQueryInvoker should be an interface...
30+
31+
private @NonNull GraphQLQueryInvoker wrappedInvoker;
32+
33+
/**
34+
* Constructor.
35+
* @param wrappedInvoker The wrapped query invoker. Must not be null.
36+
*/
37+
public TransactionalGraphQLQueryInvokerWrapper(final @NonNull GraphQLQueryInvoker wrappedInvoker) {
38+
super(null, null, null, null);
39+
this.wrappedInvoker = Objects.requireNonNull(wrappedInvoker);
40+
}
41+
42+
/**
43+
* {@inheritDoc}
44+
*/
45+
@Override
46+
public ExecutionResult query(final GraphQLSingleInvocationInput singleInvocationInput) {
47+
return wrappedInvoker.query(singleInvocationInput);
48+
}
49+
50+
/**
51+
* {@inheritDoc}
52+
*/
53+
@Override
54+
public List<ExecutionResult> query(final List<GraphQLSingleInvocationInput> batchedInvocationInput,
55+
final ContextSetting contextSetting) {
56+
return wrappedInvoker.query(batchedInvocationInput, contextSetting);
57+
}
58+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.oembedler.moon.graphql.boot;
2+
3+
import graphql.ExecutionResult;
4+
import graphql.servlet.context.ContextSetting;
5+
import graphql.servlet.core.GraphQLQueryInvoker;
6+
import graphql.servlet.input.GraphQLSingleInvocationInput;
7+
import org.junit.Before;
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
import org.mockito.Mock;
11+
import org.mockito.junit.MockitoJUnitRunner;
12+
13+
import javax.transaction.Transactional;
14+
import java.util.Collections;
15+
import java.util.List;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.BDDMockito.given;
19+
import static org.mockito.BDDMockito.then;
20+
import static org.mockito.Mockito.mock;
21+
22+
@RunWith(MockitoJUnitRunner.class)
23+
public class TransactionQueryInvokerWrapperTest {
24+
25+
@Mock
26+
private GraphQLQueryInvoker wrappedInvoker;
27+
28+
private TransactionalGraphQLQueryInvokerWrapper wrapper;
29+
30+
@Before
31+
public void setUp() {
32+
wrapper = new TransactionalGraphQLQueryInvokerWrapper(wrappedInvoker);
33+
}
34+
35+
@Test
36+
public void shouldHaveTransactionalAnnotation() {
37+
assertThat(TransactionalGraphQLQueryInvokerWrapper.class.getAnnotation(Transactional.class)).isNotNull();
38+
}
39+
40+
@Test
41+
public void shouldWrapExistingQueryInvokerWithSingleQuery() {
42+
//GIVEN
43+
final GraphQLSingleInvocationInput invocationInput = mock(GraphQLSingleInvocationInput.class);
44+
final ExecutionResult expectedExecutionResult = mock(ExecutionResult.class);
45+
given(wrappedInvoker.query(invocationInput)).willReturn(expectedExecutionResult);
46+
//WHEN
47+
final ExecutionResult actualExecutionResult = wrapper.query(invocationInput);
48+
//THEN
49+
then(wrappedInvoker).should().query(invocationInput);
50+
then(wrappedInvoker).shouldHaveNoMoreInteractions();
51+
assertThat(actualExecutionResult)
52+
.as("Should call the wrapped invoker, and return the execution result returned by it.")
53+
.isEqualTo(expectedExecutionResult);
54+
}
55+
56+
@Test
57+
public void shouldWrapExistingQueryInvokerWithBatchedQuery() {
58+
//GIVEN
59+
final GraphQLSingleInvocationInput invocationInput = mock(GraphQLSingleInvocationInput.class);
60+
final List<GraphQLSingleInvocationInput> invocationInputList = Collections.singletonList(invocationInput);
61+
final ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION;
62+
final ExecutionResult expectedExecutionResult = mock(ExecutionResult.class);
63+
final List<ExecutionResult> expectedExecutionResultList = Collections.singletonList(expectedExecutionResult);
64+
given(wrappedInvoker.query(invocationInputList, contextSetting)).willReturn(expectedExecutionResultList);
65+
//WHEN
66+
final List<ExecutionResult> actualExecutionResultList = wrapper.query(invocationInputList, contextSetting);
67+
//THEN
68+
then(wrappedInvoker).should().query(invocationInputList, contextSetting);
69+
then(wrappedInvoker).shouldHaveNoMoreInteractions();
70+
assertThat(actualExecutionResultList)
71+
.as("Should call the wrapped invoker, and return the execution result list returned by it.")
72+
.isEqualTo(expectedExecutionResultList);
73+
}
74+
}

graphql-spring-boot-autoconfigure/src/test/java/com/oembedler/moon/graphql/boot/test/web/GraphQLWebAutoConfigurationTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.oembedler.moon.graphql.boot.test.web;
22

33
import com.oembedler.moon.graphql.boot.GraphQLWebAutoConfiguration;
4+
import com.oembedler.moon.graphql.boot.TransactionalGraphQLQueryInvokerWrapper;
45
import com.oembedler.moon.graphql.boot.test.AbstractAutoConfigurationTest;
56
import graphql.analysis.MaxQueryComplexityInstrumentation;
67
import graphql.analysis.MaxQueryDepthInstrumentation;
@@ -12,12 +13,15 @@
1213
import graphql.servlet.AbstractGraphQLHttpServlet;
1314
import graphql.servlet.config.DefaultGraphQLSchemaProvider;
1415
import graphql.servlet.config.GraphQLSchemaProvider;
16+
import graphql.servlet.core.GraphQLQueryInvoker;
1517
import org.junit.Assert;
1618
import org.junit.Test;
1719
import org.springframework.context.annotation.Bean;
1820
import org.springframework.context.annotation.Configuration;
1921
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
2022

23+
import static org.assertj.core.api.Assertions.assertThat;
24+
2125
/**
2226
* @author <a href="mailto:java.lang.RuntimeException@gmail.com">oEmbedler Inc.</a>
2327
*/
@@ -162,4 +166,30 @@ public void appContextLoadsWithCustomSchemaProvider() {
162166

163167
Assert.assertNotNull(this.getContext().getBean(AbstractGraphQLHttpServlet.class));
164168
}
169+
170+
@Test
171+
public void queryInvokerShouldNotBeTransactionalByDefault() {
172+
load(SimpleConfiguration.class);
173+
assertThatQueryInvokerIsNotTransactional();
174+
}
175+
176+
@Test
177+
public void queryInvokerShouldNotBeTransactionalIfDisabled() {
178+
load(SimpleConfiguration.class, "graphql.query-invoker.transactional=false");
179+
assertThatQueryInvokerIsNotTransactional();
180+
}
181+
182+
@Test
183+
public void queryInvokerShouldBeTransactionalIfConfigured() {
184+
load(SimpleConfiguration.class, "graphql.query-invoker.transactional=true");
185+
assertThat(this.getContext().getBean(GraphQLQueryInvoker.class))
186+
.as("Should be a transactional query invoker.")
187+
.isInstanceOf(TransactionalGraphQLQueryInvokerWrapper.class);
188+
}
189+
190+
private void assertThatQueryInvokerIsNotTransactional() {
191+
assertThat(this.getContext().getBean(GraphQLQueryInvoker.class))
192+
.as("Should be a non-transactional query invoker.")
193+
.isNotNull().isNotInstanceOf(TransactionalGraphQLQueryInvokerWrapper.class);
194+
}
165195
}

0 commit comments

Comments
 (0)