Bookshelfアプリを作成する

新規プロジェクトを作成する。
プロジェクトの種類は、Springスターター・プロジェクトを選択する。

プロジェクト名を「bookshelf」にして次へをクリック。

依存関係は、以下の項目を選択する。

  • HyperSQL Database
  • Spring Data JPA
  • Spring Web
  • Thymeleaf

「完了」でbookshelfプロジェクトができる。

pom.xml を開いて、8行目のバージョンを2.2.1に修正する。

		<version>2.2.1.RELEASE</version>

[実行]-[Spring Boot アプリケーション]を選択するとサーバーが起動する。

http://localhost:8080 にアクセスすると、Whitelabel Error Page が表示される。

IndexControler.java を作成する。

package jp.kpc;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {

	@RequestMapping("/")
	public String index() {
		return "index";
	}

}

example プロジェクトの index.html をコピーして、src/main/resources の templates に貼り付ける。

SpringBootアプリケーションを再起動すると、「最初のページ」が表示される。

application.properties にデータベース接続設定を追加する。

spring.datasource.url=jdbc:hsqldb:hsql://localhost/bookshelf
spring.datasource.username=SA
spring.datasource.password=
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.jpa.hibernate.ddl-auto=update

hsqldb.bat に bookshelf インスタンスを追加する。

cd data
java -classpath ../lib/hsqldb.jar org.hsqldb.server.Server ^
--database.0 db/shindan --dbname.0 shindan ^
--database.1 db/bookshelf --dbname.1 bookshelf

index.html に、新しい本棚を追加するためのフォームを用意する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚</title>
</head>
<body>

<h1>Bookshelf list</h1>

<hr />

<div>新しい本棚</div>
<form action="/addshelf" method="post">
	<div>なまえ</div>
	<div><input type="text" name="name" /></div>
	<div><input id="submit" type="submit" value="+追加" /></div>
</form>


</body>
</html>

IndexController.java に /addshelf に対する POST を受け取る用意をする。POSTを受け取ったあとは、URL=”/” にリダイレクトする。

package jp.kpc;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class IndexController {

	@RequestMapping("/")
	public String index() {
		return "index";
	}

	@RequestMapping(value="/addshelf", method=RequestMethod.POST)
	public ModelAndView formPost(ModelAndView mav,
			@RequestParam("name") String name) {
		return new ModelAndView("redirect:/");
	}
}

本棚に対応する Bookshelf クラスを作成する。

package jp.kpc;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Entity
public class Bookshelf {
	@Id
	@GeneratedValue
	@Column
	@NotNull
	private long id;

	@Column
	@NotEmpty
	private String name;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}

新規インターフェースを作成する。
BookshelfRepository.java

package jp.kpc;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookshelfRepository extends JpaRepository<Bookshelf, Long> {

}

IndexController.java で、リクエストパラメータで渡された名前の本棚を保存する処理を追加する。

package jp.kpc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class IndexController {

	@Autowired
	private BookshelfRepository repository;

	@RequestMapping("/")
	public String index() {
		return "index";
	}

	@RequestMapping(value="/addshelf", method=RequestMethod.POST)
	public ModelAndView formPost(ModelAndView mav,
			@RequestParam("name") String name) {
		Bookshelf bookshelf = new Bookshelf();
		bookshelf.setName(name);
		repository.saveAndFlush(bookshelf);
		return new ModelAndView("redirect:/");
	}
}

トップページにアクセスがあったときに、本棚リストをテンプレートに渡す。

IndexController.java

@Controller
public class IndexController {

	@Autowired
	private BookshelfRepository repository;

	@RequestMapping("/")
	public ModelAndView index(ModelAndView mav) {
		mav.setViewName("index");
		List<Bookshelf> list = repository.findAll();
		mav.addObject("list", list);
		return mav;
	}

テンプレートで本棚のリストを表示する。

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚</title>
</head>
<body>

<h1>Bookshelf list</h1>
<table>
	<tr th:each="bs : ${list}">
		<td th:text="${bs.name}"></td>
	</tr>
</table>

<hr />

<div>新しい本棚</div>
<form action="/addshelf" method="post">
	<div>なまえ</div>
	<div><input type="text" name="name" /></div>
	<div><input id="submit" type="submit" value="+追加" /></div>
</form>


</body>
</html>

Bookクラスを作成する。

package jp.kpc;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Entity
public class Book {
	@Id
	@GeneratedValue
	@Column
	@NotNull
	private long id;

