Spring Boot GraphQL

13.01.2018

Spring Boot - GraphQL

This article explains how to add a GraphQL interface to a Spring Boot application. An existing application with data model, repository and services is used and extended so that the data in the database can be accessed using GraphQL. The starting point is the following application: ItemStore The finished application has a GraphQL interface that can be accessed with cURL or Postman.

GraphQL Libraries

First, the following dependencies are added to the pom file of the Spring Boot application:

<dependency>
	<groupId>com.graphql-java</groupId>
	<artifactId>graphql-spring-boot-starter</artifactId>
	<version>3.6.0</version>
</dependency>
<dependency>
	<groupId>com.graphql-java</groupId>
	<artifactId>graphql-java-tools</artifactId>
	<version>3.2.0</version>
</dependency>

GraphQL Schema

The next step is to describe the data model and queries in a GraphQL schema.

schema {
 query : Query
}

type Query {
 allItems: [Item]
 item(id: Long): Item
 itemByDescription(description: String): [Item]
}

type Item {
 id : Long
 description: String
 location: String
 formattedDate: String
}

Three queries are defined under Query. One that returns all items (allItems) and one (item(id: Long)) that uses the id of an Item to search for the item. In the third query (itemByDescription(description: String)), items are searched based on their description. The square brackets indicate that a list is expected as the return value. At the end the Item is described with all its attributes and data types. It should be noted that the date has the type String, because the datatype date is not supported. More on this later in the article.

GraphQL Fetcher

Next, so-called DataFetchers are created, which serve to get the data from the database. In general, a DataFetcher is required for each query defined in the GraphQL schema.

ItemDataFetcher

For the Query item(id: Long)

package org.hameister.service;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import org.hameister.model.Item;
import org.hameister.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Created by hameister on 12.01.18.
 */
@Component
public class ItemDataFetcher implements DataFetcher<Item> {

    @Autowired
    ItemRepository itemRepository;

    @Override
    public Item get(DataFetchingEnvironment dataFetchingEnvironment) {
        Long id = dataFetchingEnvironment.getArgument("id");
        return itemRepository.findOne(id);

    }
}

ItemByDescriptionFetcher

For the Query itemByDescription(description: String)

package org.hameister.service;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import org.hameister.model.Item;
import org.hameister.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Created by hameister on 13.01.18.
 */
@Component
public class ItemByDescriptionFetcher implements DataFetcher<List<Item>>{

    @Autowired
    private ItemRepository itemRepository;


    @Override
    public List<Item> get(DataFetchingEnvironment environment) {
        String description = environment.getArgument("description");
        return itemRepository.findByDescription(description);
    }
}

ItemListDataFetcher

For the Query allItems

package org.hameister.service;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import org.hameister.model.Item;
import org.hameister.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Created by hameister on 12.01.18.
 */
@Component
public class ItemListDataFetcher implements DataFetcher<List<Item>> {


    @Autowired
    ItemRepository itemRepository;

    @Override
    public List<Item> get(DataFetchingEnvironment dataFetchingEnvironment) {
        return itemRepository.findAll();
    }
}

GraphQL Service

The DataFetchers are used by the GraphQL service to retrieve and prepare the data from the database:

package org.hameister.service;

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;

/**
 * Created by hameister on 12.01.18.
 */
@Service
public class GraphQLService {
    @Value("classpath:items.graphql")
    Resource schemaResource;

    @Autowired
    private ItemDataFetcher itemDataFetcher;

    @Autowired
    private  ItemListDataFetcher allItemFetcher;

    @Autowired
    private  ItemByDescriptionFetcher itemByDescriptionFetcher;

    private  GraphQL graphQL;

    @PostConstruct
    public void loadGraphQLSchema() throws IOException {
        File schema = schemaResource.getFile();
        TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(schema);
        RuntimeWiring runtimeWiring = initWiring();
        GraphQLSchema graphQLSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry,runtimeWiring);

        graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

    private RuntimeWiring initWiring() {

        return RuntimeWiring.newRuntimeWiring()
                .type("Query", typeWiring -> typeWiring
                .dataFetcher("allItems", allItemFetcher)
                .dataFetcher("itemByDescription", itemByDescriptionFetcher)
                .dataFetcher("item", itemDataFetcher)).build();
    }

    public GraphQL getGraphQL() {
        return graphQL;
    }
}

The service reads the GraphQL schema from the file items.graphql and initializes the DataFetchers and creates a GraphQL object that the controller uses to perform the queries.

REST Controller

Finally, a REST controller is created using a POST method against which the queries are executed by Postman or cURL.

package org.hameister.controller;

import graphql.ExecutionResult;
import org.hameister.service.GraphQLService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by hameister on 12.01.18.
 */
@RestController
public class GraphQLItemController {

    @Autowired
    GraphQLService graphQLService;

    @PostMapping(value = "/graphql/items")
    public ResponseEntity<Object> allItems(@RequestBody String query) {
        ExecutionResult result = graphQLService.getGraphQL().execute(query);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }
}

Date of the Item

In the GraphQL schema, the data type for the date was String, because GraphQL does not support the LocalDate data type. For a formatted date to be returned as a String in a query, a small change must be made to the class Item.

package org.hameister.model;

import javax.persistence.*;
import java.time.LocalDate;

/**
 * Created by hameister on 24.12.16.
 */
@Entity
@Table(name = "Item")
public class Item {

    public Item() {
    }

    public Item(String description, String location, LocalDate itemdate) {
        this.description = description;
        this.location = location;
        this.itemdate = itemdate;
    }

    public Item(Long id,String description, String location, LocalDate itemdate) {
        this.id=id;
        this.description = description;
        this.location = location;
        this.itemdate = itemdate;
    }

    @Id
    @GeneratedValue
    Long id;

    @Column(name = "description")
    private String description;

    @Column(name = "location")
    private  String location;

    @Column(name = "itemdate")
    private LocalDate itemdate;

    private transient  String formattedDate;

    // Getter and setter

    public String getFormattedDate() {
        return getItemdate().toString();
    }
}

For the sake of simplicity, the transient variable formattedDate has simply been added and a getter for the variable.

The project structure should look like this after the changes.

When starting the application, there is another REST endpoint under: localhost:8080/graphql/items

This can be used, for example, with Postman:

On the screenshot you can see that in the POST request all three queries are sent at once.

The application then delivers the following response:

{
    "errors": [],
    "data": {
        "itemByDescription": [
            {
                "id": 1,
                "description": "Tasse",
                "location": "Regal A"
            },
            {
                "id": 3,
                "description": "Tasse",
                "location": "Regal A"
            }
        ],
        "item": {
            "location": "Regal A",
            "description": "Teller",
            "formattedDate": "2016-12-09"
        },
        "allItems": [
            {
                "id": 1,
                "location": "Regal A",
                "description": "Tasse",
                "formattedDate": "2016-12-08"
            },
            {
                "id": 2,
                "location": "Regal A",
                "description": "Teller",
                "formattedDate": "2016-12-09"
            },
            {
                "id": 3,
                "location": "Regal A",
                "description": "Tasse",
                "formattedDate": "2016-12-10"
            }
        ]
    },
    "extensions": null
}

The complete source code can be found on Github at ItemStore in the branch graphQL.