Java Application with Neo4j: How to use Spring custom queries and projections
In the previous blog post, we built a Java application on top of a Neo4j graph database showcasing the minimal translation between database and application models and the benefits of storing data with relationships.
We can take this a few steps further by exploring some additional features offered between the application and database, such as custom queries and domain class projections.
Project recap
Last time, we built a Spring Boot application that connected to a Neo4j AuraDB free cloud instance containing Java version data. The data model in the database shows two nodes connected by two different types of relationships. One relationship pulls diffs with older language versions, and one relationship pulls diffs with newer language versions.
In our application code, we created a REST API that called the derived Spring findAll()
method to retrieve Java versions and connected version diffs from the database. We can review the current status of the application by pulling the step-two branch of the Github repository, running the application from an IDE or command line, and navigating a browser to the URL localhost:8080/versions
to see the data.
Now that we have reviewed where we left off, let’s dive into a couple of other special features with Java and Spring Data Neo4j. As always, the code written for today’s demo is available in the related Github repository (step-three and main branches).
Custom queries
We can write our own custom queries instead of using Spring’s provided derived methods. We do this by writing our own method and accompanying query in the repository interface. Let’s say we want to be a bit more selective about the data we get back in a custom query. We only want to retrieve JavaVersion entities that have a FROM_NEWER
relationship, ignoring all the connected diffs to newer versions. This means our results will exclude Java 1.0
because it doesn’t have any diffs connected to it at all. They will also exclude Java 1.1
because it doesn’t have any older versions connected to it.
Add custom query to repository
public interface JavaVersionRepo extends Neo4jRepository<JavaVersion, String> { @Query("MATCH (j:JavaVersion)-[r:FROM_NEWER]->(d:VersionDiff) RETURN j, collect(r), collect(d);") Iterable<JavaVersion> findAllCustom(); }
We use the @Query
annotation to specify our domain-specific language query. Because Neo4j uses Cypher for its query language, we write a Cypher query that matches JavaVersion nodes connected to VersionDiff
nodes across a FROM_NEWER
relationship and returns each JavaVersion
entity and collected relationship and diff lists. The next line defines our findAllCustom()
method that expects multiple JavaVersion
entities in the results.
Next, we need to add the method to our controller class.
Add custom query to controller
@RestController @RequestMapping("/versions") @AllArgsConstructor public class JavaVersionController { <repository injection> <findAll default method> @GetMapping("/custom") Iterable<JavaVersion> findAllCustom() { return javaVersionRepo.findAllCustom(); } }
We will create a separate endpoint/implementation for this method, so that we can compare existing functionality. The @GetMapping
annotation appears again, but we have added a /custom
to the url for this method. We then define the method with our return type and method name, and return the results from calling the repository’s findAllCustom()
method we just created.
Test custom query
Let’s test it in our browser at localhost:8080/versions/custom
!
Everything is as expected! We don’t see Java 1.0 or 1.1 in the list because they don’t have any FROM_NEWER
relationships. We also don’t see any results in the newerVersionDiffs
because we are ignoring the FROM_OLDER
relationships in our custom query.
Let’s see if we can be even more selective about the data we return by using a projection.
Projection interfaces
We can use projections like a custom view of a data class, where we can trim out some fields or manipulate certain fields. Let’s try this with our JavaVersion
class so that we only see the version and the list of versions for the compared diffs. To do that, we will need to create a new interface called JavaVersionProjection
.
Create projection interface
public interface JavaVersionProjection { String getJavaVersion(); List<DiffProjection> getOlderVersionDiffs(); interface DiffProjection { String getFromVersion(); } }
Inside the interface definition, we can utilize getters from domain classes to retrieve desired pieces of our larger data classes. We start by calling the getJavaVersion()
to retrieve the javaVersion
property from our JavaVersion
class.
Then, we want to pull a comparing version from the VersionDiff
entity. So we need to create a nested projection to avoid pulling all the values from VersionDiff. We expect a List<>
of VersionDiff entities to return, then call the getter method from the JavaVersion domain class for getOlderVersionDiffs()
. The next line of code defines the DiffProjection
as a nested projection within our JavaVersionProjection. Inside the DiffProjection definition, we call the getFromVersion()
method to retrieve the field of the other Java version we are comparing.
With this in place, we can plug our JavaVersionProjection into repository and controller methods. Let’s start with the repository.
Use projection in repository query
public interface JavaVersionRepo extends Neo4jRepository<JavaVersion, String> { <findAllCustom() method> @Query("MATCH (j:JavaVersion)-[r:FROM_NEWER]->(d:VersionDiff) RETURN j, collect(r), collect(d);") Iterable<JavaVersionProjection> findAllProjection(); }
We will copy/paste our custom query to a new query for our projection. The only differences are the name of the method (called this one findAllProjection()
) and the return type (Iterable<JavaVersionProjection>
).
Next, we need to add it to our controller class.
Use projection in controller
@RestController @RequestMapping("/versions") @AllArgsConstructor public class JavaVersionController { <repository injection> <findAll default method> <findAll custom method> @GetMapping("/projection") Iterable<JavaVersionProjection> findAllProjections() { return javaVersionRepo.findAllProjections(); } }
Similar to our repository, we will create a new endpoint for comparison with the old results. We use the /projection
added to the url for our @GetMapping
, then have a return type of multiple JavaVersionProjection
entities and name the method findAllProjections()
. Inside the method, we return the results from calling the repository’s findAllProjections()
method.
Test projection
Let’s test it by going to our browser with the URL localhost:8080/versions/projection
!
We are getting back only the Java version and the list of versions from compared diffs, making the results clean and easy to read. We can also test our other endpoints of localhost:8080/versions
and localhost:8080/custom
to see what we had previously.
Wrap up!
In today’s post, we continued some previous work on a Java application. We explored additional features for Java applications with custom queries and projections, allowing us to pick and choose the data returning.
There are so many more things we could explore on this topic, but hopefully, this sparked your curiosity to discover. Happy coding!
Resources
- Github repository: today’s code
- Neo4j AuraDB: free cloud instance
- Github repository: Java version data
- Blog post: Part 1 of Pouring Coffee into the Matrix
I was browsing the internet a few months back when I came across another website that provided an in-depth discussion on this subject. I am relieved that you were able to put some light on the true nature of the situation that is taking on out there. Certain websites have a clear and obvious bias toward topics of this nature. In light of this, what do you anticipate will happen to the industry as a whole?