Starting development using GraphQL

Hello! We are Ekaterina and Victoria, developer and senior developer at BFT-Holding. In this article we will briefly talk about the basics of the DGS framework, its advantages, the problems we encountered while working with it, and also show how to create a simple service with WebFlux support.

DGS (Domain Graph Service) is an open source project by Netflix. At first it was an internal project of the company, but in 2020 it was decided to make it open to the community. The framework has been developing since 2019 and was used by Netflix even before it was released into open source. According to the creators, the DGS framework is a production-ready solution.

DGS one of several frameworks for working with GraphQL in Java. It is built based on the library graphql-java and makes it easier to work with.

Reasons for choosing DGS Framework

DGS provides convenient out-of-the-box tools for project setup. The framework provides the ability to configure a template request handler via DataFetcher<T> and assemble the diagram using annotations @DgsCodeRegistry And @DgsTypeDefinitionRegistry, which allows you to store schema metadata in any structure and build it during application startup. Including the DGS framework, it can load ready-made GraphQLSchema schemas, where they will be combined with other schemas (ready-made or from annotations). In our example, for simplicity, we consider loading a ready-made circuit, but in practice the project most often does not look so simple.

Technical requirements

The DGS framework uses Spring Boot. For 6.x and later versions of the framework, Spring Boot 3 and JDK 17 are required. However, you can use the framework with earlier versions of Spring Boot, then you will need to use older versions of the DGS framework. For projects with Spring Boot 2.7, DGS 5.5.x releases are suitable. If the project uses Spring Boot 2.6, then version 5.4.x is required.

Implementation of GraphQL service

Here we will briefly describe the main points of creating an application using the DGS framework.

Initial setup

Let's start by creating a Spring Boot application. The project used JDK 17, Spring Boot 3.0.11, WebFlux and Maven. Let us remind you that older versions of Spring Boot are not supported in new versions of DGS (starting from 6.x).

The project structure will look like this:

Project structure

Let's go to the generated project. Let's add the necessary dependencies to pom.xml:

 <dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>com.netflix.graphql.dgs</groupId>
              <artifactId>graphql-dgs-platform-dependencies</artifactId>
              <!-- The DGS BOM/platform dependency. This is the only place you set version of DGS -->
              <version>6.0.5</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
					<!-- fix bug Could not initialize class com.netflix.graphql.dgs.DgsExecutionResult$Builder -->
					<dependency>
              <groupId>com.graphql-java</groupId>
              <artifactId></artifactId>
              <version>20.3</version>
          </dependency>
      </dependencies>
  </dependencyManagement>
<dependency>
    <groupId>com.netflix.graphql.dgs</groupId>
    <artifactId>graphql-dgs-webflux-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version>
</dependency>

In the test project, we are considering graphql-dgs-webflux-starter, since for the most part we strive to use reactive programming, and we needed to find out whether the DGS Framework can support the non-blocking approach. If the project is based on regular Spring Boot, then you can use graphql-dgs-spring-boot-starter.

The com.graphql-java block in is required to fix the Could not initialize class com.netflix.graphql.dgs.DgsExecutionResult$Builder error. The reason for this error is that the default version of the com.graphql-java dependency is pulled. Therefore, you need to specify the version manually in dependencyManagement (you can read more about the problem and solution Here).

Code generation

If there is a need to generate java models and data fetchers, you can add the use of a code generator. The framework has such a plugin (for Gradle, for Maven there is only community plugin). We used only the basic features and settings of the plugin: we specified the path to the schema and the package for the generated classes .

The plugin is quite flexible and allows you to customize many of its parameters. You can read more about generation settings and the list of customizable parameters on the plugin's GitHub page.

 <dependency>
    <groupId>com.netflix.graphql.dgs.codegen</groupId>
    <artifactId>graphql-dgs-codegen-client-core</artifactId>
    <version>5.1.17</version>
</dependency>
<plugin>
    <groupId>io.github.deweyjose</groupId>
    <artifactId>graphqlcodegen-maven-plugin</artifactId>
    <version>1.24</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <schemaPaths>
            <param>src/main/resources/schema/blog.graphqls</param>
        </schemaPaths>
        <packageName>com.example.blogdemo.generated</packageName>
    </configuration>
</plugin>
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>${project.build.directory}/generated-sources</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

Scheme

Next, following the schema-first approach, we will add a GraphQL schema to src/resources/schema/blog.graphqls. In this project, as an example, the structure of a simple blog with posts, comments and their authors will be described:

type Query {
    posts(titleFilter: String): [Post]
    post(idFilter: Int!): Post
}

type Post {
    id: Int!
    title: String
    text: String
    likes: Int
    author: User!
    comments: [Comment]
}

type Comment {
    id: Int!
    text: String!
    user: User!
    post: Post!
}

type User {
    id: Int!
    name: String!
    email: String
}

This scheme describes two requests: a list of posts (posts) and one post by its id (post). In the posts query, the filter by titleFilter is optional, meaning you can run the query with or without the filter. The schema also contains a description of the object data types that are used in these requests (Post, Comment and User).

Models

Then you will need similar java models corresponding to each object type described in the schema. They can either be generated based on the schema using the plugin mentioned above, or you can write them yourself. They are regular POJO classes and reflect the data structure described in blog.graphqls. For example, a post data class would look like this:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Post {
  private Integer id;
  private String title;
  private String text;
  private Integer likes;
  private Integer authorId;
}

To simplify the class structure I use Lombok.

Data Fetcher

Data fetchers are responsible for processing the request and returning the result of its execution. They can also be generated or written manually. These classes must be annotated @DgsComponent. For example, a data fetcher for posts would look like this:

