Spring - GraphQL

Overview

GraphQL is a query language that offers an alternative model to developing APIs instead of REST, SOAP or gRPC. It allows partial fetch of data, you can use a single endpoint to fetch different formats of data.

Github: https://github.com/gitorko/project96

Spring Boot GraphQL

Lets say you have a rest api that returns customer profile, the customer profile has 200+ fields, so a mobile device may not need all the fields, it may need may be 5 fields like name, address etc. Requesting a big payload over wire is costly. So now you end up writing a rest endpoint that returns just the 5 fields. This can become overwhelming when the requirements increase and you end up creating different endpoint for such requirement. In GraphQL you define a schema and let the user/consumer decide which fields they want to fetch.

Before GraphQL 1.0 was Released spring had to extend the classes GraphQLMutationResolver, GraphQLQueryResolver. Its no longer required.

GraphQLMutationResolver -> @MutationMapping

GraphQLQueryResolver -> @QueryMapping

The code uses Extended Scalars for graphql-java to support Date and other type objects in GraphQL The code shows how pagination can be done in GraphQL

Code

 1package com.demo.project96.controller;
 2
 3import java.util.Optional;
 4
 5import com.demo.project96.domain.Comment;
 6import com.demo.project96.domain.Post;
 7import com.demo.project96.domain.PostPage;
 8import com.demo.project96.repo.CommentRepository;
 9import com.demo.project96.repo.PostRepository;
10import lombok.RequiredArgsConstructor;
11import lombok.extern.slf4j.Slf4j;
12import org.springframework.data.domain.Page;
13import org.springframework.data.domain.PageRequest;
14import org.springframework.graphql.data.method.annotation.Argument;
15import org.springframework.graphql.data.method.annotation.QueryMapping;
16import org.springframework.graphql.data.method.annotation.SchemaMapping;
17import org.springframework.stereotype.Controller;
18
19@Controller
20@Slf4j
21@RequiredArgsConstructor
22public class QueryController {
23
24    private final PostRepository postRepository;
25    private final CommentRepository commentRepository;
26
27    @QueryMapping
28    public Iterable<Post> findAllPosts() {
29        return postRepository.findAll();
30    }
31
32    @QueryMapping
33    public PostPage findAllPostsPage(@Argument Integer page, @Argument Integer size) {
34        PageRequest pageOf = PageRequest.of(page, size);
35        Page<Post> all = postRepository.findAll(pageOf);
36        return PostPage.builder()
37                .posts(all.getContent())
38                .totalElements(all.getTotalElements())
39                .totalPages(all.getTotalPages())
40                .currentPage(all.getNumber())
41                .size(all.getNumberOfElements())
42                .build();
43    }
44
45    @QueryMapping
46    public Optional<Post> findPostById(@Argument("id") Long id) {
47        return postRepository.findById(id);
48    }
49
50    @QueryMapping
51    public Iterable<Comment> findAllComments() {
52        //Will cause N+1 problem
53        //return commentRepository.findAll();
54        return commentRepository.findAllComments();
55    }
56
57    @QueryMapping
58    public Optional<Comment> findCommentById(@Argument("id") Long id) {
59        return commentRepository.findById(id);
60    }
61
62    @QueryMapping
63    public long countPosts() {
64        return postRepository.count();
65    }
66
67    @QueryMapping
68    public Iterable<Comment> findCommentsByPostId(@Argument("postId") Long postId) {
69        Optional<Post> byId = postRepository.findById(postId);
70        if (byId.isPresent()) {
71            return commentRepository.findByPost(byId.get());
72        } else {
73            throw new RuntimeException("Post not found!");
74        }
75    }
76
77    /**
78     * Functionality will work same without this method as well.
79     * Hibernate Lazy fetch prevents the post entity from being fetched even without this method.
80     * So no unnecessary db call is made if post entity is not needed in the response even without this method.
81     * However if there is any reason why we want to control a single field explicitly we can use this approach and define how that field gets data.
82     * eg: You want to sort the comments
83     */
84    @SchemaMapping(typeName = "Comment", field = "post")
85    public Post getPost(Comment comment) {
86        return postRepository.findById(comment.getPost().getId()).orElseThrow(null);
87    }
88
89}
 1package com.demo.project96.controller;
 2
 3import java.time.ZonedDateTime;
 4import java.util.Optional;
 5
 6import com.demo.project96.domain.Comment;
 7import com.demo.project96.domain.Post;
 8import com.demo.project96.repo.CommentRepository;
 9import com.demo.project96.repo.PostRepository;
