0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Spring Bootでアプリを作成してみた【課題管理システム】

Last updated at Posted at 2023-03-16

JetBrains Toolbox App をインストールする

1.プロジェクト作成

公式:spring initializr

① ブラウザを開き、(https://start.spring.io/) を入力し、開く。
② 下記を入力し、Dependenciesで下記を選択し、『GENERATE』ボタンを押す。

  • Project: Gradle
  • Language: Java
  • SpringBoot: 3.1.0
  • Group: com.example
  • Name : its
  • Artifact: its
  • Description: Issue Tracking System
  • Package name: com.example

Dependencies

  • Spring Web
  • Thymeleaf
  • Spring dev tools

2.トップページを表示する

Webアプリケーションを実装するとは

  1. リクエストとControllerを紐付ける
  2. Controllerでレスポンスを生成する

①src > main > java > com > example > <プロジェクト名> 直下にwebフォルダを作成し、IndexController.javaファイルを作成する。

web / IndexController.java
package com.example.its.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody

@Controller
public class IndexController {

    @GetMapping("/")
    @ResponseBody
    public String index() {
        return "<h1>Hello, World!!</h1>";
    }
}

3.テンプレートエンジンを利用する

テンプレートエンジンとは、テンプレートとデータから動的に文字を生成する仕組みのこと

①spring initializrに『Thymeleaf』を追加して、『EXPLORE』を押し、下記のファイルにコードを加える。

build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

②src > main > resource > templete にindex.htmlファイルを作成する。

src/main/resources/templates/index.html
<!DOCTYPE >
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>Assignment management app</h1>
  </body>
</html>

③IndexController.javaファイルを編集する。

web/IndexController.java
package com.example.its.web;

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

@Controller
public class IndexController {

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

4.Spring Dev Toolsを導入して開発効率を上げる

①spring initializrに『Spring dev Tools』を追加して、『EXPLORE』を押し、下記のファイルにコードを加える。

build.gradle
developmentOnly 'org.springframework.boot:spring-boot-devtools'

②main/resouces/application.propertiesに下記のコードを加える。

main/resouces/application.properties
spring.thymeleaf.prefix=file:src/main/resources/templates/

5.modelとcontrollerを作成する

①src > main > java > com > example > <プロジェクト名> 直下に
domainissueフォルダを作成し、IssueEntityファイルを作成する。

domainissue/IssueEntity.java
package com.example.its.domainissue;

public class IssueEntity {
    private long id;
    private String summary;
    private String description;

    public IssueEntity(long id, String summary, String description) {
        this.id = id;
        this.summary = summary;
        this.description = description;
    }

    public long getId() {
        return id;
    }

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

    public String getSummary() {
        return summary;
    }

    public void setSummry(String summary) {
        this.summary = summary;
    }

    public String getDescription() {
        return summary;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

②webにissueフォルダを作成し、IssueController.javaファイルを作成する。

issue/IssueController.java
package com.example.its.web.issue;

import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.thymeleaf.engine.AttributeName;

import java.util.List;
import com.example.its.domainissue.IssueEntity;

@Controller
public class IssueController {

    // GET /issues
    @GetMapping("/issues")
    public String showList(Model model) {
        var issueList:List<IssueEntity> = List.of(
            new IssueEntity(1, "概要1", "説明1"),
            new IssueEntity(2, "概要2", "説明2"),
            new IssueEntity(3, "概要3", "説明3")

        );
        model.addAllAttribute(attributeName:"issueList",issueList);
        return "issues/list";
    }
}

6.VIEWを作成する

①src > main > resource > templeteに issuesフォルダにlist.htmlファイルを作成する。

issues/list.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題一覧 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題一覧</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>概要</th>
        </tr>
      </thead>
      <tbody>
        <!-- Javaから渡された複数件のissueを1件ずつ取る -->
        <tr th:each="issue : ${issueList}">
          <th th:text="${issueList.id}">(id)</th>
          <td th:text="${issueList}.summry">(summary)</td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

7.ページ間を移動できる用にリンクを貼る

①templete/index.htmlファイルを編集する。

templete/index.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理アプリケーション</h1>
    <ul>
      <li><a href="./issues/list.html" th:href="@{/issues}">課題一覧</a></li>
    </ul>
  </body>
</html>

②templete/issues/list.htmlファイルを編集する。

templete/issues/list.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題一覧 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題一覧</h1>
    <a href="../index.html" th:href="@/">トップページ</a>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>概要</th>
        </tr>
      </thead>
      <tbody>
        <!-- Javaから渡された複数件のissueを1件ずつ取る -->
        <tr th:each="issue : ${issueList}">
          <th th:text="${issueList.id}">(id)</th>
          <td th:text="${issueList}.summry">(summary)</td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

8.Lombokを使ってリファクタリングをする

※ Lombokとは、コンストラクターやゲッター、セッターのコードを自動生成してくれるライブラリーのこと
①下記のコードを加える。

build.gradle
configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}

//dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

@AllArgsConstructorと@Dataをdomainissue/IssueEntityに記入する。

domainissue/IssueEntity.java
package com.example.its.domainissue;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class IssueEntity {
    private long id;
    private String summary;
    private String description;
}

9.サービス層を作ってビジネスロジックを分離する

①issueフォルダに、IssueServiceファイルを作成する。

issue/IssueService.java
package com.example.its.web.issue;

import com.example.its.domainissue.IssueEntity;
import java.util.List;

public class IssueService {

    public List<IssueEntity> findAll() {
        return List.of(
                new IssueEntity(1, "概要1", "説明1"),
                new IssueEntity(2, "概要2", "説明2"),
                new IssueEntity(3, "概要3", "説明3"));
    }
}

②IssueController.javaファイルを編集する。

issue/IssueController.java
package com.example.its.web.issue;

import com.example.its.web.issue.IssueService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IssueController {

    private final IssueService issueService = new IssueService();

    @GetMapping("/issues")
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }
}

10.dependency injectionを使う

DIとは?

DI:Dependency Injection(依存性の注入)
依存性の注入:使いたいオブジェクトを直接newせず、外部から渡してもらうようにするというデザインパターン

『直接newせず外部からもらう』とは?

sample.java
//DI無

@Controller
public class IssueController {                  //newしている
  private final IssueService IssueService = new IssueService();
}

//DI有
@Controller
public class IssueController {                  
  private final IssueService IssueService;
                      //コンストラクタを呼び出す側でnewしてもらう
public IssueController (IssueService IssueService){            
  this.IssueService = IssueService;
 }
}

①issue/IssueServiceに、@Serviceを加える。

issue/IssueService.java
package com.example.its.web.issue;

import com.example.its.domainissue.IssueEntity;
import java.util.List;
import org.springframework.stereotype.Service;

//ビジネスロジック・アルゴリズム等が書かれた処理を提供する
@Service
public class IssueService {

    public List<IssueEntity> findAll() {
        return List.of(
                new IssueEntity(1, "概要1", "説明1"),
                new IssueEntity(2, "概要2", "説明2"),
                new IssueEntity(3, "概要3", "説明3"));
    }
}

②issue/IssueService.javaに、引数有のコンストラクターを用意する。

issue/IssueSController.java
package com.example.its.web.issue;

import com.example.its.web.issue.IssueService;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
public class IssueController {

    private final IssueService issueService;

    @GetMapping("/issues")
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }
}

11.H2データベースとMyBatisのセットアップをする

①spring initializrに『H2 Database』を追加して、『MyBatis』を押し、下記のファイルにコードを加える。

build.gradle
dependencies {
//
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
runtimeOnly 'com.h2database:h2'
}

② resources/application.propertiesを編集する。

resources/application.properties
spring.thymeleaf.prefix=file:src/main/resources/templates/
spring.datasource.data-source-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:its;DB_CLOSE_ON_EXIT=TRUE;MODE=MySQL
spring.datasource.username=sa
spring.datasource.password=

③ src > main > resource に schema.sql と data.sqlでテーブル作成と初期データ投入する。

resource/schema.sql
create table issues (
id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
summary VARCHAR(256) NOT NULl,
description VARCHAR(256) NOT NULl
);
resource/data.sql
insert into issues (summary, description) values ('バグA', 'バグがあります');
insert into issues (summary, description) values ('機能要望B', 'Bに追加機能がほしいです');
insert into issues (summary, description) values ('画面Cが遅い', '早くしてほしいです');

12. データアクセス層を作ってデータベースにselect文を実行する

①domainissueフォルダを作成し、IssueReositoryファイルを作成する。

domainissue/IssueReository.java
package com.example.its.domainissue;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface IssueRepository {
    @Select("Select * from issue")
    List<IssueEntity> findAll();
}

②IssueServiceファイルを編集する。

domainissue/IssueService.java

package com.example.its.domainissue;

import lombok.RequiredArgsConstructor;
import com.example.its.domainissue.IssueEntity;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@RequiredArgsConstructor
public class IssueService {

    praivate final IssueRepository IssueRepository;

    public List<IssueEntity> findAll() {
        return IssueRepository.finalAll();
    }
}

13.課題作成のための入力フォームを作る

①HTML画面を作成する為、templete/issues/creationForm.htmlファイルを作成する。

issues/creationForm.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理</h1>
    <form>
      <div>
        <label for="summaryInput">概要</label>
        <input type="text" id="summaryInput" />
      </div>
      <div>
        <label for="descriptionInput">説明</label>
        <textarea type="text" id="descriptionInput" row="10"></textarea>
      </div>
      <div>
        <button type="submit">作成</button>
      </div>
    </form>
  </body>
</html>

②issueControllerの中にハンドラーメソッドを作成する。

issue/IssuesController.java
package com.example.its.web.issue;

import com.example.its.domainissue.IssueService;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/issues")
@RequiredArgsConstructor
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm() {
        return "issues/creationForm";
    }
}

③一覧画面に作成画面のリンクを表示する。

templete/issues/list.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題一覧 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題一覧</h1>
    <a href="../index.html" th:href="@/">トップページ</a>
    <a href="./creationtForm.html" th:href="@/issues/creationtForm">作成</a>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>概要</th>
        </tr>
      </thead>
      <tbody>
        <!-- Javaから渡された複数件のissueを1件ずつ取る -->
        <tr th:each="issue : ${issueList}">
          <th th:text="${issueList.id}">(id)</th>
          <td th:text="${issueList}.summry">(summary)</td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

14. 課題作成のリクエストを受けるハンドラーメソッドを実装する

①イシューコントローラーの中にメソッドを作る。

issue/IssueController.java
package com.example.its.web.issue;

import com.example.its.domainissue.IssueService;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/issues")
@RequiredArgsConstructor
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm() {
        return "issues/creationForm";
    }

    @PostMapping("creationForm")
    public String create(IssueForm form, Model model) {
        return showList(model);
    }
}

②issuesフォルダに、IssueFormファイルを作成する。

issues/IssueForm.java
package com.example.its.web.issue;

import lombok.Data;

@Data
public class issueForm {
    private String summary;
    private String description;
}

③issues/creationForm.htmlを編集する。

issues/creationForm.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理</h1>
    <form action="#" th:action="@{/issues}" th:method="post">
      <div>
        <label for="summaryInput">概要</label>
        <input type="text" id="summaryInput" />
      </div>
      <div>
        <label for="descriptionInput">説明</label>
        <textarea type="text" id="descriptionInput" row="10"></textarea>
      </div>
      <div>
        <button type="submit">作成</button>
      </div>
    </form>
  </body>
</html>

15.Viewからデータを受け取り、受け取ったデータをDBに保存する

①issues/creationForm.htmlを編集する。

issues/creationForm.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理</h1>
    <form
      action="#"
      th:action="@{/issues}"
      th:method="post"
      th:object="${issueForm}"
    >
      <div>
        <label for="summaryInput">概要</label>
        <input type="text" id="summaryInput" th:field="*{summary}" />
      </div>
      <div>
        <label for="descriptionInput">説明</label>
        <textarea
          type="text"
          id="descriptionInput"
          row="10"
          th:field="*{description}"
        ></textarea>
      </div>
      <div>
        <button type="submit">作成</button>
      </div>
    </form>
  </body>
</html>

②issue/IssueController.javaを編集する。

issue/IssueSController.java
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm(@ModelAttribute issueForm form) {
        return "issues/creationForm";
    }

    @PostMapping
    public String create(issueForm form, Model model) {
        issueService.create(form.getSummary(), form.getDescription());
        return showList(model);
    }
}

③issue/IssueServiceファイルを編集する。

issue/IssueService.java

package com.example.its.domainissue;

import lombok.RequiredArgsConstructor;
import com.example.its.domainissue.IssueEntity;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@RequiredArgsConstructor
public class IssueService {

    praivate final IssueRepository IssueRepository;

    public List<IssueEntity> findAll() {
        IssueRepository.insert(summary, description);
    }
}

④issue/IssueRepositoryファイルを編集する。

issue/IssueRepository.java
package com.example.its.domainissue;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface IssueRepository {

    @Select("Select * from issues")
    List<IssueEntity> findAll();

    @Insert("insert into issue(summary, description) values(#{summary},#{description})")
    void insert(String summary, String description);
}

16. 登録処理にトランザクションを張る

※ トランザクションとは、複数の処理を1まとまりにして扱う考え方。
※ ロールバックとは、途中で処理が失敗したときに、それまでの変更なかったことにするという機能。

issue/IssueService.java
package com.example.its.domainissue;

import lombok.RequiredArgsConstructor;
import com.example.its.domainissue.IssueEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class IssueService {

    praivate
    final IssueRepository IssueRepository;

    public List<IssueEntity> findAll() {
       return IssueRepository.findAll();
    }
    @Transactional
    public void create(String summary, String description) {
        IssueRepository.insert(summary, description);
    }
}

17. 2重サブミット対策をする

※2重サブミットとは、二重で実行されるとシステムとして困る操作を二重ですること
※2重サブフィットが起こる原因は、ブラウザーのリロード処理が直前のリクエストを再実行するから

①issue/IssueController.javaを編集する。

issue/IssueController.java
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm(@ModelAttribute issueForm form) {
        return "issues/creationForm";
    }

    @PostMapping
    public String create(issueForm form, Model model) {
        issueService.create(form.getSummary(), form.getDescription());
        return "redirect:/issues";
    }
}

18. バリデーションを実装する

①spring initializrに『Validation』を追加して、『EXPLORE』を押し、下記のファイルにコードを加える。

build.gradle
dependencies {
//
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

②issues/IssueForm.javaを編集する。

issues/IssueForm.java
package com.example.its.web.issue;

import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Data
public class issueForm {

    @NotBlankBlank
    @Size(max = 256)
    private String summary;

    @NotBlankBlank
    @Size(max = 256)
    private String description;
}

③issue/IssueController.javaを編集する。

issue/IssueController.java
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm(@ModelAttribute issueForm form) {
        return "issues/creationForm";
    }

    @PostMapping
    public String create(@Validated issueForm form, BindingResult bindingResult, Model model) {
        if(bindingResult.hasErrors()){
            return showCreationForm(form;)
        }
        issueService.create(form.getSummary(), form.getDescription());
        return "redirect:/issues";
    }
}

④issues/creationForm.htmlを編集する。

issues/creationForm.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理</h1>
    <form
      action="#"
      th:action="@{/issues}"
      th:method="post"
      th:object="${issueForm}"
    >
      <div>
        <label for="summaryInput">概要</label>
        <input type="text" id="summaryInput" th:field="*{summary}" />
        <p th:if="${#fields.hasErrors('summary')}" th:errors="*{summary}">
          (error)
        </p>
      </div>
      <div>
        <label for="descriptionInput">説明</label>
        <textarea
          type="text"
          id="descriptionInput"
          row="10"
          th:field="*{description}"
        ></textarea>
        <p
          th:if="${#fields.hasErrors('description')}"
          th:errors="*{description}"
        >
          (error)
        </p>
      </div>
      <div>
        <button type="submit">作成</button>
      </div>
    </form>
  </body>
</html>

19. 課題詳細画面を実装

(1)View
①issues/detail.htmlを作成する。

issues/detail.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題詳細 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題詳細</h1>
    <div>
      <h2 th:text="${issue.summary}">(summary)</h2>
      <p th:text="${issue.description}">(description)</p>
    </div>
  </body>
</html>

(2)Controller
①issue/IssueController.javaを編集する。

issue/IssueController.java
package com.example.its.web.issue;

import com.example.its.domainissue.IssueEntity;
import com.example.its.domainissue.IssueService;

import lombok.RequiredArgsConstructor;

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

@Controller
@RequestMapping("/issues")
@RequiredArgsConstructor
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm(@ModelAttribute issueForm form) {
        return "issues/creationForm";
    }

    @PostMapping
    public String create(@Validated issueForm form, BindingResult bindingResult, Model model) {
        if(bindingResult.hasErrors()){
            return showCreationForm(form;)
        }
        issueService.create(form.getSummary(), form.getDescription());
        return "redirect:/issues";
    }

    @GetMapping("/{issueId}")
    public String showDetail(@PathVariable("issueId") long issueId, Model model) {
        IssueEntity dummyEntity = new IssueEntity(id:1,summary:"概要", description:"説明"));
        model.addAttribute("issue", dummyEntity);
        return "issues/detail";
    }
}

(3)Serivce + Repository
①issue/IssueController.javaを編集する。

issue/IssueController.java
package com.example.its.web.issue;

import com.example.its.domainissue.IssueEntity;
import com.example.its.domainissue.IssueService;

import lombok.RequiredArgsConstructor;

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

@Controller
@RequestMapping("/issues")
@RequiredArgsConstructor
public class IssueController {

    private final IssueService issueService;

    @GetMapping
    public String showList(Model model) {
        model.addAttribute("issueList", issueService.findAll());
        return "issues/list";
    }

    @GetMapping("creationForm")
    public String showCreationForm(@ModelAttribute issueForm form) {
        return "issues/creationForm";
    }

    @PostMapping
    public String create(@Validated issueForm form, BindingResult bindingResult, Model model) {
        if(bindingResult.hasErrors()){
            return showCreationForm(form;)
        }
        issueService.create(form.getSummary(), form.getDescription());
        return "redirect:/issues";
    }

    @GetMapping("/{issueId}")
    public String showDetail(@PathVariable("issueId") long issueId, Model model) {
        model.addAttribute("issue", issueService.findById(issueId));
        return "issues/detail";
    }
}

②issue/IssueController.javaを編集する。

issue/IssueService.java
package com.example.its.domainissue;

import lombok.RequiredArgsConstructor;
import com.example.its.domainissue.IssueEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class IssueService {

    praivate
    final IssueRepository IssueRepository;

    public List<IssueEntity> findAll() {

        return IssueRepository.findAll();
    }

    @Transactional
    public void create(String summary, String description) {
        IssueRepository.insert(summary, description);
    }

    public IssueEntity findById(long issueId){
        return IssueRepository.findById(issueId);
    }
}

③issue/IssueRepository.javaを編集する。

issue/IssueRepository.java
package com.example.its.domainissue;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface IssueRepository {

    @Select("Select * from issues")
    List<IssueEntity> findAll();

    @Insert("insert into issue(summary, description) values(#{summary},#{description})")
    void insert(String summary, String description);

    @Select("select * from issues where id = #{issueId}")
    IssueEntity findById(long issueId);
}

(4)画面遷移のためのリンクを作る

①issue/IssueRepository.javaを編集する。

issue/IssueRepository.java
package com.example.its.domainissue;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface IssueRepository {

    @Select("Select * from issues")
    List<IssueEntity> findAll();

    @Insert("insert into issue(summary, description) values(#{summary},#{description})")
    void insert(String summary, String description);

    @Select("select * from issues where id = #{issueId}")
    IssueEntity findById(long issueId);
}

②issues/list.htmlのth:textをaタグに変える。

templete/issues/list.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題一覧 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題一覧</h1>
    <a href="../index.html" th:href="@/">トップページ</a>
    <a href="./creationtForm.html" th:href="@/issues/creationtForm">作成</a>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>概要</th>
        </tr>
      </thead>
      <tbody>
        <!-- Javaから渡された複数件のissueを1件ずつ取る -->
        <tr th:each="issue : ${issueList}">
          <th th:text="${issueList.id}">(id)</th>
          <td>
            <a
              href="./detail.html"
              th:href="@{/issueList/{issueId}(issueId=${issue.id})}"
              th:text="${issueList.summary}"
              >(summary)
            </a>
          </td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

