Part 2: GraphQL with Spring Boot, JPA and Kotlin

In Part 1 we set up a simple GraphQL backend application using Spring Boot, JPA and Kotlin. In the second part we will discuss best practices for such applications – especially how to test them.

You can find the source code here.

Best practices & useful configuration

JPA

JPA closes database sessions by default after loading all related data for a single query. GraphQL on the other hand resolves entities in lazily evaluated relationships in separate sessions. This is the reason why you receive a LazyInitializationException when you query a lazy collection:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.akquinet.demo.graphql.model.PersonEntity.pets, could not initialize proxy - no Session

To fix this error you must confine the execution of the query to a single transaction. It is similar to annotating a RestController class with @Transactional, but we cannot annotate the GraphQL API directly with @Transactional. Instead, we can choose a transactional ExecutionStrategy for GraphQL. In order to do this, we create a custom ExecutionStrategyService extending AsyncExecutionStrategy and annotate it with @Transactional.

@Service
class AsyncTransactionalExecutionStrategyService : AsyncExecutionStrategy() {

    @Transactional
    @Throws(NonNullableFieldWasNullException::class)
    override fun execute(executionContext: ExecutionContext, parameters: ExecutionStrategyParameters): CompletableFuture<ExecutionResult> {
        return super.execute(executionContext, parameters)
    }
}

Then we activate the async execution for GraphQL by setting the queryExecutionStrategy to the custom service:

@Configuration
class GraphQLConfig @Autowired constructor(val asyncTransactionalExecutionStrategyService: AsyncTransactionalExecutionStrategyService) {

    @Bean
    fun executionStrategies(): Map<String, ExecutionStrategy> {
        val executionStrategyMap = HashMap<String, ExecutionStrategy>()
        executionStrategyMap["queryExecutionStrategy"] = asyncTransactionalExecutionStrategyService
        return executionStrategyMap
    }
}

Now your GraphQL queries can lazily load entity relationships.

Please keep in mind, that a transaction will remain open for the whole lifetime of the request when you use AsyncExecutionStrategy. Long-running transactions could cause problems once you scale the application up. A better approach would be to use data loaders to batch queries (see link).

Alternatively, you can configure JPA to eagerly load related entities. But in the case you may run into performance issues, especially if you have highly interconnected data and a single request can load many entities from your database.

Split schema by database entity

Anyone who has created a GraphQL schema knows that it can quickly become very large and therefore confusing. This can be circumvented by dividing the schema across several files. One file per entity is ideal. However, since there can only be one definition of each of the base types Query and Mutation, each file must define a type extending a base type using the keyword extend. The main base types themselves must also be defined. Therefore, you have to create a root schema file containing empty definitions of the base types. See src/main/resources/graphqls/query.graphqls:

type Query {}
type Mutation {}

Now you create a new file for the entity Person in which you extend the Query base type:

type Person {
    id: ID!
    name: String
    address: Address
    pets: [Pet!]!
}

extend type Query {
    persons: [Person!]!
    person(id: ID!): Person
}

Error HandlING

Error handling is an important issue for GraphQL: GraphQL can deliver partial results and several independent error messages in the same response. By default, no error handler is provided in the Graphql-Spring-Boot-Starter project. This gives the client no specific information about errors, except the error messages.

For example, let’s take a look at the PersonQueryResolver, which produces an error, if a queried person doesn’t exists:

class PersonQueryResolver @Autowired constructor(private val personRepository: PersonRepository) : GraphQLQueryResolver {
    fun person(id: Long): PersonEntity = personRepository
            .findById(id)
            .orElseThrow { Exception("Can not find person with id: $id") }
}

In the following very small example it is easy enough to detect, which query part produced the error. It is fetch person with ID=2. To locate the error in source code it would be helpful to receive additional information.

Only the error message is displayed

To receive the more detailed error information you have to do the following steps: First of all, exception handlers should be activated in the Spring configuration file: graphql.servlet.exception-handlers-enabled: true

In addition, a custom error handler Bean extending GraphQLErrorHandler must be provided:

@Bean
fun graphQLErrorHandler(): GraphQLErrorHandler {
    return GraphQLErrorHandler { errors -> errors }
}