	@Column
	@NotEmpty
	private String title;

	@Column
	@NotEmpty
	private String author;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}


}

BookController.java

package jp.kpc;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class BookController {
	@RequestMapping("/books")
	public ModelAndView index(ModelAndView mav) {
		mav.setViewName("books");
		return mav;
	}

}

books.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Book - 本のリスト</title>
</head>
<body>

<h1>Book list</h1>
<table>
	<tr th:each="book : ${list}">
		<td th:text="${book.title}"></td>
		<td th:text="${book.author}"></td>
	</tr>
</table>

<hr />

<div>新しい本</div>
<form action="/addbook" method="post">
	<div>タイトル: <input type="text" name="title" /></div>
	<div>著者: <input type="text" name="author" /></div>
	<div><input id="submit" type="submit" value="+追加" /></div>
</form>


</body>
</html>

BookRepository.java

package jp.kpc;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {

}

リポジトリを用意したので、それをBookControllerで使う。

package jp.kpc;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class BookController {
	@Autowired
	private BookRepository repository;

	@RequestMapping("/books")
	public ModelAndView index(ModelAndView mav) {
		mav.setViewName("books");
		List<Book> list = repository.findAll();
		mav.addObject("list", list);
		return mav;
	}

	@RequestMapping(value="/addbook", method=RequestMethod.POST)
	public ModelAndView formPost(ModelAndView mav,
			@RequestParam("title") String title,
			@RequestParam("author") String author) {
		Book book = new Book();
		book.setTitle(title);
		book.setAuthor(author);
		repository.saveAndFlush(book);
		return new ModelAndView("redirect:/books");
	}
}

本と本棚の関連付けを作る。

Bookshelf.java

package jp.kpc;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Entity
public class Bookshelf {
	@Id
	@GeneratedValue
	@Column
	@NotNull
	private long id;

	@Column
	@NotEmpty
	private String name;

	@OneToMany
	private List<Book> books;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

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

	public void setBooks(List<Book> books) {
		this.books = books;
	}

}

Book.java

package jp.kpc;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Entity
public class Book {
	@Id
	@GeneratedValue
	@Column
	@NotNull
	private long id;

	@Column
	@NotEmpty
	private String title;

	@Column
	@NotEmpty
	private String author;

	@ManyToOne
	private Bookshelf bookshelf;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}

	public Bookshelf getBookshelf() {
		return bookshelf;
	}

	public void setBookshelf(Bookshelf bookshelf) {
		this.bookshelf = bookshelf;
	}
}

本を入れる本棚を指定するための画面を作る。
そのために、/book/{id} (idはBookのid)というURLを用意する。

package jp.kpc;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class BookController {
	@Autowired
	private BookRepository repository;

	@RequestMapping("/books")
	public ModelAndView index(ModelAndView mav) {
		mav.setViewName("books");
		List<Book> list = repository.findAll();
		mav.addObject("list", list);
		return mav;
	}

	@RequestMapping(value="/addbook", method=RequestMethod.POST)
	public ModelAndView formPost(ModelAndView mav,
			@RequestParam("title") String title,
			@RequestParam("author") String author) {
		Book book = new Book();
		book.setTitle(title);
		book.setAuthor(author);
		repository.saveAndFlush(book);
		return new ModelAndView("redirect:/books");
	}


	@RequestMapping(value = "/book/{id}", method = RequestMethod.GET)
	public ModelAndView book(ModelAndView mav,
			@PathVariable long id) {
		mav.setViewName("book");
		Optional<Book> data = repository.findById(id);
		mav.addObject("book", data.get());
		return mav;
	}

}

book.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Book - 本を入れる本棚を指定する</title>
</head>
<body>

<h1>Book</h1>
<table>
	<tr>
		<td th:text="${book.title}"></td>
		<td th:text="${book.author}"></td>
	</tr>
</table>

<hr />

<div>どの本棚に入れますか?</div>
<form action="/book" method="post">
	<input type="hidden" name="bookId" th:value="${book.id}" />
	<div>本棚ID: <input type="text" name="bookshelfId" /></div>
	<div><input id="submit" type="submit" value="本棚に入れる" /></div>
