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
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