Webアプリ作成 Spring Boot
⑥日記登録・詳細確認

日記の登録・詳細確認を画面からできるようにします。

日記登録

Form

登録用のFormを作成します。
formパッケージ内に「PostForm.java」を作成します。

package diary.form;

import java.util.Date;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class PostForm {

	private int id;

	private String categoryForm;

	@NotNull (message = "日付を入力してください。")
	private Date dateForm;

	@NotNull (message = "題名を入力してください。")
	@Size(min = 1, max = 25, message="25文字以内で入力してください。")
	private String titleForm;

	private String contentForm;

	public String getCategoryForm() {
		return categoryForm;
	}

	public void setCategoryForm(String categoryForm) {
		this.categoryForm = categoryForm;
	}

	public Date getDateForm() {
		return dateForm;
	}

	public void setDateForm(Date dateForm) {
		this.dateForm = dateForm;
	}

	public String getTitleForm() {
		return titleForm;
	}

	public void setTitleForm(String titleForm) {
		this.titleForm = titleForm;
	}

	public String getContentForm() {
		return contentForm;
	}

	public void setContentForm(String contentForm) {
		this.contentForm = contentForm;
	}

	public int getId() {
		return id;
	}

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


中身を記載するとimport文でエラーになるかと思いますので解消していきます。
プロジェクト上で右クリック「Spring→Add Starters」を選択します。

I/O内から「Validation」を追加して「Next」を選択します。

「build.gradle」を選択して、「Finish」を選択します。

プロジェクト上で右クリックし、「Refresh」を選択するとエラーが消えるはずです。

ポイント

@NotNull (message = “日付を入力してください。”)
@Size(min = 1, max = 25, message=”25文字以内で入力してください。”)

 javax.validationライブラリを利用することで簡単に画面の入力値をチェックすることが可能です。NotNullは値が空でないこと。Sizeは文字数をチェックしています。チェックに引っ掛かるとmessageの内容を渡すことができます。
※このチェックをちゃんと機能させるにはControllerにも一工夫が必要なので後続で記載します。

Repository

新規登録用のメソッドを追加していきます。
インターフェース「IDiaryDao.java」に以下を追記します。
※formのimportも忘れずに

// 日記を登録する
int insert(PostForm form);


「DiaryDao.java」に以下を追記します。

	// 日記を登録する
	@Override
	public int insert(PostForm form) {
		// 登録件数を格納
		int count = 0;
		String sql = "INSERT INTO diary(category, title, content, date , update_datetime) "
				+ "VALUES(:category, :title, :content, :date , :update_datetime)";
		// パラメータ設定用Map
		Map<String, Object> param = new HashMap<>();
		param.put("category", form.getCategoryForm());
		param.put("title", form.getTitleForm());
		param.put("content", form.getContentForm());
		param.put("date", form.getDateForm());
		Timestamp timestamp = new Timestamp(System.currentTimeMillis());
		param.put("update_datetime", timestamp);
		count = jdbcTemplate.update(sql, param);
		return count;
	}


★ポイント
パラメータ設定
 検索時と同様登録する内容はMapに格納し、キー値とValues内の変数は紐づいています。

Service

「DiaryService.java」にもメソッドを追加します。

	public int insert(PostForm form) {
		return dao.insert(form);
	}

Controller

「DiaryController.java」に2つメソッドを追加します。

   /**
     * 新規登録へ遷移
     * @param model
     * @return resources/templates/form.html
     */
    @GetMapping("/form")
    public String formPage(
    	Model model
    ) {
        return "form";
    }

    /**
     * 「一覧へ」選択時、一覧画面へ遷移
     * @param model
     * @return resources/templates/list.html
     */
    @PostMapping(path={"/insert", "/form"}, params="back")
    public String backPage(
    	Model model
    ) {
    	return "redirect:/diary";
    }

    /**
     * 日記を新規登録
     * @param postForm
     * @param model
     * @return
     */
    @PostMapping(path="/insert", params="insert")
    public String insert(
    	@Valid @ModelAttribute PostForm form,
    	BindingResult result,
    	Model model
    ) {
    	if(result.hasErrors()) {
    		model.addAttribute("error", "パラメータエラーが発生しました。");
    		return "form";
    	}
    	int count = diaryservice.insert(form);
        model.addAttribute("postForm", form);
        return "redirect:/diary";
    }
ポイント

【formPage】
新規登録用の画面へ遷移します。

【backPage】
 登録せずに一覧画面へ戻ります。
 path={“/insert”, “/form”}と指定することで複数のURLとマッピングすることが可能です。「/form」は後程の詳細画面の方で使用します。

【insert】
@PostMapping(path=”/insert”, params=”insert”)
 Post(insert)用のマッピングをしています。
 一覧表示する際はパスのみ引数を設定していましたが「params=”insert”」も今回追加しています。パスで設定したURLが同一だけど複数メソッドを用意したいときに指定します。「backPage」のparamsと値を変えることで分岐しています。

@Valid @ModelAttribute PostForm form
 @Valid:formにバインドされたデータをチェックする
 @ModelAttribute:画面からの入力データをformクラスに紐づける(バインドする)

BindingResult result
 @Validでチェックした結果を受け取ります。「result.hasErrors()」これでエラーがある場合、ない場合で分岐することが可能です。エラーが発生した際は、メッセージと共にform画面へ遷移させています。

return “redirect:/diary”;
 リダイレクト二重送信防止になります。

画面作成

まずは「list.html」を修正していきます。

      <form method="GET" th:action="@{/diary/form}">
        <div class="footer-button row justify-content-end">
          <div class="col-1 m-1">
            <button type="submit" class="btn btn-primary">日記作成</button>
            <input type="hidden"  name="isUpdate" value=false>
          </div>
        </div>
      </form>


th:action=”@{/diary/form}”を追加しています。
<input type=”hidden”  name=”isUpdate” value=false>は別章で使用します。

form用のHTMLを作成します。
「template/form.html」

<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <title>日記アプリ</title>
  <!-- bootstrapを読み込む -->
  <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}">
  <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>
  <!-- jqueryを読み込む -->
  <script type="text/javascript" th:src="@{/js/jquery-3.6.0.min.js}"></script>
  <!-- bootstrap-datepickerを読み込む -->
  <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap-datepicker.min.css}">
  <script type="text/javascript" th:src="@{/js/bootstrap-datepicker.min.js}"></script>
  <script type="text/javascript" th:src="@{/js/bootstrap-datepicker.ja.min.js}"></script>
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-light" style="background-color:#CCFFFF;">
      <span class="navbar-brand">
        <strong>日記アプリ</strong>
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-book" viewBox="0 0 16 16">
          <path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/>
        </svg>
      </span>
      <div class="collapse navbar-collapse justify-content-end">
        <ul class="navbar-nav">
          <li class="nav-item ">
            <span class="navbar-text" th:text="ログインユーザ"></span>
          </li>
        </ul>
        <form method="POST">
          <button type="submit" class="btn btn-link">ログアウト</button>
        </form>
      </div>
    </nav>
    <div class="container-fluid">
      <h2 th:if="${error}" style="color: red;">[[${error}]]</h2>
      <div class="create m-2">
        <form id="form" class="needs-validation" novalidate method="POST" th:action="@{/diary/insert}" onsubmit="return check()">
          <p class="p-1 h4" th:text="新規作成"></p>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="dateForm">日付</label>
              <input type="text" class="form-control" name="dateForm" id="date_sample" required>
              <div class="invalid-feedback">必須入力です</div>
            </div>
          </div>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="categoryForm">分類</label>
              <select id="categoryForm" class="form-select col-8" name="categoryForm" required>
                <option selected></option>
                <option value="1">仕事</option>
                <option value="2">趣味</option>
                <option value="3">その他</option>
              </select>
              <div class="invalid-feedback">必須入力です</div>
            </div>
          </div>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="title">題名</label>
              <input type="text" class="form-control" name="titleForm" id="title" required>
              <div class="invalid-feedback">25文字以内で入力してください</div>
            </div>
          </div>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="floatingTextarea">内容</label>
              <textarea class="form-control" placeholder="日記の内容" name="contentForm" style="height: 200px"></textarea>
            </div>
          </div>
          <div class="row justify-content-end">
            <div class="col-1">
              <button type="submit" class="btn btn-primary" name="insert" onClick="btn='insert'">登録</button>
            </div>
            <div class="col-1">
              <button type="submit" class="btn btn-light" name="back" onClick="btn='back'">一覧へ</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </body>

  <script>
    $('#date_sample').datepicker({
      format: 'yyyy/mm/dd',
      language: 'ja',
      autoclose: true,
    });

    const check = (event) => {
	  // 一覧ボタン押下した場合はチェックしない
	  if (btn === 'back'){
		return true;
	  }

      const form = document.getElementById('form');
      if (!form.checkValidity()){
        form.classList.add('was-validated');
        return false;
      } else {
	    return true;
      }
    };
  </script>