@DgsComponent
@AllArgsConstructor
public class PostDataFetcher {
    private final PostService postService;

    @DgsQuery
    public Flux<Post> posts(@InputArgument String titleFilter) {
        if (titleFilter == null) {
            return postService.getPosts();
        }
        return postService.getPostsByTitle(titleFilter);
    }

    @DgsQuery
    public Mono<Post> post(@InputArgument Integer idFilter) {
        return postService.getPostById(idFilter);
    }
}

Methods must be annotated @DgsQuery and they match the queries described in the blog.graphqls schema. Except @DgsQuery The DGS framework has specialized annotations that point to other GraphQL operations: @DgsMutation And @DgsSubscription.

For greater clarity, in a data fetcher we obtain data from simple services that simply return data from an array.

Data Loader and the N+1 problem

Let's imagine that you want to get a list of posts with information about the author of each post. And let’s say that we will receive posts and authors from two different services. In a simple implementation, to obtain information about N posts, you will need to contact the post service 1 time to obtain a list of posts and N times contact the service with the authors, once for each post. This means that in total you will need to execute N+1 requests. Obviously this is not a very optimal solution.

Problem N+1

Problem N+1

The situation described is known as the N+1 problem. This is not unique to GraphQL and, depending on the tool, can be solved in different ways. When using DGS, this problem can be solved by using a data loader and batch loading data.

The process of obtaining data will change in this way: after receiving the list of posts, a list of required author ids will be prepared. Next, using the received set of ids, a request will be made for the entire list of authors at once.

Solving the N+1 Problem

Solving the N+1 Problem

The implementation of this approach is possible if two conditions are met. First, the author service must provide the ability to download a list of users by a list of ids. And secondly, the data fetcher must be able to batch load from the user service.

Let's create a data loader. This class will implement org.dataloader.BatchLoader or org.dataloader.MappedBatchLoader. These classes are generics, so you will need to specify the types for the key and result object. In this example, we will search for users with type User by their id of type Integer, so we will use org.dataloader.BatchLoader<Integer, User>.

@DgsDataLoader(name = "users")
@AllArgsConstructor
public class UserLoader implements BatchLoader<Integer, User> {
    private final UserService userService;
    @Override
    public CompletionStage<List<User>> load(List<Integer> list) {
        return CompletableFuture.supplyAsync(()->userService.getUserListByIds(list));
    }
}

In the created class you only need to implement one method: CompletionStage<List> load(List keys). The created class must be marked with an annotation @DgsDataLoaderso that the framework recognizes it as a data loader. However, although the data loader will be registered due to the annotation, it will not be used until its use in the data fetcher is described.

When retrieving a list of comments for a list of posts, the described approach will still apply, but with some changes. The result type and data types that the overridden method receives and returns will change:

@DgsDataLoader
@AllArgsConstructor
public class CommentLoader implements MappedBatchLoader<Integer, List<Comment>> {
    private final CommentService commentService;
    @Override
    public CompletionStage<Map<Integer, List<Comment>>> load(Set<Integer> list) {
        return CompletableFuture.supplyAsync(()->commentService.getCommentListByPostIds(new ArrayList<>(list)));
    }
}

Using Data Loader

As mentioned above, the described data loader must be used in some kind of data fetcher. Let's create such a data fetcher. Since the user will be needed both in the post and in the comment, there will also be two methods in the class.

@DgsComponent
public class UserDataFetcher {
    @DgsData(parentType = "Post", field = "author")
    public CompletableFuture<User> author(DgsDataFetchingEnvironment dfe) {
        DataLoader<Integer, User> dataLoader = dfe.getDataLoader(UserLoader.class);
        Post post = dfe.getSource();
        Integer id = post.getAuthorId();
        return dataLoader.load(id);
    }

    @DgsData(parentType = "Comment", field = "user")
    public CompletableFuture<User> commentUser(DataFetchingEnvironment dfe) {
        DataLoader<Integer, User> dataLoader = dfe.getDataLoader("users");
        Comment comment = dfe.getSource();
        Integer id = comment.getUserId();
        return dataLoader.load(id);
    }
}

IN @DgsData We indicate the parent object type and the field for which this method will be executed. The return type of the methods will be CompletableFuture, this is required for batch loading of data. The most important difference between this data fetcher will be that the data will be loaded not directly from the data service, but through a data loader.

You can get the required data loader in two ways, depending on what is specified in the received method parameters: DataFetchingEnvironment or DgsDataFetchingEnvironment. In case of working with DgsDataFetchingEnvironment You can search for the required data loader by class name. This method is more type-safe. If you use DataFetchingEnvironment, then the data loader will be searched by its name. Therefore, you will need to specify this name using the name parameter in the annotation @DgsDataLoader.

In the example above, the first method uses DgsDataFetchingEnvironmentsecond – DataFetchingEnvironment.

RxJava in Data Fetcher

A quick note about why in a data fetcher using a data loader the return type is CompletableFuture and not Flux as expected. As mentioned at the beginning of the article, the DGS framework uses the graphql-java library internally. And graphql-java, in turn, does not support RxJava/WebFlux. And DGS itself converts CompletableFuture to Mono/Flux.

Launch

The DGS framework by default adds the ability to use the GraphiQL tool to the project. You can access it at http://localhost:8080/graphiql after the standard project launch.

Conclusion

The DGS Framework provides a simple and quite attractive way to use GraphQL. The DGS platform makes request processing fast and accessible, simplifying the project development process. The source code for the demo project can be found in my repositories.

This article only covers a small part of what the DGS Framework can do, so if you still have questions about the framework, we will be happy to answer them.

I hope the article was interesting and useful. Thank you for your attention!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *