Create a Data Marvel — Part 5: Writing the Domain Classes
*Update*: All parts of this series are published and related content available.
Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7, Part 8, Part 9, Part 10
Completed Github project (+related content)
Picking up from where we left off before the holiday season in Part 4 of this Marvel tech series, this post will show the code for our Marvel comic entities. As you may recall, our data model is structured with 6 entities, as shown in the image below.
At the end of the last post, we created the Spring Data Neo4j application using the start.spring.io site, opened the project in IntelliJ, and delved into the project structure. Now, we are ready to write some code, starting with our domain classes!
Domain classes
Just as we did when we loaded the data from the Marvel API, we will start with the domain entity classes. They are a smaller and simpler entrypoint to the rest, and the values are more familiar to even non-comic-book fans.
The Character class
We will create our domain class for characters in IntelliJ by right-mouse clicking on the demo
folder (filepath: src->main->java->com->example->demo), mousing over the New
option, and choosing the Java Class
option.
Fill in the name you want for your class (in this case, Character
), click OK
, and IntelliJ generates some bare-bones structure for us, so we can take it from there.
One brief note on class names before we move on. Take care when naming your classes that they do not conflict with language keywords and types. For instance, the class Character
caused some confusion with later code because there is also a Java character type. We noted the problem and easily avoided it, but it is something to keep in mind when you’re in full “domain definition mode”. If I would have given this a bit more thought before our project, I probably would have chosen something like ‘ComicCharacter’ instead.
The generated code includes just an outline of our class structure. We have the package path defined at the top for us and the (empty) class declaration beneath that.
Now we start adding some definition for our Marvel use case. The full code for this class is below. We will explain the syntax in the following paragraphs.
package com.example.demo;import …@Data
@NoArgsConstructor
@RequiredArgsConstructor
@NodeEntity
public class Character {
@Id
@GeneratedValue
private Long neoId; @NonNull
private Long id; @NonNull
private String name, description, resourceURI, thumbnail;
}
We first want to add annotations for Lombok to shortcut some boilerplate like our getters/setters and constructors. The @Data
annotation handles much of that boilerplate for the standard POJOs, supplying the getters, setters, and equals+hashcode+toString methods. Annotations @NoArgsConstructor
and @RequiredArgsConstructor
generate a constructor with no arguments and a constructor with one argument for each field required to be @NonNull
, respectively. We also need to annotate with @NodeEntity
to let Spring Data Neo4j (SDN) know that this is a domain class for a node in our graph database.
Within the class declaration, we start defining our member variables/fields for the database and the Marvel data. The first id field (neoId
) is the internal id that Neo4j assigns to each of its nodes and relationships when it is stored. It is rarely used or referenced, but it is part of the entity in the database, so we assign the @Id
and @GeneratedValue
annotations to it since we do not handle the values.
The next id field (id
) is the identifier from the Marvel API. This value allows us to retrieve any additional info from the API or reference an entity without translating for custom ids. We add the @NonNull
annotation to ensure that the field always has a value and will not accept null. The last fields are String values from the API that we felt were the most meaningful to our application. The @NonNull
annotation also applies here to ensure values are not missing.
The repository interface
Next, we will need to create a repository for Character
objects for the data access layer to the database. Spring Data is designed with the flexibility to interface with different kinds of data stores, so the interaction with Neo4j should be similar to many of Spring Data’s other interface projects.
Just as we did to create the Character
class, we can right-click on the demo
folder in the project structure, choose New
, then pick Java Class
. When the box appears to type in the name, we will use CharacterRepo
, then choose Interface
as the Kind, rather than the Class
type.
Click OK
, and the interface should appear with some skeleton code generated. Again, the package path is defined for us, as well as the interface declaration.
The only thing we need to add here is to make our interface extend the Neo4jRepository
interface, as shown below.
package com.example.demo;import org.springframework.data.neo4j.repository.Neo4jRepository;public interface CharacterRepo
extends Neo4jRepository<Character, Long> {}
The Neo4jRepository
interface provides Neo4j-specific implementation details on top of several extended Spring repositories as the foundation. Neo4jRepository
requires two types to be specified — our class type and its id type. Once we add our Character
and Long
values here, we have finished our required code for this interface.
* Note: If we wanted to run some queries to return
Character
data specifically or view a character’s relationships, we could define methods and other details in this interface. However, for simplicity, we chose to define all those methods through one entity — theComicIssue
. Since we decided in an earlier step that our project only cared about data as it was related to theComicIssue
and made it the center of our graph data model, it made sense to focus our methods there.
The controller class
Finally, we need one more piece to complete our Character
classes. We have our entity defined and our data access interface written. The controller acts as the messenger between the data layer and the user interface to accept requests from the user and send back responses. This is where the code for logic and data manipulation is typically placed. It coordinates different responses based on the kind of input it receives.
For our character controller, we add a new Java class like we did with the domain class (right-click on demo
folder, choose New->Java Class
). Type in the name CharacterController
, keep the Kind value as Class
, and click OK
.
We will need to add a couple of annotations, as well as some methods to this class. The completed class code is below, and the explanation will follow.
package com.example.demo;import ….@RestController
@RequestMapping(“/characters”)
public class CharacterController {
private final CharacterRepo repo; public CharacterController(CharacterRepo repo) {
this.repo = repo;
} @GetMapping
public Iterable<Character> getAllCharacters() {
return repo.findAll();
}
}
The @RestController
annotation combines the @Controller
and @ResponseBody
annotations, eliminating the need to put @ResponseBody
on each request handling method. Our @RequestMapping
annotation is used to map requests to a controller method for a certain path (in this case, /characters
).
Within the class declaration, we begin by creating a local member variable for our CharacterRepo
, and the next line, our constructor, injects that repository into our controller so that we can access the data layer. The @GetMapping
annotation tells us this will be a GET method and precedes the method declaration for getAllCharacters()
that accesses the repository, executes the provided findAll()
method, and returns multiple Character
entities (Iterable of type Character
).
Wrapping up
Fortunately, this is all the code we need at this point to define our Character
-related classes. This would be enough to hit the /characters
endpoint (using the curl
command or something similar) and return all the Character
entities in our database.
What I Learned
I had built similar applications with Spring in the past, so this was pretty straightforward and consistent with my past experiences. Spring handles much of the boilerplate and nuances, so I could focus on the data and representation, rather than the setup code to get from the database to the user. It didn’t take too much additional research or effort to integrate with Neo4j. Helpful documentation and examples also shortened the learning curve, as well.
As always, the highlights of my learning experience in this step are listed below. :)
- The built-in capabilities of the Spring ecosystem help reduce boilerplate code and nearly eliminate the most rudimentary (and often boring) parts of the codebase.
- Spring Data provides a highly-flexible and simple interface to plug into a variety of data sources. It is designed to work seamlessly, whether you are working with relational, NoSQL, or graph databases. Consistent syntax helped me focus on the pieces of the code that were specific to Neo4j and the graph data model.
- Coding the
Character
-related classes first gave me an easier on-ramp because the code was simple and nearly mirrored previous applications with relational backends. Starting with a small piece of the application and not focusing (yet!) on the relationship component of the graph model allowed me to easily consume and better internalize any differences in smaller chunks.
Next Steps
Even with the little bit of code that we wrote today, we can now retrieve Character
entities from Neo4j and display them (even if it is only in JSON format). :) In upcoming posts, we will continue adding pieces of code until we can retrieve and review all of our entities through a prettier interface!
Resources
- Follow the duo on Twitter to see what’s coming: @mkheck and @jmhreif
- Download Neo4j
- Spring Data Neo4j docs
- Spring Data Neo4j Guide
- Project Lombok docs
- Previous parts of this blog series: Part 1, Part 2, Part 3, Part 4