Now you get the full error information: The column and line in the query where the error occurred. It should be noted that only DataFetchingExceptions are displayed here, not the actual thrown Exceptions. 😦

Now you see a more detailed error message

Asynchon Resolver

In the first part we saw how different resolvers are implemented. So far, all implemented resolver methods shown are sequential. However, asynchronous resolvers are possible. If suspending functions are used in a resolver, utilize Coroutines to execute them asynchronously. See example below:

@Component
class PersonQueryResolver @Autowired constructor(private val personRepository: PersonRepository) : GraphQLQueryResolver {

    suspend fun persons(): List<PersonEntity> {
        delay(1000L) // some async stuff
        return personRepository.findAll()
    }
}

Activate response compression

Due to their complexity, GraphQL responses can very quickly become very large. It is helpful to activate gzip compression in the Spring configuration:

server.compression.enabled: true

By default, only responses above a size of 2kb are compressed to avoid unnecessary overhead.

Testing

Frequently, automated tests receive too little attention, although they can provide assurance that an application works as intended. For this reason, we present a setup for GraphQL unit and integration testing.

Unit Tests

When using Kotlin with Mockito, note that the original mockito-library is not fully compatible with Kotlin. The following error may occur: Mockito returns null values for calls to methods like any(), which can cause IllegalStateException when passing them as non-nullable parameters. However, this can be fixed by a patched mockito variant. See build.gradle.kts:

dependencies {
    testCompile("com.graphql-java-kickstart:graphql-spring-boot-starter-test:5.10.0") {
        exclude(module = "mockito-core")
    }
    testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0")
}

Writing unit tests for GraphQL resolvers is analogous to your usual REST controller tests. GraphQL provides the GraphQLTestTemplate class, which works much like a RestTemplate with an interface designed specifically for GraphQL calls. Unfortunately, Spring Boot can only inject a GraphQLTestTemplate when the whole application is up. Therefore, a database must be provided for such unit tests. The following configuration can be used for this:

