Create a Data Marvel — Part 8: Controlling and Servicing our Comic Endpoints

Jennifer Reif
Feb 14 · 10 min read

We will continue developing our classes for passing the objects, along with any entities related to them. This post will cover the controller and service classes for handling requests and shaping any results. As before, previous blog posts (Parts 1–7) have built upon information and code leading up to this. For review of code for the domain, repository, and controller classes, you can check out Part 7 of this series.

Let’s go ahead and dive in!

Controller vs. Service classes

As we have previously discussed, the controller class is the intermediary 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.

One minor distinction for our is that we will use the controller to outline and map our endpoints, then use a service class to handle logic and formatting of the data for us. This type of architecture (creating an external and internal API) is mostly personal preference for division of labor and easier-to-read code. Hopefully, you will see why shortly. If we included all the upcoming code in one class, the waters could get murky very quickly. Creating a separate ‘service layer’ class adds passthroughs to a couple of operations, but it also keeps our controller class more readable and maintainable by placing the rather substantial data assembly/formatting in its own class, along with those remaining data access (repository) calls.

The controller class

package com.thehecklers.sdnmarvelcomics.comicissue;import …@AllArgsConstructor
@Controller
@RequestMapping(“/comicissues”)
public class ComicIssueController {
private final ComicIssueService issueService; @GetMapping(“/findbyname”)
@ResponseBody
public ComicIssue findByName(@RequestParam String name) {
return issueService.findByName(name);
}
@GetMapping(“/findbynamelike”)
@ResponseBody
public Iterable<ComicIssue> findByNameLike(@RequestParam String name) {
return issueService.findByNameLike(name);
}
@GetMapping(“/buildgraph”)
@ResponseBody
public Map<String, Object> buildgraph(@RequestParam(required = false) Integer limit) {
return issueService.graph(limit == null ? 100 : limit);
}
@GetMapping(“/graph”)
public String graph(@RequestParam(required = false) Integer limit, Model model) {
model.addAttribute(buildgraph(limit));
return “issuesgraph”;
}
}

In the code above, we start our class with an Lombok annotation to create a constructor with one parameter for each field in the class, in this case, the bean we want Spring Boot to infect/autowire for our use. The next annotation for is different from our other entity controllers because we do not want to return JSON for every method in the . Remember that the annotation we have been using is a combination of and , the latter of which defaults to a JSON-format return. Instead, we want to specify which methods should return JSON and which could return an html page (through a template engine). Finally, our last annotation sets up the general endpoint for all of our calls (“”).

Our class body starts by injecting our service class, then defines our methods. The and methods are first. These are pretty straightforward, as we need to set up the endpoints for each using the annotation and then annotate with to notify Spring that these methods will simply return JSON as the response.

The method is expected to only return a single result, and we need to pass in the searched as a parameter. The method will call the service class method with the parameter as the argument.

The is handling a fuzzy search, so it could return several possible matching comic issues. It will expect an of in return and also pass the searched as a parameter. Just like our method, this method calls the service class method and passes the to it.

Our third method is . This method also uses the same annotations as our previous methods for mapping the endpoint and defining our JSON return. However, this method definition and call is a bit different. The code is repeated below for reference alongside the explanation.

@GetMapping(“/buildgraph”)
@ResponseBody
public Map<String, Object> buildgraph(@RequestParam(required = false) Integer limit) {
return issueService.graph(limit == null ? 100 : limit);
}

First, we are expecting a to return from this method because it formats our data to be displayed for our visualization. D3 expects a map of nodes and relationships, so that is what must return here. Getting the data into that map format is what our service class will do. We will look at that code in a bit.

We are also passing an integer parameter called to this method. This will allow us to specify how much of the graph we want to return in our visualization. If we have a very large graph, rendering a visualization in our webpage could overload the page or cause performance impact. Depending on user preferences, we can pass in an arbitrary number to limit the volume of the return.

