First impressions with GraphQL in Java

October 16, 2017

GraphQL-Java

GraphQL is an open query language for exposing data in a flexible way. It was developed by Facebook and now in production use in many projects. The main concepts are that GraphQL describes the data available and then the consumer can ask for what they need. All using the same endpoint and language.

We are investigating its use to expose natural language processing results to replace, or augment, REST.

Java ecosystem

GraphQL is programming language agnostic and different implementations exist in many languages for this project we are using Java and Spring Boot so the natural choice is graphql-java. It may not be the only java implementation but it is certainly the most active at this time. This core project stays true to the specification and there are a number of supporting projects to ease integration and use.

Approach

In GraphQL you need to declare your data schema so clients can introspect the system and query correctly. However, the schema needs to be an accurate representation of your underlying data classes. While you could create both of these things manually, generating one from the other will reduce effort and errors. One approach is to define your schema and generate the data classes. This approach is provided by graphql-apigen and graphql-java-tools. While this approach could be very useful if you have a new project with a strict schema specification, we already have data classes. Therefore, we take the classes-first approach and generate the schema. There is a graphql-java-annotations project based on this approach, however, development of it seems to have stopped and the community seems to be moving towards graphgql-spqr (pronounced “speaker”). It looks likely that this will become the official graphql-java class-first approach.

Getting started

It was very easy to get started using the graphql-spring-boot. This gives:

  • A graphql-java servlet, to serve the schema, and accept GET and POST GraphQL requests
  • A GraphiQL UI, for writing and executing GraphQL queries against the published schema.
  • grapghql-java-tools schema first
  • graphql spring common class first

We chose not to use the latter and added graphql-spqr simple by providing a graphql.schema.GraphQLSchema Bean generated using graphql-spqr and annotations on DemoService and the POJOs used.

package io.committed;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import graphql.schema.GraphQLSchema;
import io.committed.query.DemoService;
import io.leangen.graphql.GraphQLSchemaGenerator;

@Controller
@EnableAutoConfiguration
@ComponentScan
public class GrahpQLDemo {

  @Autowired
  DemoService demoService;

  @Bean
  GraphQLSchema schema() {
    return new GraphQLSchemaGenerator()
        .withOperationsFromSingleton(demoService)
        .generate();
  }

  public static void main(String[] args) throws Exception {
    SpringApplication.run(GrahpQLDemo.class, args);
  }

}

Getting going

It was simple to add the graphql-spqr annotations to our existing JPA/MongoDB data access objects (DAO). In fact, if you are using the same names it is not even necessary as they will be picked up automatically. Alternatively, if you want to separate the DAO from the GraphQL definition you can define a set of data transfer objects (DTO) and use those. This may give you more flexibility if you can’t change your DAO layer. We exposed the types to the GraphQL by adding a root query method to each service i.e.:

package io.committed.query;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import io.committed.dao.repository.DemoRepository;
import io.committed.dto.Document;
import io.leangen.graphql.annotations.GraphQLQuery;

@Component
public class DocumentService {

  @Autowired
  DemoRepository repository;

  @GraphQLQuery(name = "allDocuments", description="Get all documents")
  public List<Document> getDocuments() {
    return repository.findAll();
  }
}

Arguments can be added to these methods for filtering, limiting, etc and if using Streams these can be returned directly:

  @GraphQLQuery(name = "allDocuments", description="Get all documents")
  public Stream<Document> getDocuments(
      @GraphQLArgument(name = "limit", defaultValue = "0") int limit) {
    Stream<Document> stream = repository.streamAll();
    if (limit > 0) {
      stream = stream.limit(limit);
    }

    return stream;
  }

Similarly, we can get a specific document by id, and directly return an Optional:

  @GraphQLQuery(name = "document")
  public Optional<Document> getDocument(@GraphQLArgument(name = "id") String id) {
    return repository.findById(id);
  }

In GraphQL the client asks explicitly for what data the result should contain. This does not have to be restricted to the fields of your data objects, methods can also be used to provide calculated results, here is a trivial example:

package io.committed.dto;

import io.leangen.graphql.annotations.GraphQLId;
import io.leangen.graphql.annotations.GraphQLQuery;
import lombok.Data;

@Data
public class Document {

  @GraphQLId
  private String id;
  private String content;

  @GraphQLQuery(name = "length")
  public int length() {
    return content.length();
  }

}

This will expose a length field on the Document class. This method is only called if requested by the query so the client does not have to pay the penalty for any calculations that they do not need. Such methods also allow the client to save on data transfer by getting the server to do the calculations and only transferring the result.

Getting graphing

The real power of GraphQL comes from the ability to traverse data type by their joining properties. For example, if I have Entities extracted from a Document then I want to be able to query for the entities contained in a document. If these are already stored in your Document object this is trivial, however, they may be stored in a different table or collection in your database. To embed entities in the document we add the following:

  @GraphQLQuery(name = "entities")
  public Stream<Entity> getByDocument(@GraphQLContext Document document) {
    return repository.getByDocumentId(document.getId());
  }

where the @GraphQLContext annotation provides the linking logic to build the schema.

Getting querying

You can query using the GraphiQL UI hosted on /graphiql or send HTTP requests to /graphql and get the schema from /schema.json, I’m sure all these are configurable too. You can also use the GraphQL internally, for example by creating a QueryService:

package io.committed.query;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;

@Service
public class QueryService {

  private final GraphQL graphQL;

  @Autowired
  public QueryService(GraphQLSchema graphQLSchema) {
    graphQL = GraphQL.newGraphQL(graphQLSchema).build();
  }

  public ExecutionResult query(String query) {
    return graphQL.execute(query);
  }

}

Or by exposing the GraphQL object as a bean.

Getting better

We were able to get a quick GraphQL endpoint up and running in less that a day on our existing DAO objects providing a powerful querying mechanism for clients. During this investigation, we uncovered and reported a bug in graphql-spqr and got a rapid response from the author. It looks likely that this ecosystem will develop fast, out suggestions for improvements to graphql-spqr are:

  • Ability to ignore certain fields
  • Out of the box support for limiting and filtering
  • Support for graphql-java v5

Overall, this is a very promising route for rapid integration of GraphQL into an existing spring-boot server.

Work with us

Have a project or an idea? We'd love to talk with you and see if we can help.

GET IN TOUCH
.

3 Strand Court

Bath Road

Cheltenham

Gloucestershire

GL53 7LW

© Committed