Spring BootでTodoアプリ2 -Todo作成-

Todo画面の作成

前回ログイン機能を作成したので、次はメインとなるTodo画面の作成。
下記の記事を参考に作成、少し修正したり機能追加したり。
Spring BootでToDoアプリを作ってみた - かずきのBlog@hatena

Todo用テーブルの作成

前回作成したusrテーブルのuser_idを外部キーとして持つtodoテーブルを作成

DROP TABLE IF EXISTS todoitems CASCADE;
CREATE TABLE IF NOT EXISTS todoitems(
  id SERIAL NOT NULL,
  title VARCHAR(255),
  done BOOLEAN,
  tododate TIMESTAMP NOT NULL,
  user_id VARCHAR(255) NOT NULL,
  PRIMARY KEY (id),
  FOREIGN KEY (user_id) REFERENCES usr
);

Todo画面を作成

前回作成したtodo.htmlを修正する。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>Todo</title>
</head>
<body>
    <h2>Todo Spring App</h2>
    <p th:inline="text">login user: [[${lastName}]] [[${firstName}]]</p>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
    <a th:unless="${todoItemForm.done}" th:href="@{/todo?isDone=true}">完了したアイテムの表示</a>
    <a th:if="${todoItemForm.done}" th:href="@{/todo?isDone=false}">TODOの表示</a>

    <hr />

    <h3>TODOの追加</h3>
    <form method="post" th:action="@{/new}">
        <input type="text" name="title" />
        <input type="submit" value="追加" />
    </form>

    <h3>TODOリスト</h3>
    <table>
        <thead>
            <tr>
                <th>Title</th>
                <th>
                    <form th:if="${todoItemForm.done}" method="post" th:action="@{/deleteAll}">
                        <input type="hidden" name="done" th:value="true" />
                        <input type="submit" value="DeleteAll" />
                    </form>
                    <form th:unless="${todoItemForm.done}" method="post" th:action="@{/doneAll}">
                        <input type="submit" value="DoneAll" />
                    </form>
                </th>
            </tr>
        <tbody>
            <tr th:each="todoItem : ${todoItemForm.todoItems}">
                <td th:text="${todoItem.title}">xxx</td>
                <td>
                    <form th:unless="${todoItemForm.done}" method="post" th:action="@{/done}" th:object="${todoItem}">
                        <input type="hidden" name="id" th:value="*{id}" />
                        <input type="submit" value="Done" />
                    </form>
                </td>
                <td>
                    <form th:if="${todoItemForm.done}" method="post" th:action="@{/restore}" th:object="${todoItem}">
                        <input type="hidden" name="id" th:value="*{id}" />
                        <input type="submit" value="Restore" />
                    </form>
                </td>
                <td>
                    <form th:if="${todoItemForm.done}" method="post" th:action="@{/delete}" th:object="${todoItem}">
                        <input type="hidden" name="id" th:value="*{id}" />
                        <input type="submit" value="Delete" />
                    </form>
                </td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Javaを書いてく

前回作成したログイン機能のみのアプリに下記を加えていく。
/SpringTodo/src/main/java/nyata/app/todo
- HomeController.java
/SpringTodo/src/main/java/nyata/domain/model
- TodoItem.java
/SpringTodo/src/main/java/nyata/domain/repositoriy
- TodoItemRepository.java
/SpringTodo/src/main/java/nyata/domain/service
- TodoItemForm.java

HomeController.java

画面から操作された時のコントローラ。参照したり更新したり。

package nyata.app.todo;

import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import nyata.domain.model.TodoItem;
import nyata.domain.repositoriy.TodoItemRepository;
import nyata.domain.service.TodoItemForm;
import nyata.domain.service.TodoUserDetails;

/**
 * Todo画面のコントローラ
 * @author nyata
 */
@Controller
public class HomeController {
    @Autowired
    TodoItemRepository repository;

    /**
     * 一覧表示
     * @param todoItemForm
     * @param isDone
     * @param userDetails
     * @param model
     */
    @GetMapping(value = "/todo")
    public String todo(@ModelAttribute TodoItemForm todoItemForm, @RequestParam("isDone") Optional<Boolean> isDone,
            @AuthenticationPrincipal TodoUserDetails userDetails, Model model) {
        todoItemForm.setDone(isDone.isPresent() ? isDone.get() : false);
        todoItemForm.setTodoItems(
                this.repository.findByDoneAndUserOrderByTododateAsc(todoItemForm.isDone(), userDetails.getUser()));
        model.addAttribute("firstName", userDetails.getUser().getFirstName());
        model.addAttribute("lastName", userDetails.getUser().getLastName());
        return "todo";
    }

    /**
     * 状態を「完了」に変更
     * @param id
     */
    @PostMapping(value = "/done")
    public String done(@RequestParam("id") long id) {
        TodoItem item = this.repository.getOne(id);
        item.setDone(true);
        this.repository.save(item);
        return "redirect:/todo?isDone=false";
    }

    /**
     * すべてのアイテムの状態を「完了」に変更
     * @param todoItemForm
     * @param userDetails
     */
    @PostMapping(value = "/doneAll")
    public String doneAll(@ModelAttribute TodoItemForm todoItemForm,
            @AuthenticationPrincipal TodoUserDetails userDetails) {
        todoItemForm.setTodoItems(this.repository.findByDoneAndUser(false, userDetails.getUser()));
        for (TodoItem todoitem : todoItemForm.getTodoItems()) {
            todoitem.setDone(true);
            this.repository.save(todoitem);
        }
        return "redirect:/todo?isDone=false";
    }