</html>
ポイント

Bootstrap Validation
 入力値のチェックにBootstrapのバリデーション機能を使用しております。
 ※詳しくは公式サイトへ
 https://getbootstrap.jp/docs/5.0/forms/validation/
 classに「invalid-feedback」と指定しているのはエラーメッセージとなります。
 必須チェックのみ実施しています。form部品内にrequiredと記載している箇所が対象です。これはHTMLの機能となり、入力されているかどうか確認します。
 (Bootstrapなしでもこれだけで必須チェックをし、ブラウザデフォルトのエラーメッセージを表示することが可能)
 ※題名の25文字制限をあえてかけていない理由はControllerで記載したformチェックが実施されるか確認するためです。

<h2 th:if=”${error}” style=”color: red;”>[[${error}]]</h2>
 tymeleafのif機能を使用して、errorという値が存在すれば表示するという意味になります。
 errorとはどこから来るかというとControllerで記載した
 「model.addAttribute(“error”, “パラメータエラーが発生しました。”);」
 これになります。なのでサーバー側でパラメータエラーが発生した際はこのh2タグの内容が
 表示されます。
 また、thymeleafは「th:text」を使用しなくてもタグ内に[[${変数名}]]という形で埋め込む
 ことが可能です。

<form id=”form” class=”needs-validation” novalidate method=”POST” th:action=”@{/diary/insert}” onsubmit=”return check()”>
 onsubmitでsubmitする前に関数を実行しています。
 その関数の戻り値がtrueであればsubmit実行、falseであれば実行しないという動きになります。
 check関数はscriptタグ内に記載しています。
 「一覧へ」ボタン押下時はチェックしないよう工夫もしています。

動作確認

アプリを動かし、動作確認していきます。

①画面表示
新規登録ボタンを選択して、画面が表示されることを確認します。

②入力チェック(フロントエンド側)
何も入力しないで登録ボタンを選択します。
入力チェックに引っ掛かることを確認します。

③入力チェック(サーバー側)
必須項目に値を入力します。
題名には25文字を超えて入力し、登録ボタンを選択します。
あえて画面側で制御しなかったチェックです。
Controllerでのエラー処理が動作するかを確認します。

④登録
値を入力し、登録ボタンを選択します。
データが追加されることを確認します。
※画面だけでなく、DBを中身も確認してみましょう。

⑤登録画面から一覧へ戻る
一覧へボタンを選択して、一覧画面に戻れるかを確認します。


ここまで上手くいけば、登録処理は完了です!

日記詳細

一覧画面の詳細ボタンから登録した内容を詳細を確認できるようにします。
詳細画面からさらに編集ボタン押下することで内容を変更できるようにしていきます。
※編集は次章で実施していきます。

Repository

詳細画面へ表示する際のデータを再度検索します。
(一覧画面で検索したデータをもって再検索しなくても可能ですが、あえて再検索します。)

メソッドを追加していきます。
インターフェース「IDiaryDao.java」に以下を追記します。

	// idを指定して日記を1件取得
	Diary findById(int id);


「DiaryDao.java」に以下メソッドを追記します。

	@Override
	public Diary findById(int id) throws  IncorrectResultSizeDataAccessException {
		String sql = "SELECT d.id, d.category, d.title, d.content, TO_CHAR(d.date, 'YYYY/MM/DD') AS date, d.update_datetime, c.name "
				+ "FROM diary AS d INNER JOIN category_code AS c ON d.category = c.cd "
				+ "WHERE d.id = :id";

		// パラメータ設定用Map
		Map<String, Object> param = new HashMap<>();
		param.put("id", id);
		// 一件取得
		Map<String, Object> result = jdbcTemplate.queryForMap(sql, param);
		Diary diary = new Diary();
		diary.setId((int)result.get("id"));
		diary.setCategory((String)result.get("category"));
		diary.setTitle((String)result.get("title"));
		diary.setContent((String)result.get("content"));
		diary.setDate((String)result.get("date"));
		diary.setUpdate_datetime((Timestamp)result.get("update_datetime"));
		diary.setName((String)result.get("name"));
		
		return diary;
	}
ポイント

throws IncorrectResultSizeDataAccessException
 「queryForMap」はデータ1件を取得するためのメソッドです。
 1件もデータが取得できない場合はExceptionが発生するため例外処理を記載しています。

Service

「DiaryService.java」に以下を追記していきます。

    public Diary findById(int id) {
    	try {
    		return dao.findById(id);
    	} catch(IncorrectResultSizeDataAccessException e) {
    		return null;
    	}
	}
ポイント

エラー処理
 Daoクラスでthrowしたエラーをtry〜catchで受け取っています。
 1件もデータを取得できない場合はnullを返すようにしています。

Controller

