かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Spring BootでToDoアプリを作ってみた

こんな感じで作ってみた。

f:id:okazuki:20150720133735p:plain

画面はしょっぱいですが、TODOアプリです。

f:id:okazuki:20150720133842p:plain

pomの準備

とりあえずmavenでプロジェクトを作成したら、pomを編集します。お約束ですね。最近みないで打てるようになってきました。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>okazuki</groupId>
    <artifactId>todoapp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>todoapp</name>
    <url>http://maven.apache.org</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.5.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.4-1201-jdbc41</version>
        </dependency>
    </dependencies>
</project>

今回はPostgreSQLを使うのでそれのJDBCドライバも追加しています。

application.propertiesの編集

DBの接続関連の情報を追加してます。仮にtodoappという名前のDBにtodoappというユーザーでtodoappというパスワードでつなぐことにします。

spring.datasource.url=jdbc:postgresql://localhost:5432/todoapp
spring.datasource.username=todoapp
spring.datasource.password=todoapp
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

DBの作成

PostgreSQLに適当にDBを作ります。上のDBを作ります。作成するテーブルは、こんな感じでさくっと。

CREATE TABLE todoitems
(
  id serial NOT NULL,
  title text,
  done boolean,
  CONSTRAINT todoitems_pkey PRIMARY KEY (id)
)

idのserialは、todoitem_id_seqというシーケンスからとってくるようになります。(GUIで作ったらそうなった)

Entityの作成

JPAのEntityを作成します。eclipseって自動生成機能ってあるのかな…わからなかったので手書きしました。

package okazuki.todoapp.entities;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity
@Table(name = "todoitems")
public class TodoItem {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "todoitems_id_seq")
    @SequenceGenerator(name = "todoitems_id_seq", sequenceName = "todoitems_id_seq")
    private Long id;
    private String title;
    private Boolean done;

    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 Boolean getDone() {
        return done;
    }

    public void setDone(Boolean done) {
        this.done = done;
    }

}

リポジトリの作成

今回はTODOが完了してるかしてないかというのを取得したいのでDoneプロパティで絞込できるようにメソッドをはやしたリポジトリを作成します。ここらへん便利ですね。Spring JPA

package okazuki.todoapp.repositories;

import java.util.List;

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

import okazuki.todoapp.entities.TodoItem;

public interface TodoItemRepository extends JpaRepository<TodoItem, Long> {
    public List<TodoItem> findByDoneOrderByTitleAsc(boolean done);
}

Controllerの作成

次にコントローラを作成します。初期表示、アイテムを完了にする、完了にしたものをもどす、新規に追加するの4つのメソッドを持っています。

package okazuki.todoapp.controllers;

import java.util.Optional;

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

import okazuki.todoapp.entities.TodoItem;
import okazuki.todoapp.forms.TodoItemForm;
import okazuki.todoapp.repositories.TodoItemRepository;

@Controller
public class HomeController {

    @Autowired
    TodoItemRepository repository;
    
    @RequestMapping
    public String index(@ModelAttribute TodoItemForm todoItemForm, @RequestParam("isDone") Optional<Boolean> isDone) {
        todoItemForm.setDone(isDone.isPresent() ? isDone.get() : false);
        todoItemForm.setTodoItems(this.repository.findByDoneOrderByTitleAsc(todoItemForm.isDone()));
        return "index";
    }
    
    @RequestMapping(value = "/done", method = RequestMethod.POST)
    public String done(@RequestParam("id") long id) {
        TodoItem item = this.repository.findOne(id);
        item.setDone(true);
        this.repository.save(item);
        return "redirect:/?isDone=false";
    }
    
    @RequestMapping(value = "/restore", method = RequestMethod.POST)
    public String restore(@RequestParam("id") long id) {
        TodoItem item = this.repository.findOne(id);
        item.setDone(false);
        this.repository.save(item);
        return "redirect:/?isDone=true";
    }
    
    @RequestMapping(value = "/new", method = RequestMethod.POST)
    public String newItem(TodoItem item) {
        item.setDone(false);
        this.repository.save(item);
        return "redirect:/";
    }
    
}

TodoItemFormは、以下のようになっています。

package okazuki.todoapp.forms;

import java.util.List;

import okazuki.todoapp.entities.TodoItem;

public class TodoItemForm {
    private boolean isDone;
    
    private List<TodoItem> todoItems;

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

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

    public boolean isDone() {
        return isDone;
    }

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

}

Viewの作成

最後にViewを作成します。見た目適当です。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Insert title here</title>
</head>
<body>
    <h2>Todo app</h2>
    
    <a th:unless="${todoItemForm.done}" th:href="@{/?isDone=true}">完了したアイテムの表示</a>
    <a th:if="${todoItemForm.done}" th:href="@{/?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></th>
            </tr>
        </thead>
        
        <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>
                    <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>
            </tr>
        </tbody>
    </table>
</body>
</html>

これくらいの量のコードでCRUD(Dは厳密にいうと無いけど)の画面ができるって割といい感じじゃないですかね。