③issues/detail.htmlを編集する。

issues/detail.html
<!DOCTYPE >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>課題詳細 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題詳細</h1>
    <a href="./list.html" th:href="@{/issues}">一覧に戻る</a>
    <div>
      <h2 th:text="${issue.summary}">(summary)</h2>
      <p th:text="${issue.description}">(description)</p>
    </div>
  </body>
</html>

20. Thymleaf のすべてのページでフラグメントを使ってリファクタリングする

①templates/fragments/layout.htmlを作成する。

templates/fragments/layout.html
<!DOCTYPE >
<html
  lang="en"
  xmlns:th="http://www.thymeleaf.org"
  th:fragment="layout(title,content)"
>
  <head>
    <meta charset="UTF-8" />
    <title th:replace="${title}">(default title)</title>
  </head>
  <body>
    <div th:insert="${content}">(default title)</div>
  </body>
</html>

②templates/issues/index.htmlを編集する。

templates/issues/index.html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{fragments/layout :: layout(~{::title},~{::body})}"
>
  <head>
    <meta charset="UTF-8" />
    <title>トップページ | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理アプリケーション</h1>
    <ul>
      <li><a href="./issues/list.html" th:href="@{/issues}">課題一覧</a></li>
    </ul>
  </body>
</html>

③templates/issues/list.htmlをリファクタリングする。

templates/issues/list.html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{fragments/layout :: layout(~{::title},~{::body})}"
>
  <head>
    <title>課題一覧 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題一覧</h1>
    <a href="../index.html" th:href="@/">トップページ</a>
    <a href="./creationtForm.html" th:href="@/issues/creationtForm">作成</a>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>概要</th>
        </tr>
      </thead>
      <tbody>
        <!-- Javaから渡された複数件のissueを1件ずつ取る -->
        <tr th:each="issue : ${issueList}">
          <th th:text="${issueList.id}">(id)</th>
          <td>
            <a
              href="./detail.html"
              th:href="@{/issueList/{issueId}(issueId=${issue.id})}"
              th:text="${issueList.summary}"
              >(summary)
            </a>
          </td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

④templates/issues/detail.htmlをリファクタリングする。

templates/issues/detail.html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{fragments/layout :: layout(~{::title},~{::body})}"
>
  <head>
    <title>課題詳細 | 課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題詳細</h1>
    <a href="./list.html" th:href="@{/issues}">一覧に戻る</a>
    <div>
      <h2 th:text="${issue.summary}">(summary)</h2>
      <p th:text="${issue.description}">(description)</p>
    </div>
  </body>
</html>

④templates/issues/creationForm.htmlをリファクタリングする。

templates/issues/creationForm.html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{fragments/layout :: layout(~{::title},~{::body})}"
>
  <head>
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1>課題管理</h1>
    <form
      action="#"
      th:action="@{/issues}"
      th:method="post"
      th:object="${issueForm}"
    >
      <div>
        <label for="summaryInput">概要</label>
        <input type="text" id="summaryInput" th:field="*{summary}" />
        <p th:if="${#fields.hasErrors('summary')}" th:errors="*{summary}">
          (error)
        </p>
      </div>
      <div>
        <label for="descriptionInput">説明</label>
        <textarea
          type="text"
          id="descriptionInput"
          row="10"
          th:field="*{description}"
        ></textarea>
        <p
          th:if="${#fields.hasErrors('description')}"
          th:errors="*{description}"
        >
          (error)
        </p>
      </div>
      <div>
        <button type="submit">作成</button>
      </div>
    </form>
  </body>
</html>

20. Boostrapを使用する

(1) BoostrapをCDNから読み込む。
公式サイト : Boostrap

templates/fragments/layout.html
<!DOCTYPE >
<html
  lang="en"
  xmlns:th="http://www.thymeleaf.org"
  th:fragment="layout(title,content)"