10import lombok.RequiredArgsConstructor;
11import lombok.extern.slf4j.Slf4j;
12import org.springframework.graphql.data.method.annotation.Argument;
13import org.springframework.graphql.data.method.annotation.MutationMapping;
14import org.springframework.stereotype.Controller;
15
16@Controller
17@Slf4j
18@RequiredArgsConstructor
19public class MutationController {
20
21    private final PostRepository postRepository;
22    private final CommentRepository commentRepository;
23
24    @MutationMapping
25    public Post createPost(@Argument("header") String header, @Argument("createdBy") String createdBy) {
26        Post post = new Post();
27        post.setHeader(header);
28        post.setCreatedBy(createdBy);
29        post.setCreatedDt(ZonedDateTime.now());
30        post = postRepository.save(post);
31        return post;
32    }
33
34    @MutationMapping
35    public Comment createComment(@Argument("message") String message, @Argument("createdBy") String createdBy, @Argument("postId") Long postId) {
36        Comment comment = new Comment();
37        Optional<Post> byId = postRepository.findById(postId);
38        if (byId.isPresent()) {
39            Post post = byId.get();
40            comment.setPost(post);
41            comment.setMessage(message);
42            comment.setCreatedBy(createdBy);
43            comment.setCreatedDt(ZonedDateTime.now());
44            comment = commentRepository.save(comment);
45            return comment;
46        } else {
47            throw new RuntimeException("Post not found!");
48        }
49
50    }
51
52    @MutationMapping
53    public boolean deleteComment(@Argument("id") Long id) {
54        commentRepository.deleteById(id);
55        return true;
56    }
57
58    @MutationMapping
59    public Comment updateComment(@Argument("id") Long id, @Argument("message") String message) {
60        Optional<Comment> byId = commentRepository.findById(id);
61        if (byId.isPresent()) {
62            Comment comment = byId.get();
63            comment.setMessage(message);
64            commentRepository.save(comment);
65            return comment;
66        }
67        throw new RuntimeException("Post not found!");
68    }
69}

The schema for GraphQL. The ! simply tells us that you can always expect a value back and will never need to check for null.

 1scalar DateTime
 2
 3type Post {
 4    id: ID!
 5    header: String!
 6    createdDt: DateTime!
 7    createdBy: String!
 8}
 9
10type PostPage {
11    posts: [Post]
12    totalElements: Int
13    totalPages: Int
14    currentPage: Int
15    size: Int
16}
17
18type Query {
19    findAllPosts: [Post]
20    findPostById(id: ID!): Post
21    countPosts: Int!
22    findAllPostsPage(page: Int = 0, size: Int = 20): PostPage
23}
24
25type Mutation {
26    createPost(header: String!, createdBy: String!): Post
27}

GraphQL accepts only one root Query and one root Mutation types, To keep the logic in different files we extend the Query and Mutation types.

 1type Comment {
 2    id: ID!
 3    message: String!
 4    createdBy: String!
 5    createdDt: DateTime!
 6    post: Post
 7}
 8
 9extend type Query {
10    findAllComments: [Comment]!
11    findCommentById(id: ID!): Comment!
12    findCommentsByPostId(postId: ID!): [Comment]
13}
14
15extend type Mutation {
16    createComment(message: String!, createdBy: String!, postId: ID!): Comment!
17    updateComment(id: ID!, message: String!): Comment!
18    deleteComment(id: ID!): Boolean
19}

The key terminologies in GraphQL are

  • Query: Used to read data
  • Mutation: Used to create, update and delete data
  • Subscription: Similar to a query allowing you to fetch data from the server. Subscriptions offer a long-lasting operation that can change their result over time.

Postman

Import the postman collection to postman

Postman Collection

Setup

 1# Project 96
 2
 3Spring Boot & GraphQL
 4
 5[https://gitorko.github.io/spring-graphql/](https://gitorko.github.io/spring-graphql/)
 6
 7### Version
 8
 9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14
15node --version
16v16.16.0
17
18yarn --version
191.22.18
20```
21
22### Postgres DB
23
24```
25docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
26docker ps
27docker exec -it pg-container psql -U postgres -W postgres
28CREATE USER test WITH PASSWORD 'test@123';
29CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
30grant all PRIVILEGES ON DATABASE "test-db" to test;
31
32docker stop pg-container
33docker start pg-container
34```
35
36### Dev
37
38To run the backend in dev mode.
39Postgres DB is needed to run the integration tests during build.
40
41```bash
42./gradlew clean build
43./gradlew bootRun
44```
45
46### Prod
47
48To run as a single jar.
49
50```bash
51./gradlew bootJar
52cd project96/build/libs
53java -jar project96-1.0.0.jar
54```
55
56### Graph IQL
57
58GraphQL comes with a browser client to test the Query. This can be enabled in properties
59
60```yaml
61graphql.graphiql.enabled: true
62```
63
64Open [http://localhost:8080/graphiql](http://localhost:8080/graphiql)
65
66### Postman
67
68Import the postman collection to postman
69
70[Postman Collection](https://github.com/gitorko/project96/blob/main/postman/Project96.postman_collection.json)

References

https://spring.io/projects/spring-graphql

https://github.com/graphql-java/graphql-java-extended-scalars

https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/

https://spring.io/blog/2022/05/19/spring-for-graphql-1-0-release

comments powered by Disqus