Just like our previous methods, will call the service class method, but it passes something more interesting in the parameter (). This bit of code is a ternary operator that checks if is equal to . If it is, it uses the default value of 100. Otherwise, it uses the passed-in value. If the user forgets to pass in a value (or doesn’t need a specific amount returned), this ensures that the number of nodes and relationships returned to the visualization will not exceed 100. It will avoid an accidental browser overload or poor performance.

The last method is . This code is what will call the template engine that searches for an html page and loads it to the browser, if it finds one. We have restated the method code below to use as reference with the following explanation.

@GetMapping(“/graph”)
public String graph(@RequestParam(required = false) Integer limit, Model model) {
model.addAttribute(buildgraph(limit));
return “issuesgraph”;
}

Our usual annotation sets up the endpoint for calling this method, and we can then define it. We are expecting a return type of because our template engine will use the returned string to search for an html file with that same name to serve up to our browser. Our parameters for the method include the integer (again, for the visualization) and a object that can supply attributes for rendering pages. In our case, we call the method on the and pass our method with the parameter to convert our data to the expected visualization format, then add that as an attribute to the model for rendering. Finally, the method returns the String, which will cause the template engine to look for an file to match it. We will show the code for the html file later.

Now that we have our class completed and all of our endpoints and methods defined, we can move on to our service class and show how we handle additional logic there.

The service class

This class has quite a bit of code, so we will break up the blocks into a couple of chunks for easier consumption. Let us start with the annotations, class declaration, and a couple of our simpler methods.

package com.thehecklers.sdnmarvelcomics.comicissue;import …@AllArgsConstructor
@Service
public class ComicIssueService {
private final ComicIssueRepo issueRepo; @Transactional(readOnly = true)
public Iterable<ComicIssue> findAllComicIssues() {
return issueRepo.findAll();
}
@Transactional(readOnly = true)
public ComicIssue findByName(String name) {
return issueRepo.findByName(name);
}
@Transactional(readOnly = true)
public Iterable<ComicIssue> findByNameLike(String name) {
return issueRepo.findByNameLike(name);
}
@Transactional(readOnly = true)
public Map<String, Object> graph(Integer limit) {
return toD3Format(issueRepo.graph(limit));
}
...
}

In the above segment, we use the to have Lombok include a constructor with all member variables as parameters. The is an alias for , which tells Spring to create a bean and make it available to the application for use. We use instead of as an indication that this is a stateless interface (a la DDD) that serves as a foundation for our internal API.

Within our class body, we inject our into our service, allowing the service to call the repository methods. Next, we have 4 methods using the annotation. This annotation sets boundaries for how much will run in a single transaction. We could run multiple calls to Neo4j within a transaction, but we want to separate each method into its own unit of work. We also specify that data in this method should be read-only and should not have any write transactions.

Our first method is . This method simply calls the repo and executes the built-in method to return all in the database, using the same pattern from our other entity controller classes. The next two methods are for and . These methods are straightforward and call the respective method from the and both pass in the value as a parameter.

The last method in this block is for . It expects a returned with the nodes and relationships for the visualization and expects the parameter. The return call is the most interesting piece. First, we call the and associated method and pass in our value. However, in order to get our data into the format the D3 visualization expects (), we also need to transform and manipulate those results. That is what the method will do, and we will look at that code shortly. We wrap the call within the method, and we should end up with our structure for displaying the graph visualization on our webpage.

Now we can look at what the method does to transform the data into a .

private Map<String, Object> toD3Format(Iterable<ComicIssue> issues) {List<Map<String, Object>> nodes = new ArrayList<>();
List<Map<String, Object>> rels = new ArrayList<>();
int i = 0;
Iterator<ComicIssue> result = issues.iterator();
while (result.hasNext()) {
ComicIssue issue = result.next();
nodes.add(map(“name”, issue.getName(), “label”, “issue”));
int target = i;
i++;
for (Character character : issue.getCharacters()) {
Map<String, Object> comicChar = map(“name”, character.getName(), “label”, “character”);
int source = nodes.indexOf(comicChar);
if (source == -1) {
nodes.add(comicChar);
source = i++;
}
rels.add(map(“source”, source, “target”, target));
}
for (Creator creator : issue.getCreators()) {
//same code block as for Characters
}
for (Series series : issue.getSeries()) {
...
}
for (Story story : issue.getStories()) {
...
}
for (Event event : issue.getEvents()) {
...
}
}
return map(“nodes”, nodes, “links”, rels);
}
private Map<String, Object> map(String key1, Object value1, String key2, Object value2) {
Map<String, Object> result = new HashMap<String, Object>(2);
result.put(key1, value1);
result.put(key2, value2);
return result;
}