</form>


</body>
</html>

本と本棚の関連付けを保存する。

BookController.java

	@Autowired
	private BookshelfRepository bookshelfRepository;
	@RequestMapping(value="/book", method=RequestMethod.POST)
	public ModelAndView save(ModelAndView mav,
			@RequestParam("bookId") long bookId,
			@RequestParam("bookshelfId") long bookshelfId) {
		Optional<Book> data = repository.findById(bookId);
		Book book = data.get();
		Optional<Bookshelf> bsData = bookshelfRepository.findById(bookshelfId);
		Bookshelf bookshelf = bsData.get();
		book.setBookshelf(bookshelf);
		repository.saveAndFlush(book);
		return new ModelAndView("redirect:/books");
	}

books.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Book - 本のリスト</title>
</head>
<body>

<h1>Book list</h1>
<table>
	<tr>
		<th>ID</th><th>タイトル</th><th>著者</th><th>本棚</th>
	</tr>
	<tr th:each="book : ${list}">
		<td th:text="${book.id}"></td>
		<td th:text="${book.title}"></td>
		<td th:text="${book.author}"></td>
		<td th:if="${book.bookshelf != null}" th:text="${book.bookshelf.name}"></td>
		<td th:if="${book.bookshelf == null}" th:text="本棚に入れてません"></td>
	</tr>
</table>

<hr />

<div>新しい本</div>
<form action="/addbook" method="post">
	<div>タイトル: <input type="text" name="title" /></div>
	<div>著者: <input type="text" name="author" /></div>
	<div><input id="submit" type="submit" value="+追加" /></div>
</form>


</body>
</html>

本のタイトルをクリックしたら、/book/{id} に飛べるようにする。

<h1>Book list</h1>
<table>
	<tr>
		<th>ID</th><th>タイトル</th><th>著者</th><th>本棚</th>
	</tr>
	<tr th:each="book : ${list}">
		<td th:text="${book.id}"></td>
		<td><a th:href="@{'/book/' + ${book.id}}" th:text="${book.title}"></a></td>
		<td th:text="${book.author}"></td>
		<td th:if="${book.bookshelf != null}" th:text="${book.bookshelf.name}"></td>
		<td th:if="${book.bookshelf == null}" th:text="本棚に入れてません"></td>
	</tr>
</table>

本棚に入っている本のリストを表示する画面を作る。

/bookshelf/{id} で本棚内の本のリストを表示する。

bookshelf.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚に入っている本のリスト</title>
</head>
<body>

<h1>Bookshelf - 本棚に入っている本のリスト</h1>

<h3 th:text="${bookshelf.name}"></h3>

<ul th:each="book : ${bookshelf.books}">
	<li th:text="${book.title + ' - ' + book.author}"></li>
</ul>


</body>
</html>

Bookshelf.java

@Entity
public class Bookshelf {
	@Id
	@GeneratedValue
	@Column
	@NotNull
	private long id;

	@Column
	@NotEmpty
	private String name;

	@OneToMany(mappedBy = "bookshelf")
	private List<Book> books;

IndexController.java

