Donny's Programming Blog

Making an SSR Web App with Spring Boot (Part 4)

March 31, 2019

Quick Aside

There is a great resource that describes JPA in great detail. Feel free to dig in here.

Introduction

When we last left off, our application was able to display all of our books, a single book, and accept input from our users to create new books. Let’s add a little more functionality to Books as well as make an Author entity as well as a Genre entity.

Creating an Author

Right now our application is functioning pretty well. But our Author should probably be built out a little further instead of just being a String inside of our Book class. So let’s build that out and include it properly inside of our Book entity. For now we will assume that a Book can only have one Author.

package com.donhamiltoniii.literateoctodollop.models;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Author {

    @Id
    @GeneratedValue
    private Long id;

    private String firstName;
    private String lastName;

    @OneToMany(mappedBy="author")
    private Collection<Book> books;

    public Author() {
    }

    public Author(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Long getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Collection<Book> getBooks() {
        return books;
    }

    @Override
    public String toString() {
        return "Author [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", books=" + books + "]";
    }

}

Let’s make sure to add a corresponding Repository for our Authors

package com.donhamiltoniii.literateoctodollop.repositories;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.donhamiltoniii.literateoctodollop.models.Author;

@Repository
public interface AuthorRepository extends CrudRepository<Author, Long> {

}

Now to update our Book constructor:

package com.donhamiltoniii.literateoctodollop.models;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Book {

    @Id
    @GeneratedValue
    private Long id;
    private String title;

    @ManyToOne
    private Author author;
    private String genre;

    // This is a hook for JPA - We will never call this Constructor
    public Book() {
    }

    public Book(String title, Author author, String genre) {
        this.title = title;
        this.author = author;
        this.genre = genre;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public Author getAuthor() {
        return author;
    }

    public String getGenre() {
        return genre;
    }

    @Override
    public String toString() {
        return "Book [id=" + id + ", title=" + title + ", author=" + author + ", genre=" + genre + "]";
    }

}

So let’s talk about those two new annotations, @OneToMany(mappedBy="author") and @ManyToOne. These annotations come to us from JPA as seen from their imports import javax.persistence.OneToMany; and import javax.persistence.ManyToOne; respectively. These annotations tell JPA how to map these relationships in the relational database it creates for us. Each of these @Entitys we make create a table in that database and the rows in those tables need some way to relate to the rows they correspond to in the other. In other words, we need to describe how we want our database to map the way it relates Authors and Books without having to get into the business of actually writing SQL queries to describe those relationships.

You’ll now notice that we have errors in our Initializer. Let’s go deal with those:

package com.donhamiltoniii.literateoctodollop;

import javax.annotation.Resource;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;

import com.donhamiltoniii.literateoctodollop.models.Author;
import com.donhamiltoniii.literateoctodollop.models.Book;
import com.donhamiltoniii.literateoctodollop.repositories.AuthorRepository;
import com.donhamiltoniii.literateoctodollop.repositories.BookRepository;

@Service
public class Initializer implements CommandLineRunner {

    @Resource
    AuthorRepository authorRepo;

    @Resource
    BookRepository bookRepo;

    @Override
    public void run(String... args) throws Exception {
        Author rodJohnson = authorRepo.save(new Author("Rod", "Johnson"));
        Author craigWalls = authorRepo.save(new Author("Craig", "Walls"));
        Author elisabethRobson = authorRepo.save(new Author("Elisabeth", "Robson"));

        bookRepo.save(new Book("J2EE Development withou EJB", rodJohnson, "Tech"));
        bookRepo.save(new Book("Spring Boot In Action", craigWalls, "Tech"));
        bookRepo.save(new Book("Head First Design Patterns", elisabethRobson, "Tech"));
    }

}

This gets rid of our errors because we are now passing actual Author objects to our Books. But wait. We’re saving the Authors to their respective Repository and then saving that to an Author variable?

We are. Here’s what’s happening. When we save something to a Repository, JPA is doing some figurative magic in the background by saving a new row to our database and then passing back that value. Why this is happening is a bit out of scope for this post but that is why we are able to save authorRepo.save(new Author("Rod", "Johnson")) to a variable.

This is great for us because we now have a database managed version of this entity that we can pass to our Book and save it to the BookRepository!

So our Initializer is all good, but now we have an issue with the addBook method in our BookController. Let’s deal with that:

@PostMapping("/")
public String addBook(String title, Long authorId, String genre) {
    Author author = authorRepo.findById(authorId).get();

    bookRepo.save(new Book(title, author, genre));

    return "redirect:/books";
}

We also have an issue with how we render authors for selection. We’ll need to pass Authors to getAllBooks.

@GetMapping({ "", "/", "/index" })
public String getAllBooks(Model model) {
    model.addAttribute("books", bookRepo.findAll());
    model.addAttribute("authors", authorRepo.findAll());
    return "books/all";
}

That’s a lot of change, but it’s not terribly complicated. So we are now taking a String called authorName since this is no longer an author. Let’s take a look at how our template changes:

Instead of:

<label>Author: <input type="text" name="author"/></label>

We now have:

<label>
  Author: 
  <select name="authorId">
    <option th:each="author : ${authors}" th:value="${author.id}" th:text="|${author.firstName} ${author.lastName}|">Author</option>
  </select>
</label>

We now see our application back up and running! Let’s now add a new controller so that we can interact with our Authors the same way we interact with our books:

package com.donhamiltoniii.literateoctodollop.controllers;

import java.util.Optional;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.donhamiltoniii.literateoctodollop.models.Author;
import com.donhamiltoniii.literateoctodollop.repositories.AuthorRepository;

@Controller
@RequestMapping("/authors")
public class AuthorController {

    @Resource
    AuthorRepository authorRepo;

    @GetMapping({ "", "/", "/index" })
    public String getAllAuthors(Model model) {
        model.addAttribute("authors", authorRepo.findAll());
        return "authors/all";
    }

    @GetMapping("/{id}")
    public String getAuthor(@PathVariable Long id, Model model) throws Exception {
        Optional<Author> authorOptional = authorRepo.findById(id);
        if (authorOptional.isPresent()) {
            model.addAttribute("author", authorOptional.get());
        } else {
            throw new Exception("The requested author doesn't exist");
        }
        return "authors/single";
    }

    @PostMapping("/")
    public String addAuthor(String firstName, String lastName) {
        authorRepo.save(new Author(firstName, lastName));

        return "redirect:/authors";
    }
}

We’ll now need corresponding templates. Let’s make them based off of what we have for our Books:

templates/authors/all.html

<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
  <head>
    <title>Literate Octo Dollop: Authors</title>
  </head>
  <body>
    <header>
      <section>
        <h1>Literate Octo Dollop</h1>
        <h2>Authors</h2>
      </section>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/books">Books</a></li>
          <li><a href="/authors">Authors</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <ul>
        <li th:each="author : ${authors}">
          <h3>
            <a
              th:href="@{|/authors/${author.id}|}"
              th:text="|${author.firstName} ${author.lastName}"
              >Default Name</a
            >
          </h3>
        </li>
      </ul>
      <form action="/authors/add" method="POST">
        <label>First Name: <input type="text" name="firstName"/></label>
        <label>Last Name: <input type="text" name="lastName"/></label>
        <button type="submit">Submit</button>
      </form>
    </main>
    <footer>
      <small>© Literate Octo Dollop 2019</small>
    </footer>
  </body>
</html>

templates/authors/single.html

<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
  <head>
    <title th:text="|${author.firstName} ${author.lastName}|">
      Default Name
    </title>
  </head>
  <body>
    <header>
      <h1 th:text="|${author.firstName} ${author.lastName}|">Default Name</h1>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/books">Books</a></li>
          <li><a href="/authors">Authors</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <ul>
        <li th:each="book : ${author.books}">
          <a href="|/books/${book.id}|" th:text="${book.title}">Title</a>
        </li>
      </ul>
    </main>
    <footer>
      <small>© Literate Octo Dollop 2019</small>
    </footer>
  </body>
</html>

Awesome! Now everything is wor-what?! Wait, if you click on a book from an author page, you get a crazy error?!

Yep. Here’s what’s going on. We are still calling a reference to book.author on line 10 of templates/books/single.html as well as line 23 of templates/books/all.html. The problem is that now we are recursively calling an author from a book. That author has a collection of books. Each of those books has an author, which have books, each of which has an author, each of which have books, each of which…

FOREVER

So we have to be more specific about what it is we’re asking for. Here’s what we can do to change that:

templates/books/single.html

<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
  <head>
    <title th:text="${book.title}">Default Title</title>
  </head>
  <body>
    <header>
      <section>
        <h1 th:text="${book.title}">Default Title</h1>
        <h2>
          <a
            th:href="|/authors/${book.author.id}|"
            th:text="|${book.author.firstName} ${book.author.lastName}|"
            >Default Author</a
          >
        </h2>
      </section>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/books">Books</a></li>
          <li><a href="/authors">Authors</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <small th:text="${book.genre}">Default Genre</small>
      <p>More book info coming soon...</p>
    </main>
    <footer>
      <small>© Literate Octo Dollop 2019</small>
    </footer>
  </body>
</html>

templates/books/all.html

<!DOCTYPE html>
<html xmlns:th="https://thymeleaf.org">
  <head>
    <title>Literate Octo Dollop: Books</title>
  </head>
  <body>
    <header>
      <section>
        <h1>Literate Octo Dollop</h1>
        <h2>Books</h2>
      </section>
      <nav>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="/books">Books</a></li>
          <li><a href="/authors">Authors</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <ul>
        <li th:each="book : ${books}">
          <h3>
            <a th:href="@{|/books/${book.id}|}" th:text="${book.title}"
              >Default Title</a
            >
          </h3>
          <h4>
            <a
              th:href="|/authors/${book.author.id}|"
              th:text="|By: ${book.author.firstName} ${book.author.lastName}|"
              >Default Author</a
            >
          </h4>
          <small th:text="${book.genre}">Default Genre</small>
        </li>
      </ul>
      <form action="/books/add" method="POST">
        <label>Title: <input type="text" name="title"/></label>
        <label>Author: <input type="text" name="authorName"/></label>
        <label>Genre: <input type="text" name="genre"/></label>
        <button type="submit">Submit</button>
      </form>
    </main>
    <footer>
      <small>© Literate Octo Dollop 2019</small>
    </footer>
  </body>
</html>

Ah ha! Now here we find ourselves working again. The big lesson is that once we populate Java objects with other Java objects, JPA is actually referencing relationships inside of a database instead of the regular POJOs we’re used to. So instead of getting the usual .toString() output we would expect, we’re actually getting references to other rows in a database. This is where the recursion comes from.

Also of note is the fact that we can reference properties of an Author from a Book. (i.e. book.author.firstName) This kind of nested dot notation can go on forever and ever so long as the relationships are there. But they shouldn’t…

Keep referential relationships as simple as possible. Don’t let that stop you from doing it though. Just make sure they don’t go too deep.

Now we just have to clean up Genres and style! See you in Part 5


Don Hamilton III

Written by Don Hamilton III who lives and works in Columbus, OH teaching folx to program at We Can Code IT. You should follow him on Twitter