I have condensed the code a bit to make it easier to read, but the same code block within the loop is the same for each entity. The method will return a type, which is what d3 needs to render our visualization. Our is passed into the method, so we can iterate over the list and separate our objects into nodes and relationships.

We begin the method body by creating two new — one for nodes and one for relationships (). Then, we set up an iterator to loop through each object and grab the next comic (). The next line adds two values to our nodes map for the issue and the on the issue (which is ). It does this by passing the and to the method that creates a with two keys and two values, then adding the resultant to the list of mapped nodes.

Now, for each entity, we have sub-entities for each , , , , or related to that comic. We will need to loop through each of those lists and map the start and end node of the relationship, as well. This bit of code is repeated for clarity.

int target = i;
i++;
for (Character character : issue.getCharacters()) {
Map<String, Object> comicChar = map(“name”, character.getName(), “label”, “character”);
int source = nodes.indexOf(comicChar);
if (source == -1) {
nodes.add(comicChar);
source = i++;
}
rels.add(map(“source”, source, “target”, target));
}

We start with our node, which is the current node that is on one side of the relationship. For each related to the particular , we call the method to create another from the property and of each . Then, we pull the the index of that entity in the list and assign that to our source variable (). At the end of the block, we also call the method to format the and values into our expected HashMap and add them to the relationship map. Now, each of the ArrayLists we initially set up ( and ) will contain a list of indexes for the related objects and their connections.

We loop through the same logic for the rest of our entities (Creator, Event, Series, Story) that we did for Character, adding each of the nodes and relationships for these objects to the arrays.

Now we have a functional application that should return ComicIssues from our endpoints, along with the related Character, Creator, Event, Series, and Story entities! We can tap some of the endpoints to ensure everything is working. Our last step will be to create our html page for viewing the data in a webpage and rendering our visualization!

What I Learned

This step was quite involved, as we starting knitting together all of the entities to return with a particular . The toughest part was probably setting up the endpoints, template engine structure, and d3 formatting for the visualization. I’ve outlined my challenges in the points below.

  1. D3 looks complicated at first, but it actually expects a straightforward format of maps. I was able to use much of the existing code from some of our sample projects, and then modify it for our use case. The complexity was in the number of entities that we have for our Marvel Comics data.
  2. The separation of Controller and Service was very confusing at first. I wasn’t sure why it was needed or what purpose it served. However, it became clear that I didn’t want to include all of the code from both classes in a single file (would’ve ended up around 140 lines of code)! Separating endpoints from the formatting and mapping logic of the d3 code made it much easier for me to sift through code and change things. Separating concerns into two classes (external-facing controller API and internal service API) made for cleaner code divisions and an API that was easier to reason about.

Next Steps

We are so close to have a full-stack application completed. Our next post will place the final (pretty!) puzzle piece into place to add the html page and allow us to interact with the data in a visual and friendly manner. Hope to see you then!

Resources

Neo4j Developer Blog

Developer Content around Graph Databases, Neo4j, Cypher, Data Science, Graph Analytics, GraphQL and more.

Jennifer Reif

Written by

Jennifer Reif is an avid developer and problem-solver. She enjoys learning new technologies, sometimes on a daily basis! Her Twitter handle is @JMHReif.

Neo4j Developer Blog

Developer Content around Graph Databases, Neo4j, Cypher, Data Science, Graph Analytics, GraphQL and more.