>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title th:replace="${title}">(default title)</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <div th:insert="${content}"></div>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

(2) 余白を調整する:Container
①templates/fragments/layout.htmlに divタグに class="container"を記載する

templates/fragments/layout.html
<!DOCTYPE >
<html
  lang="en"
  xmlns:th="http://www.thymeleaf.org"
  th:fragment="layout(title,content)"
>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title th:replace="${title}">(default title)</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <div class="container" th:insert="${content}"></div>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

(3)余白を調整する:Spacing
①htmlファイルに h1要素にmt-3というclassを付ける。

(4)テーブルのスタイルを調整する

①templates/issues/list.htmlに、TABLE要素にCLASSでTABLEと書く。

(5)フォームのスタイルを調整する

①templates/issues/creationForm.htmlを概要のラベルにCLASSでフォームラベルを指定する。
②input要素にはCLASSのフォームコントロールを指定する。
③作成ボタンにボタンクラスとボタンプライマリークラスを付ける。

templates/issues/creationForm.html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{fragments/layout :: layout(~{::title},~{::body})}"
>
  <head>
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1 class="mt-3">課題管理</h1>
    <form
      action="#"
      th:action="@{/issues}"
      th:method="post"
      th:object="${issueForm}"
    >
      <div class="mt-3">
        <label for="summaryInput" class="form-label">概要</label>
        <input
          type="text"
          id="summaryInput"
          th:field="*{summary}"
          class="form-control"
        />
        <p th:if="${#fields.hasErrors('summary')}" th:errors="*{summary}">
          (error)
        </p>
      </div>
      <div class="mt-3">
        <label for="descriptionInput" class="form-label">説明</label>
        <textarea
          type="text"
          id="descriptionInput"
          row="10"
          th:field="*{description}"
          class="form-control"
        ></textarea>
        <p
          th:if="${#fields.hasErrors('description')}"
          th:errors="*{description}"
        >
          (error)
        </p>
      </div>
      <div class="mt-3">
        <button type="submit" class="btn btn-primary">作成</button>
        <a href="./list.html" th:href="@{/issues}" class="btn btn-secondary"
          >キャンセル</a
        >
      </div>
    </form>
  </body>
</html>

(6)リデーションのスタイルを調整する
①バリデーションエラー時のみインバリとCLASSをCLASS属性に追加する。

templates/issues/creationForm.html
<!DOCTYPE html>
<html
  xmlns:th="http://www.thymeleaf.org"
  th:replace="~{fragments/layout :: layout(~{::title},~{::body})}"
>
  <head>
    <title>課題管理アプリケーション</title>
  </head>
  <body>
    <h1 class="mt-3">課題管理</h1>
    <form
      action="#"
      th:action="@{/issues}"
      th:method="post"
      th:object="${issueForm}"
    >
      <div class="mt-3">
        <label for="summaryInput" class="form-label">概要</label>
        <input
          type="text"
          id="summaryInput"
          th:field="*{summary}"
          class="form-control"
          th:classappend="${#fields.hasErrors('summary')} ? is-invalid"
        />
        <p
          class="invalid-feedback"
          th:if="${#fields.hasErrors('summary')}"
          th:errors="*{summary}"
        >
          (error)
        </p>
      </div>
      <div class="mt-3">
        <label for="descriptionInput" class="form-label">説明</label>
        <textarea
          type="text"
          id="descriptionInput"
          row="10"
          th:field="*{description}"
          class="form-control"
          is-invalid
        ></textarea>
        <p
          class="invalid-feedback"
          th:if="${#fields.hasErrors('description')}"
          th:errors="*{description}"
        >
          (error)
        </p>
      </div>
      <div class="mt-3">
        <button type="submit" class="btn btn-primary">作成</button>
        <a href="./list.html" th:href="@{/issues}" class="btn btn-secondary"
          >キャンセル</a
        >
      </div>
    </form>
  </body>
</html>

参考サイト

IntelliJ ではじめる Spring Boot:課題管理アプリを作って学ぶWebアプリケーション開発の基礎
【5分でわかる】Springでよく使うアノテーション40選! 基本からバリデーションまでまるっとご紹介

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?