Build a Java Microservice With AuraDB Free
For today’s adventure, we want to build a Java microservice that connects to and interacts with graph data in a Neo4j AuraDB Free database. Our data is a trimmed-down version of the Goodreads data set, which includes the book, author, and review information. While books and authors are well-suited for a document database such as MongoDB, once you add reviews to the mix, the nuance of the relationships makes this project better suited for AuraDB. This way, we can utilize relationships between the different entities to improve analysis based on the structure of the connections. While everything we will do here is feasible with other data stores, a graph database dramatically simplifies queries into how entities are connected.
If you are new to graphs, here are some resources:
Blog post: How do you know if a graph database solves the problem?
Neo4j guide: What is a graph database?
Neo4j offers several deployment options. We could spin up a Docker container (as we did in an earlier project using MongoDB) or take advantage of the database-as-a-service option called AuraDB with a free tier. Neo4j will handle managing the database, and it should have everything we need.
For this task, we will create our Neo4j database, get the data loaded, then build a microservice that interacts with the database and provides an API for client services.
Database-As-A-Service: AuraDB
Signing up and creating the Neo4j AuraDB Free instance takes a couple of steps, such as verifying your email address and waiting for the instance to spin up (it takes a few minutes). You can find detailed information about that process, along with screenshots, in many of my colleague Michael Hunger's "Discover AuraDB Free" blog posts, such as this one.
Graph Data Load
Once the instance is running, we can get started with our data load. The starter file containing books is available in today’s code repository, alongside instructions in the folder's readme and a load script. A couple of queries included in the readme allow us to verify the data.
Note Larger versions of this data set fit on an AuraDB Free tier instance, as well, but for ease and speed of loading in this post, we chose to keep the data set smaller.
Application Service
Time to start building our application to pull review data. For our microservice, we will build a Spring Boot application with a couple of REST endpoints to access the data from the connected Neo4j database.
We can put together the outline of our project using the Spring Initializr at start.spring.io.
On the form, we can leave Project
, Language
, and Spring Boot
fields defaulted. Under the Project Metadata
section, I have chosen to update the group name for my personal projects, but you are welcome to leave it defaulted as well. I named the artifact neo4j-java-microservice
, but naming isn't necessarily important. All other fields in this section can remain as they are. Under the Dependencies
section, we will need Spring Reactive Web
, Lombok
, and Spring Data Neo4j
. Spring Data Neo4j is one of many Spring Data projects for connecting to various data stores, helping us a map and access data loaded into our database. Finally, the project template is complete, and we can click the Generate
button at the bottom to download the project.
Note: the Spring Initializr displays in dark mode or light mode via the moon or sun icons in the right column of the page.
Generating will download the project as a ZIP file, so we can unzip it and open it in your favorite IDE.
The pom.xml
file contains the dependencies and software versions we set up on the Spring Initializr, so we can move to the application.properties
file in the src/main/resources
folder. Here, we want to connect to our Neo4j instance with URI and database credentials. We typically do not want to embed our database credentials in an application, especially since it is a cloud-based instance. Hard-coding these values could accidentally give others access to log in or tamper with our database.
Usually, configuration values like database credentials would be externalized and read in by the project at runtime using a project like Spring Cloud Config. However, configuration values are beyond the scope of this post. For now, we can embed our database URI, username, password, and database name in the properties file.
#database connection
spring.neo4j.uri=<insert Neo4j URI here>
spring.neo4j.authentication.username=<insert Neo4j username here>
spring.neo4j.authentication.password=<insert Neo4j password here>
spring.data.neo4j.database=<insert Neo4j database here>
Note: Database should be neo4j
unless you have specifically used commands to change the default.
Project Code
Let's walk through the Java files starting with the domain class.
@Data
@Node
class Review {
@Id
@GeneratedValue
private Long neoId;
@NonNull
private String review_id;
private String book_id, review_text, date_added, date_updated, started_at, read_at;
private Integer rating, n_comments, n_votes;
}
The @Data
is a Lombok annotation that generates our getters, setters, equals, hashCode, and toString methods for the domain class. It cuts down on the boilerplate code, so that's nice. Next is the @Node
annotation. This is a Spring Data Neo4j annotation that marks it as a Neo4j entity class (Neo4j entities are called nodes).
Within the class declaration, we define a few fields (properties) for our class. The `@Id` annotation marks the field as a unique identifier, and the @GeneratedValue
says that the value is generated internally by Neo4j. On our next field review_id
, we have a Lombok @NonNull
annotation that specifies this field cannot be null. We also have some other fields we want to retrieve for the review text, dates, and rating information.
Next, we need a repository interface where we can define methods to interact with the data in the database.
interface ReviewRepository extends ReactiveCrudRepository<Review, Long> {
Flux<Review> findFirst1000By();
@Query("MATCH (r:Review)-[rel:WRITTEN_FOR]->(b:Book {book_id: $book_id}) RETURN r;")
Flux<Review> findReviewsByBook(String book_id);
}
We want this repository to extend the ReactiveCrudRepository
, which will let us use reactive methods and types for working with the data. Then, we define a couple of methods. While we could use Spring Data's out-of-the-box implementations of a few default methods (listed in the code example of the documentation), we want to customize a little bit, so we will define our own. Instead of using the default .findAll()
method, we want to pull only 1,000 results because pulling all 35,342 reviews could overload the results-rendering process on the client.
Notice we do not have any implementation code with the findFirst1000By()
method (no query or logic). Instead, we are using another of Spring Data's features: derived methods. This is where Spring constructs (i.e., "derives") what the query should be based on the method name. In our example, our repository is dealing with reviews (ReactiveCrudRepository<Review, Long>
), so findFirst1000
is looking for the first 1,000 reviews. Normally, this syntax would continue by finding the results by
a certain criteria (rating, reviewer, date, etc.). However, since we want to pull any random set of reviews, we can trick Spring by simply leaving off the criteria from our method name. This is where we get the findFirst1000By
.
Note: This is a hidden workaround that is pretty handy once you know it, but it would be nice if Spring provided an out-of-the-box solution for these cases.
Our next method (findReviewsByBook()
) is a bit more straightforward. We want to find reviews for any specific book, so we need to look up reviews by book_id
. For this, we use the @Query
annotation with a query in the database's related query language, which is Cypher for Neo4j. This query looks for reviews written for a book with the specified book id.
With the repository complete, we can write our controller class that sets up some REST endpoints for other services to access the data.
@RestController
@RequestMapping("/neo")
@AllArgsConstructor
class ReviewController {
private final ReviewRepository reviewRepo;
@GetMapping
String liveCheck() { return "Neo4j Java Microservice is up"; }
@GetMapping("/reviews")
Flux<Review> getReviews() { return reviewRepo.findFirst1000By(); }
@GetMapping("/reviews/{book_id}")
Flux<Review> getBookReviews(@PathVariable String book_id) { return reviewRepo.findReviewsByBook(book_id); }
}
The @RestController
Spring annotation designates this block as a rest controller class, and the @RequestMapping
defines a high-level endpoint for all of the class methods. Within the class declaration, we inject the ReviewRepository
, so that we can utilize our written methods.
Next, we map endpoints for each of our methods. The liveCheck()
method uses the high-level /neo
endpoint to return a string, ensuring that our service is live and reachable. We can execute the getReviews()
method by adding a nested endpoint (/reviews
). This method uses the findFirst1000By()
method that we wrote in the repository and returns a reactive Flux<>
type where we expect 0 or more reviews in the results.
Our final method has the nested endpoint of /reviews/{book_id}
, where the book id is a path variable that changes based on the book we want to search. The getBookReviews()
method passes in the specified book id as the path variable, then calls the findReviewsByBook()
method from the repository and returns a Flux<>
of reviews.
Put It to the Test
Time to test our new service! I like to start projects from bottom to top, so first, ensure the Neo4j AuraDB instance is still running.
Note: AuraDB free tier pauses automatically after three days. You can resume with the `play` icon on the instance.
Next, we need to start our neo4j-java-microservice
application, either through an IDE or command line. Once that is running, we can test the application with the following commands.
1. Test application is live: Open a browser and go to localhost:8080/neo
or go to the command line with curl localhost:8080/neo
.
2. Test backend reviews api finding reviews: Open a browser and go to localhost:8080/neo/reviews
or go to the command line with curl localhost:8080/neo/reviews
.
3. Test API finding reviews for a certain book: Open a browser and go to localhost:8080/neo/reviews/178186
or go to the command line with curl localhost:8080/neo/178186
.
And here is the resulting output from the reviews api results from our service!
Wrapping Up
We walked through creating a graph database instance using Neo4j AuraDB free tier and loaded data for books, authors, and reviews. Then we built a microservice application to connect to the cloud database and retrieve reviews. Finally, we tested all of our code by starting the application and hitting each of our endpoints to ensure we could access the data.
There is so much more we can do to expand what we have already built. To keep sensitive data private yet accessible to multiple services, we could externalize our database credentials using something like Spring Cloud Config. We could also add this service to an orchestration tool, such as Docker Compose, to manage multiple services together. Another area of exploration would be to take fuller advantage of graph data benefits by pulling more related entities from the database. Happy coding!