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

Commit 32db152

Browse files
authored
Merge pull request #299 from BlasiusSecundus/feature/transactional-query-invoker
feat: added option to use a transactional query invoker
2 parents 1d15482 + fc611f8 commit 32db152

File tree

8 files changed

+196
-3
lines changed

8 files changed

+196
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ 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
48+
LIB_INTERCEPTOR_API_VERSION=1.2.2
4749
kotlin.version=1.3.31
4850

4951
GRADLE_WRAPPER_VER = 4.10.3

graphql-spring-boot-autoconfigure/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ dependencies {
2929
compile "commons-io:commons-io:$LIB_COMMONS_IO_VER"
3030

3131
compileOnly "org.springframework.boot:spring-boot-starter-web:$LIB_SPRING_BOOT_VER"
32+
compileOnly "javax.transaction:javax.transaction-api:$LIB_TRANSACTIONS_API_VERSION"
33+
compileOnly "javax.interceptor:javax.interceptor-api:$LIB_INTERCEPTOR_API_VERSION"
3234

35+
testCompile "javax.transaction:javax.transaction-api:$LIB_TRANSACTIONS_API_VERSION"
3336
testCompile "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER"
3437
testCompile "org.springframework.boot:spring-boot-starter-web:$LIB_SPRING_BOOT_VER"
3538
testCompile "org.springframework.boot:spring-boot-starter-test:$LIB_SPRING_BOOT_VER"
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+
}
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:[email protected]">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)