「DiaryController.java」に追記していきます。

    /**
     * 一件タスクデータを取得し、詳細ページ表示
     * @param id
     * @param model
     * @return resources/templates/detail.html
     */
    @GetMapping("/{id}")
    public String showUpdate(
        @PathVariable int id,
        Model model) {
    	//Diaryを取得
    	Optional<Diary> diaryOpl = Optional.ofNullable(diaryservice.findById(id));

    	//NULLかどうかのチェック
    	if(diaryOpl.isPresent()) {
			model.addAttribute("diary", diaryOpl.get());
			return "detail";
		} else {
			model.addAttribute("error", "対象データが存在しません");
			return "detail";
		}
    }
ポイント

@GetMapping(“/{id}”)
public String showUpdate(
@PathVariable int id,

  @PathVariableを利用することで、URLに含まれる動的なパラメータを受け取ることができます。受け取った値を@GetMappingで「{パラメータ名}」という形で指定することが可能です。

Optional diaryOpl = Optional.ofNullable(diaryservice.findById(id));
  OptionalとはJava8から提供されている機能でNULLを許容し、簡単にチェックできるものです。これを利用することでNULLチェックを忘れずに実装することができます。
参考公式ドキュメント) https://docs.oracle.com/javase/jp/8/docs/api/java/util/Optional.html

if(diaryOpl.isPresent())
 NULLでなければ、詳細画面へ。NULLであればメッセージが表示されるように
 しています。

画面作成

まずは「list.html」を修正していきます。
「詳細」ボタンです。href属性を追加します。

<a class="btn btn-primary" th:href="@{/diary/{id}(id=${item.id})}">詳細</a>


詳細画面へ作成していきます。
「templates/detail.html」を作成します。

<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
  <title>日記アプリ</title>
  <!-- bootstrapを読み込む -->
  <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}">
  <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-light" style="background-color:#CCFFFF;">
      <span class="navbar-brand">
        <strong>日記アプリ</strong>
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-book" viewBox="0 0 16 16">
          <path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/>
        </svg>
      </span>
      <div class="collapse navbar-collapse justify-content-end">
        <ul class="navbar-nav">
          <li class="nav-item ">
            <span class="navbar-text" th:text="ログインユーザ"></span>
          </li>
        </ul>
        <form method="POST">
          <button type="submit" class="btn btn-link">ログアウト</button>
        </form>
      </div>
    </nav>
    <div class="container-fluid">
      <div th:if="${error}">
        <h2 th:if="${error}" style="color: red;">[[${error}]]</h2>
        <p>管理者にご連絡ください</p>
        <form method="POST" th:action="@{/diary/form}">
          <button type="submit" name="back" class="btn btn-light">一覧へ</button>
        </form>
      </div>
      <div  th:if="${diary}"  class="create m-2">
        <form method="POST" th:action="@{/diary/form}" th:object="${diary}">
          <p class="p-1 h4">詳細</p>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="dateForm">日付</label>
              <p><span th:text="${diary.date}"></span></p>
            </div>
          </div>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="categoryForm">分類</label>
              <p><span th:text="${diary.name}"></span></p>
            </div>
          </div>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="title">題名</label>
              <p><span th:text="${diary.title}"></span></p>
            </div>
          </div>
          <div class="row justify-content-center">
            <div class="form-group col-8">
              <label for="floatingTextarea">内容</label>
              <p><span th:text="${diary.content}" style="white-space: pre-wrap;"></span></p>
            </div>
          </div>
          <div class="row justify-content-end">
            <div class="col-1">
              <button type="submit" name="update" class="btn btn-primary">編集</button>
            </div>
            <div class="col-1">
              <button type="submit" name="back" class="btn btn-light">一覧へ</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </body>
</html>
ポイント

エラーによる分岐
 エラーが存在するばエラーメッセージを、日記データが存在すれば詳細を表示する
 というように「th:if」を用いて処理しています。

動作確認

画面で動作確認をしていきます。

①一覧画面から詳細ボタンを選択し内容確認

②改行テスト
内容には改行入力することが可能です。改行されて表示されるか確認してみましょう。
※「white-space: pre-wrap」を指定しているため、改行も正しく表示されます。

③エラーテスト
詳細ボタン選択した際に対象データが存在しない
一覧画面表示(データが表示されている状態)でDBより直接対象1件削除します。
その後詳細ボタンを選択します。

画面状は表示されているが、DB状からは改行テストのデータを削除

詳細画面から一覧へ戻る
一覧へボタンを選択して、一覧画面に戻れるかを確認します。

ここまで上手くいけば、詳細確認は完了です!

最後に

だいぶ長くなってしまいましたが、以上で登録処理と詳細確認は完了です。
次回は編集処理と削除処理を書いていきます。

タイトルとURLをコピーしました