    /**
     * 状態を「未完」に変更
     * @param id
     */
    @PostMapping(value = "/restore")
    public String RestoreAction(@RequestParam("id") long id) {
        TodoItem item = this.repository.getOne(id);
        item.setDone(false);
        this.repository.save(item);
        return "redirect:/todo?isDone=true";
    }

    /**
     * アイテムを削除
     * @param id
     */
    @PostMapping(value = "/delete")
    public String deleteItem(@RequestParam("id") long id) {
        this.repository.deleteById(id);
        return "redirect:/todo?isDone=true";
    }

    /**
     * 「完了」状態のすべてのアイテムを削除
     * @param done
     * @param userDetails
     */
    @PostMapping(value = "/deleteAll")
    @Transactional
    public String deleteAll(@RequestParam("done") boolean done, @AuthenticationPrincipal TodoUserDetails userDetails) {
        this.repository.deleteByDoneAndUser(done, userDetails.getUser());
        return "redirect:/todo?isDone=true";
    }

    /**
     * 新規アイテムの登録
     * @param item
     * @param userDetails
     */
    @PostMapping(value = "/new")
    public String newItem(TodoItem item, @AuthenticationPrincipal TodoUserDetails userDetails) {
        item.setDone(false);
        item.setTododate(LocalDateTime.now());
        item.setUser(userDetails.getUser());
        this.repository.save(item);
        return "redirect:/todo";
    }
}
TodoItem.java
package nyata.domain.service;

import java.util.List;

import nyata.domain.model.TodoItem;

/**
 * Todoアイテムのフォーム
 * @author nyata
 */
public class TodoItemForm {
    /* 表示リスト(完了・未完)の切替フラグ */
    private boolean isDone;
    /* 表示するTodoリスト */
    private List<TodoItem> todoItems;

    // setter, getter
    public boolean isDone() {
        return isDone;
    }

    public void setDone(boolean isDone) {
        this.isDone = isDone;
    }

    public List<TodoItem> getTodoItems() {
        return todoItems;
    }

    public void setTodoItems(List<TodoItem> todoItems) {
        this.todoItems = todoItems;
    }
}
TodoItemRepository.java

JPAを使う用のリポジトリ。ソートや一括更新のため少しメソッド追加。
ユーザーごとのアイテムリストを表示する。

package nyata.domain.repositoriy;

import java.util.List;

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

import nyata.domain.model.TodoItem;
import nyata.domain.model.User;

/**
 * Todoアイテムのリポジトリ
 * @author nyata
 */
public interface TodoItemRepository extends JpaRepository<TodoItem, Long> {
    public List<TodoItem> findByDoneAndUser(boolean done, User user);

    public List<TodoItem> findByDoneAndUserOrderByTododateAsc(boolean done, User user);

    public long deleteByDoneAndUser(boolean done, User user);
}
TodoItemForm.java

画面に表示内容を受け渡す役割

package nyata.domain.service;

import java.util.List;

import nyata.domain.model.TodoItem;

/**
 * Todoアイテムのフォーム
 * @author nyata
 */
public class TodoItemForm {
    /* 表示リスト(完了・未完)の切替フラグ */
    private boolean isDone;
    /* 表示するTodoリスト */
    private List<TodoItem> todoItems;

    // setter, getter
    public boolean isDone() {
        return isDone;
    }

    public void setDone(boolean isDone) {
        this.isDone = isDone;
    }

    public List<TodoItem> getTodoItems() {
        return todoItems;
    }

    public void setTodoItems(List<TodoItem> todoItems) {
        this.todoItems = todoItems;
    }
}

完成

ログイン機能よりすごく簡単に実装できた。フレームワークって簡単にアプリ作れてすごいなと思いました。 もう一つくらい、Springでアプリ作りたいと思ってる。ひとまずUIがしょぼいままなのでフロントの勉強として作成したアプリの見た目を整えていこうと思う。
GitHub - n-yata/ToDoSpring

はまったこと

画面遷移とリダイレクトでパスの書き方が異なる

単純に画面遷移するときは、return "todo";
リダイレクトのときは、 return "redirect:/todo";
頭に"/"をつけたりつけなかったり。ちゃんと区別しないと怒られる。

一括更新の仕方

SQLだったらUPDATE todoitems SET done = 'false' WHERE user_id = 'hogehoge'とかでまとまりのまま一括更新できるのだけど、 JPAで一括UPDATEするときにfor文利用する方法しか見つからなかった。

        todoItemForm.setTodoItems(this.repository.findByDoneAndUser(false, userDetails.getUser()));
        for (TodoItem todoitem : todoItemForm.getTodoItems()) {
            todoitem.setDone(true);
            this.repository.save(todoitem);
        }

なんかSQLの良さを殺してしまうような書き方な気がして気持ち悪いので気が向いたら修正する。

JPAの書き方

日本語の記事少ない。英語ちゃんと勉強しようと思いました・・・

参考文献

Spring BootでToDoアプリを作ってみた - かずきのBlog@hatena
[Spring MVC] パス変数の受け渡し方について - Qiita
Spring Data JPA で遊んでみる 〜その6〜 - Yamkazu's Blog
令和時代に「Spring入門」「Spring徹底入門」を読むとき気をつけるべきN個のこと - Qiita