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

Commit 0715368

Browse files
committed
Merge remote-tracking branch 'origin/master' into feature/separate-graphql-tools-starter
# Conflicts: # graphql-spring-boot-autoconfigure/build.gradle # graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLWebAutoConfiguration.java # graphql-spring-boot-autoconfigure/src/test/java/com/oembedler/moon/graphql/boot/test/web/GraphQLWebAutoConfigurationTest.java
2 parents 67c7eb2 + 32db152 commit 0715368

File tree

8 files changed

+204
-8
lines changed

8 files changed

+204
-8
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.2.1.RELEASE
4444
LIB_GRAPHQL_SERVLET_VER = 9.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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ dependencies {
3030

3131
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:$LIB_SPRING_BOOT_VER"
3232
compileOnly "org.springframework.boot:spring-boot-configuration-processor:$LIB_SPRING_BOOT_VER"
33+
compileOnly "org.springframework.boot:spring-boot-starter-web:$LIB_SPRING_BOOT_VER"
34+
compileOnly "javax.transaction:javax.transaction-api:$LIB_TRANSACTIONS_API_VERSION"
35+
compileOnly "javax.interceptor:javax.interceptor-api:$LIB_INTERCEPTOR_API_VERSION"
3336

37+
testCompile "javax.transaction:javax.transaction-api:$LIB_TRANSACTIONS_API_VERSION"
3438
testCompile "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER"
3539
testCompile "org.springframework.boot:spring-boot-starter-web:$LIB_SPRING_BOOT_VER"
3640
testCompile "org.springframework.boot:spring-boot-starter-test:$LIB_SPRING_BOOT_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: 17 additions & 8 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

@@ -216,11 +216,14 @@ public GraphQLInvocationInputFactory invocationInputFactory(GraphQLSchemaServlet
216216
return builder.build();
217217
}
218218

219-
@Bean
220-
@ConditionalOnMissingBean
221-
public GraphQLQueryInvoker queryInvoker(ExecutionStrategyProvider executionStrategyProvider) {
222-
GraphQLQueryInvoker.Builder builder = GraphQLQueryInvoker.newBuilder()
223-
.withExecutionStrategyProvider(executionStrategyProvider);
219+
@Bean
220+
@ConditionalOnMissingBean
221+
public GraphQLQueryInvoker queryInvoker(
222+
ExecutionStrategyProvider executionStrategyProvider,
223+
GraphQLQueryInvokerProperties graphQLQueryInvokerProperties
224+
) {
225+
GraphQLQueryInvoker.Builder builder = GraphQLQueryInvoker.newBuilder()
226+
.withExecutionStrategyProvider(executionStrategyProvider);
224227

225228
if (instrumentations != null) {
226229

@@ -233,8 +236,14 @@ public GraphQLQueryInvoker queryInvoker(ExecutionStrategyProvider executionStrat
233236
builder.withPreparsedDocumentProvider(preparsedDocumentProvider);
234237
}
235238

236-
return builder.build();
237-
}
239+
GraphQLQueryInvoker queryInvoker = builder.build();
240+
241+
if (graphQLQueryInvokerProperties.isTransactional()) {
242+
queryInvoker = new TransactionalGraphQLQueryInvokerWrapper(queryInvoker);
243+
log.info("Using transactional query invoker.");
244+
}
245+
return queryInvoker;
246+
}
238247

239248
@Bean
240249
@ConditionalOnMissingBean
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: 32 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,17 @@
1213
import graphql.schema.GraphQLObjectType;
1314
import graphql.schema.GraphQLSchema;
1415
import graphql.servlet.AbstractGraphQLHttpServlet;
16+
import graphql.servlet.config.DefaultGraphQLSchemaProvider;
17+
import graphql.servlet.config.GraphQLSchemaProvider;
18+
import graphql.servlet.core.GraphQLQueryInvoker;
1519
import org.junit.Assert;
1620
import org.junit.Test;
1721
import org.springframework.context.annotation.Bean;
1822
import org.springframework.context.annotation.Configuration;
1923
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
2024

25+
import static org.assertj.core.api.Assertions.assertThat;
26+
2127
/**
2228
* @author <a href="mailto:java.lang.RuntimeException@gmail.com">oEmbedler Inc.</a>
2329
*/
@@ -162,4 +168,30 @@ public void appContextLoadsWithCustomSchemaProvider() {
162168

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

0 commit comments

Comments
 (0)