Now we can make edits to our Thymeleaf template and create input fields for people to write comments:
<td> <ul> <li th:each="comment : ${image.comments}" th:text="${comment.comment}"></li> </ul> </td> <td> <form th:method="post" th:action="@{'/comments'}"> <input name="comment" value="" type="text" /> <input name="imageId" th:value="${image.id}"
type="hidden" /> <input type="submit" /> </form> </td>
The section of our preceding template where each row is rendered can be explained as follows:
- There is a new column containing an HTML unordered list to display each comment
- The unordered list consists of an HTML line item for each comment via Thymeleaf's th:each construct
- There is also a new column containing an HTML form to post a new comment
- The form contains an HTML text input for the comment itself
- The form also contains a hidden HTML element specifying the ID of the image that the comment will be associated with
To support this, we need to update HomeController as follows:
private final ImageService imageService; private final CommentReaderRepository repository; public HomeController(ImageService imageService, CommentReaderRepository repository) { this.imageService = imageService; this.repository = repository; }
We have updated the class definition as follows:
- A new repository field is created for CommentReaderRepository (which we'll define further ahead in the chapter)
- This field is initialized by constructor injection
We need to look up the comments. To do that, we need a Spring Data repository that can read comments. And reading comments is ALL this repository needs to do at this stage of our social media app.
Let's take this new repository and use it inside the Spring WebFlux handler for GET /, like this:
@GetMapping("/") public Mono<String> index(Model model) { model.addAttribute("images", imageService .findAllImages() .flatMap(image -> Mono.just(image) .zipWith(repository.findByImageId( image.getId()).collectList())) .map(imageAndComments -> new HashMap<String, Object>(){{ put("id", imageAndComments.getT1().getId()); put("name", imageAndComments.getT1().getName()); put("comments", imageAndComments.getT2()); }})
); model.addAttribute("extra", "DevTools can also detect code changes too"); return Mono.just("index"); }
This last code contains a slight adjustment to the model's images attribute:
- The code takes the Flux returned from our ImageService.findAll() method and flatMaps each entry from an Image into a call to find related comments.
- repository.findByImageId(image.getId()).collectList() actually fetches all Comment objects related to a given Image, but turns it into Mono<List<Comment>>. This waits for all of the entries to arrive and bundles them into a single object.
- The collection of comments and it's related image are bundled together via Mono.zipWith(Mono), creating a tuple-2 or a pair. (This is the way to gather multiple bits of data and pass them on to the next step of any Reactor flow. Reactor has additional tuple types all the way up to Tuple8.)
- After flatMapping Flux<Image> into Flux<Tuple2<Image,List<Comment>>>, we then map each entry into a classic Java Map to service our Thymeleaf template.
- Reactor's Tuple2 has a strongly typed getT1() and getT2(), with T1 being the Image and T2 being the list of comments, which is suitable for our needs since it's just a temporary construct used to assemble details for the web template.
- The image's id and name attributes are copied into the target map from T1.
- The comments attribute of our map is populated with the complete List<Comment> extracted from T2.
As we continue working with Reactor types, these sorts of flows are, hopefully, becoming familiar. Having an IDE that offers code completion is a key asset when putting flows like this. And the more we work with these types of transformations the easier they become.
To round out our feature of reading comments, we need to define CommentReaderRepository as follows:
public interface CommentReaderRepository extends Repository<Comment, String> { Flux<Comment> findByImageId(String imageId); }
The preceding code can be described as follows:
- It's a declarative interface, similar to how we created ImageRepository earlier in this book.
- It extends Spring Data Commons' Repository interface, which contains no operations. We are left to define them all. This lets us create a read-only repository.
- It has a findByImageId(String imageId) method that returns a Flux of Comment objects.
This repository gives us a read-only readout on comments. This is handy because it lets us fetch comments and does not accidentally let people write through it. Instead, we intend to implement something different further in this chapter.
Our CommentReaderRepository needs one thing: a Comment domain object:
package com.greglturnquist.learningspringboot.images; import lombok.Data; import org.springframework.data.annotation.Id; @Data public class Comment { @Id private String id; private String imageId; private String comment; }
This preceding domain object contains the following:
- The @Data annotation tells Lombok to generate getters, setters, toString(), equals(), and hashCode() methods
- The id field is marked with Spring Data Commons' @Id annotation so we know it's the key for mapping objects
- The imageId field is meant to hold an Image.id field, linking comments to images
- The comment field is the place to store an actual comment