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 will be a trimmed-down version of the Goodreads data set, which includes 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 nuances of the relationships make 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:
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 free tier of the AuraDB database-as-a-service option. Neo4j will handle managing the database, which 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 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 a 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 login 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 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.
- Test application is live: Open a browser and go to `localhost:8080/neo` or go to the command line with `curl localhost:8080/neo`.
- 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`.
- 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!
Find 1000 reviews
Find reviews by book
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!
Resources