GraphQL, MongoDB and Java: An introduction
TLDR; Follow the guide below if you are interested in an up to date walkthrough on how to integrate GraphQL, MongoDB and Java - if not head over to my GitHub to view the code.
I decided to create this article as although a lot of guides exist today they were either incomplete, irrelevant or out-dated. After struggling to decipher existing tutorials I thought I would contribute my findings back in hope someone would find them useful…
Background
I wanted to build a GraphQL API for a showcase project I am currently working through. Alongside GraphQL the API will run under Spring Boot (to simplify the process of creating and running a Spring application) this will then connect to a simple MongoDB instance.
Now we know the why let’s explore the how!
MongoDB
As previously stated I want the application to connect to a MongoDB instance. For this tutorial I will be running MongoDB via docker-compose. If you don’t have docker and docker-compose installed then head over here for help on getting started. Alternatively, if you want to run on your own MongoDB instance then that’s fine :) (although the rest of this section will go into detail on setting up a Docker MongoDB instance).
Our compose file looks like the following:
version: '3'
services:
mongo:
image: mongo
env_file:
- mongo.env
ports:
- 27017:27017
volumes:
- ./data/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
Nothing special here although for those following closely there are 2 things to note:
Dot Env
I make use of dot env to remove the need to store security information in our configuration files as defined as best practice in 12 Factor Apps. For the purpose of this tutorial the values stored in the dot env file are provided below.
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=example
MONGO_INITDB_DATABASE=admin
MONGO_HOST=mongo
The file configures the initial username and password to authenticate against the DB, the name of the initial database where the user will be inserted and the hostname to connect to the MongoDB server (if I are running our GraphQL app via Docker).
Volume
Finally, I have added a volume which copies an initiailsation script into the docker-entrypoint-initdb.d
directory. This script is used to bootstrap the application, a snippet of it is below:
// Auth against admin DB
db.auth('root', 'example')
// Change to desired DB
db = db.getSiblingDB('graphql-app')
// Clear previous dummy data
db.users.drop();
db.articles.drop();
// Insert dummy data
db.articles.insertMany([...
Essentially, it:
- Authenticates against the database
- Selects the database I want to create for our test application
- Clears any existing data
- Creates and inserts dummy data into 2 collections -
Users
andArticles
Application
Now that I have our database up and running its time to look into the actual application. As a reminder this will be a Spring Boot application that creates a GraphQL server that will query the previously created MongoDB database.
Pom.xml
Our application utilises Maven for its dependency management. A key aspect of this guide is to highlight the required dependencies:
<!-- GraphQL -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>11.0</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.5.2</version>
</dependency>
The above dependencies are required to add GraphQL support to our project. One of the main aspects to notice here is that the graphql-java-tools
dependency is now published under com.graphql-java-kickstart
. This was a change that occurred in September 2018 but is not reflected in many guides.
<!-- Spring Tools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>${graphQL.helper.version}</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>${graphQL.helper.version}</version>
<scope>runtime</scope>
</dependency>
Here I add the Spring Boot and MongoDB starters respectively which configures our application as a Spring Boot app. It also adds the required libraries for working with MongoDB. The final two dependencies wire up our application to serve our GraphQL app and GraphiQL (an in browser IDE for querying GraphQL). Again these dependecies were transferred to a new repository in September 2018.
<!-- Data tools -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
Finally, I add the awesome Lombok to reduce the amount of boilerplate code I need to write.
Phew, that’s all of the required dependencies added and explained!
Spring Boot
In this project I make use of Spring Boot 2. I configure the application via an application.yml
file located in the projects resource directory (you can also use an application.properties
file if you so wish). Our application.yml
file looks like this:
spring:
data:
mongodb:
database: graphql-app
port: 27017
host: localhost
username: ${MONGO_USERNAME:root}
password: ${MONGO_PASSWORD:example}
authentication-database: ${MONGO_AUTH:admin} # Our user was added in the Admin DB
server:
port: 9000
graphql:
servlet:
enabled: true
graphiql:
enabled: true
Again nothing too special here. The main thing to note is that I configure access to our MongoDB instance via environment variables or with a default value if such an environment variable is not provided.
Graphqls
Rather than defining our schema in code I want to make use of SDL (Schema Definition Language) to define our GraphQL API - graphql-java-tools
allows us to do just that. I can define .graphqls
files that use SDL to describe our desired API. These files are then stored in the projects resources directory. A snippet of these definition files is provided below:
type Query {
users: [User]
user(id: ID!): User
}
type User {
id: ID!
name: String!
age: Int!
nationality: String
createdAt: String!
articles: [Article]
}
Here I define a Query
type that details the search endpoints I want to be available in our GraphQL API. I also define a User
type that maps to our users
MongoDB collection. An important note here is that a User
can be the author of zero or more articles
(which is detailed later on).
Entities
I create POJO’s (Plain Old Java Objects) to represent our MongoDB collections. An example POJO for the User
entity is provided below:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Document(collection = "users")
public class User {
private String id;
private String name;
private Integer age;
private Date createdAt;
private String nationality;
private List<ObjectId> articles;
}
See lombok for more information on the annotations.
The @Document
annotation is provided by the Spring Boot MongoDB dependency, it is used to associate the POJO with the appropriate MongoDB collection.
Finally, the eagle-eyed reader might have noticed that I define the id
property as a String
type although I make use of ObjectID's
in the database. The reason for this is that I will run into a mapping exception if I return the id property as part of a query (as it is returned as a String
from the database).
Repositories
A repository is used to provide access to the underlying MongoDB collection for querying. In our example these are very simple - I extend the default MongoRepository
for our collection which provides access to methods such as findById
etc.
@Repository
public interface UserRepository extends MongoRepository<User, ObjectId> {}
In the above snippet the first property in the type definition is the POJO class corresponding to this repository, the second property is the data type of the key for that collection.
Queries
In our schema definition I defined the following query:
user(id: ID!): User
The above defines an endpoint (/user
) that requires a non-nullable parameter of type ID. On completion either a null
value (as indicated by the lack of !
) or a response of type User
is returned. The corresponding implementation for this endpoint looks like:
@RequiredArgsConstructor
@Component
public class UserQueries implements GraphQLQueryResolver {
private final UserRepository userRepository;
public Optional<com.aa.graphql.entities.User> getUser(ObjectId id) {
return userRepository.findById(id);
}
}
This implements the query method as it:
- Returns an Optional (nullable)
User
type - Takes in a an ID parameter of type
ObjectID
- Uses the MongoDB method
findById
to find the appropriateUser
in the database
I also implement the GraphQLQueryResolver
interface to signify that I have implemented the Query method defined in our schema definition.
Resolvers
The final part of our tutorial covers the scenario where there is a requirement to provide more complex data structures as part of the query response (i.e. lookup a value from another collection). For this I need to implement a GraphQLResolver
. The resolver is typically the name of the POJO class with Resolver
on the end (In our example below the file is named UserResolver
). Hopefully you remember that the User
type has a complex property that returns an Array of articles associated to that user:
articles: [Article]
This can’t be queried from the single collection so I need to implement a resolver to retrieve the relevant data.
@RequiredArgsConstructor
@Component
public class UserResolver implements GraphQLResolver<User> {
private final ArticleRepository articleRepository;
public Iterable<Article> getArticles(User user)
{
return articleRepository.findAllById(user.getArticles());
}
}
The resolver is only invoked when a query retrieves Articles
for a User
. When a User
record is found, that object is passed into the resolver (as denoted by GraphQLResolver<User>
). As I now have the data for the desired user available to us I can get the value(s) stored in the articles
property (a list of article ObjectId’s) and use them to retrieve the desired Article
’s information.
Code
A complete sample project can be found on my GitHub repository. The project contains instructions on how to run the project locally for those of you that would like to play around with the code.
Conclusion
As I stated at the beginning of this article I aimed to provide a detailed and up-to-date walkthrough outlining how to integrate Java, GraphQL and MongoDB. Hopefully you have found this guide interesting and informative - or at least you enjoyed playing around with the source code!