	@RequestMapping(value = "/bookshelf/{id}", method = RequestMethod.GET)
	public ModelAndView bookshelf(ModelAndView mav,
			@PathVariable long id) {
		mav.setViewName("bookshelf");
		Optional<Bookshelf> data = repository.findById(id);
		mav.addObject("bookshelf", data.get());
		return mav;
	}

index.htmlのテーブルに列を追加して、本棚のIDを表示する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚</title>
</head>
<body>

<h1>Bookshelf list</h1>
<table>
	<tr th:each="bs : ${list}">
		<td th:text="${bs.id}"></td>
		<td th:text="${bs.name}"></td>
	</tr>
</table>

<hr />

<div>新しい本棚</div>
<form action="/addshelf" method="post">
	<div>なまえ</div>
	<div><input type="text" name="name" /></div>
	<div><input id="submit" type="submit" value="+追加" /></div>
</form>


</body>
</html>

本棚の名前をクリックしたら、本棚の内容を表示するページに移動できるようにする。
本のリストページ(/books)へのリンクも追加する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚</title>
</head>
<body>

<h1>Bookshelf list</h1>
<table>
	<tr th:each="bs : ${list}">
		<td th:text="${bs.id}"></td>
		<td><a th:href="@{'/bookshelf/' + ${bs.id}}" th:text="${bs.name}"></a></td>
	</tr>
</table>

<hr />

<div>新しい本棚</div>
<form action="/addshelf" method="post">
	<div>なまえ</div>
	<div><input type="text" name="name" /></div>
	<div><input id="submit" type="submit" value="+追加" /></div>
</form>

<a href="/books">本のリスト</a>


</body>
</html>

books.html、book.html、bookshelf.html に、トップページへのリンクを追加する。

books.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Book - 本のリスト</title>
</head>
<body>

<h1>Book list</h1>

<a href="/">トップ</a>

<table>
:

book.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Book - 本を入れる本棚を指定する</title>
</head>
<body>

<h1>Book</h1>

<a href="/">トップ</a>

<table>
:

bookshelf.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚に入っている本のリスト</title>
</head>
<body>

<h1>Bookshelf - 本棚に入っている本のリスト</h1>

<a href="/">トップ</a>

<h3 th:text="${bookshelf.name}"></h3>
:

本棚ページ内に表示した本のリストで、タイトルをクリックすると本のページに移動できるようにする。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Bookshelf - 本棚に入っている本のリスト</title>
</head>
<body>

<h1>Bookshelf - 本棚に入っている本のリスト</h1>

<a href="/">トップ</a>

<h3 th:text="${bookshelf.name}"></h3>

<ul th:each="book : ${bookshelf.books}">
	<li>
		<a th:href="@{'/book/' + ${book.id}}" th:text="${book.title}"></a>
		<span th:text="${' - ' + book.author}"></span>
	</li>
</ul>


</body>
</html>

book.html で、本棚IDを入力するのではなく、select で選択できるようにする。

BookController.java で、本棚のリストをテンプレートに渡すようにする。

	@RequestMapping(value = "/book/{id}", method = RequestMethod.GET)
	public ModelAndView book(ModelAndView mav,
			@PathVariable long id) {
		mav.setViewName("book");
		Optional<Book> data = repository.findById(id);
		mav.addObject("book", data.get());
		List<Bookshelf> list = bookshelfRepository.findAll();
		mav.addObject("bookshelfList", list);
		return mav;
	}

book.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Book - 本を入れる本棚を指定する</title>
</head>
<body>

<h1>Book</h1>

<a href="/">トップ</a>

<table>
	<tr>
		<td th:text="${book.title}"></td>
		<td th:text="${book.author}"></td>
	</tr>
</table>

<hr />

<div>どの本棚に入れますか?</div>
<form action="/book" method="post">
	<input type="hidden" name="bookId" th:value="${book.id}" />
	<div>
		<select name="bookshelfId">
			<option th:each="bs : ${bookshelfList}" th:value="${bs.id}" th:selected="${bs.id == book.id}" th:text="${bs.name}"></option>
		</select>
	</div>
	<div><input id="submit" type="submit" value="本棚に入れる" /></div>
</form>


</body>
</html>

本棚に入れない選択肢を追加する。

<div>どの本棚に入れますか?</div>
<form action="/book" method="post">
	<input type="hidden" name="bookId" th:value="${book.id}" />
	<div>
		<select name="bookshelfId">
			<option value="0">本棚に入れない</option>
			<option th:each="bs : ${bookshelfList}" th:value="${bs.id}" th:selected="${bs.id == book.id}" th:text="${bs.name}"></option>
		</select>
	</div>
	<div><input id="submit" type="submit" value="本棚に入れる" /></div>
</form>

コントローラでは、本棚検索して見つからないときは null を設定する。

	@RequestMapping(value="/book", method=RequestMethod.POST)
	public ModelAndView save(ModelAndView mav,
			@RequestParam("bookId") long bookId,
			@RequestParam("bookshelfId") long bookshelfId) {
		Optional<Book> data = repository.findById(bookId);
		Book book = data.get();
		Optional<Bookshelf> bsData = bookshelfRepository.findById(bookshelfId);
		if(bsData.isPresent()) {
			Bookshelf bookshelf = bsData.get();
			book.setBookshelf(bookshelf);
		} else {
			book.setBookshelf(null);
		}
		repository.saveAndFlush(book);
		return new ModelAndView("redirect:/books");
	}