@RunWith(SpringRunner::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
@Profile("test")
@TestPropertySource(locations = ["classpath:application-test.yml"])

In order for the tests to be executed relatively quickly, an H2 database is configured in application-test.yml.

Here is an example of a test case testing resolver methods:

@RunWith(SpringRunner::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
@Profile("test")
@TestPropertySource(locations = ["classpath:application-test.yml"])
internal class PersonQueryResolverTest {

    @MockBean
    private lateinit var personRepository: PersonRepository

    @Autowired
    private lateinit var graphQLTestTemplate: GraphQLTestTemplate

    @Test
    fun `should get person by id`() {
        given(personRepository.findById(1))
                .willReturn(Optional.of(PersonEntity("myName",
                        AddressEntity("s", "z", "c"))))

        val postMultipart = graphQLTestTemplate.postMultipart(
                "query {person(id: 1) {name}}", "{}")
        assertThat(postMultipart).isNotNull

        val expectedResponseBody = """{"data":{"person":{"name":"myName"}}}"""
        JSONAssert.assertEquals(expectedResponseBody, postMultipart.rawResponse.body,
                JSONCompareMode.LENIENT)
    }
}

Integration Tests

The following is a setup for integration testing. During an integration test the application is holistically checked – therefore the same database as in production should also be used to recognize database-specific errors early.

As an example, the setup for a MariaDB is shown. To start the external database before a test, the test context should start a Docker container with the database inside. For this I use the library testcontainers. The following dependencies are needed:

dependencies {
    testIntegrationImplementation("com.graphql-java-kickstart:graphql-spring-boot-starter-test:5.10.0")
    testIntegrationImplementation("org.testcontainers:testcontainers:1.9.1")
    testIntegrationImplementation("org.testcontainers:mariadb:1.9.1")
}

Each integration test must inherit from the following configuration class. This ensures that the database is started up before each test run and stopped afterwards.

@RunWith(SpringRunner::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(initializers = [ApplicationInitializer::class])
@DirtiesContext
abstract class AbstractDbTest {
    companion object {
        private val logger = logger()
        var started = false
        val db = KMariaDBContainer("mariadb:10.4")
                .withEnv("MYSQL_INITDB_SKIP_TZINFO", "1")
                .waitingFor(Wait.forListeningPort())

        @JvmField
        @ClassRule
        val containers = object : ExternalResource() {
            override fun before() {
                logger.info("starting db")
                db.start()
                started = true
            }

            override fun after() {
                db.stop()
            }
        }
    }
}

class KMariaDBContainer(image: String) : MariaDBContainer<KMariaDBContainer>(image)

After the Docker container with the database is started, Spring needs to know how to connect to it. In this configuration class the necessary connection information is passed into corresponding environment variables.

class ApplicationInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {

    private val logger = logger()

    override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) {

        val datasourceUrl = "spring.datasource.url=" + AbstractDbTest.db.jdbcUrl
        val username = "spring.datasource.username=" + AbstractDbTest.db.username
        val password = "spring.datasource.password=" + AbstractDbTest.db.password

        val values = TestPropertyValues.of(datasourceUrl, username, password)

        logger.info("using test properties {}, {}, {}", datasourceUrl, username, password)
        values.applyTo(configurableApplicationContext)
    }
}

If this setup is present, integration tests against the GraphQL interface can be performed. For this test we have to extend the class by AbstractDbTest.

internal class PersonQueryResolverTest : AbstractDbTest() {

    @Autowired
    private lateinit var personRepository: PersonRepository

    @Autowired
    private lateinit var petRepository: PetRepository

    @Autowired
    private lateinit var graphQLTestTemplate: GraphQLTestTemplate

    @Test
    fun `should list all persons`() {
        // insert test-data
        val p0 = personRepository.saveAndFlush(
                PersonEntity("myName",
                        AddressEntity("myStreet", "123", "Berlin")
                ))
        petRepository.saveAndFlush(PetEntity(p0, "Bello", PetType.DOG))

        // query data with graphQL
        val postMultipart = graphQLTestTemplate.postMultipart("""
            query {
                persons {
                    name, 
                    pets { name }
                }
            }
        """.trimIndent(), "{}")

        // assert response
        assertThat(postMultipart).isNotNull

        val expectedResponseBody = """
           {"data":{"persons":[{"name":"myName", pets: [{"name": "Bello"}]}]}}
        """
        JSONAssert.assertEquals(expectedResponseBody, postMultipart.rawResponse.body,
            JSONCompareMode.LENIENT)
    }
}

Conclusion

This article summarizes common best practices for implementing GraphQL applications with Spring Boot and JPA.

It should be emphasized that an out-of-the-box use of the Spring Boot Starter Project is not recommended, as additional configuration for lazy loading and error handling is required. But for that, JPA’s lazy loading and GraphQL fit together perfectly, because only queried fields are resolved. However, the error handling seems lacking because the name of a thrown Exception is obscured.

This article also presents an alternative approach to writing GraphQL schemata by creating a separate schema file for each entity. For me, this increases the clarity of the entire scheme, but everyone should decide for themselves.

Writing GraphQL tests is quite similar to normal Spring Boot tests – making it easier to write such tests.

Perspective

Our GraphQL application is now ready for use, but there is still room for improvement:

  • For example, the schema of GraphQL also supports directives. These allow data to be processed before the query.
  • In case of performance issues, tracing can be used to detect bottlenecks.
  • Some queries can be very slow due to the field evaluation strategy. As a result, the same data can be loaded multiple times. For example, if all Pets are requested with their owners and multiple pets have the same owner, each owner is always loaded separately. This problem can be solved by a custom Dataloader. A Dataloader will let you batch and cache database calls. Batching means that if Dataloader figures out that you’re reading from the same database table multiple times, it’ll batch all calls together. Caching means that if the Dataloader detects that two pets have the same owner, it will reuse the Person object it already has in memory instead of making a new database call.
  • If you can create dynamic queries, it is very easy to make very complex calls. If you want to protect your application against attackers, you should take a look at GraphQL security. For example a first step could be to define a maximum request complexity / depth.
  • A nice feature from GraphQL are subscriptions. They allows the client to subscribe for data-updates for specific queries.