SpringBootで年月毎に表示できるようにJPQLを作成し、セレクトボックスで実装する方法を解説します

SpringBoot

はじめに

学習時間管理アプリ#6です。

実装する過程で、当月や先月など、月初から月末までを値で取得し、抽出するクエリを作成します。そのメソッドを使用して、セレクトボックスで実装をしていきます。

セレクトボックスを使用して年月を実装するなどの情報をお探しの方は、少し参考になるかもしれません。

ハンズオンで作成されている方は、前回の続きからの実装となりますので、まだお読みでない方は下記のリンクからお読みください。

前回の記事はこちらからどうぞ!!

改修要件

一覧画面

  • 選択した表示年月のみを画面に表示する事
  • 詳細画面のロジックを修正する

選択した表示年月のみを画面に表示できるようにする

目的:

  • 現在は学習を入力した情報がすべて1つの画面で表示されてしまうため、改修する

具体的には、当月や先月の学習時間を絞り込み、その内容を表示する。表示する際はプルダウンで過去の年/月を表示し、選択する事によって画面の切り替えが出来るようになる。

処理内容:

  • 当月や先月など、月ごとに画面を表示するためにSQLのクエリを作成する事
  • viewでセレクトボックスの作成をする
StudyTimeRepository.javaの編集

絞り込みを行い、画面を表示させるためのSQLを作成します。以下のように編集しましょう。

StudyTimeRepository.java
package com.app.progTrack.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.app.progTrack.entity.StudyTime;

public interface StudyTimeRepository extends JpaRepository<StudyTime, Long>{
	 
	 // 学習時間を計算する
	 @Query("SELECT SUM(TIMESTAMPDIFF(MINUTE, s.startTime, s.endTime)) FROM StudyTime s WHERE MONTH(s.startTime) = :month AND YEAR(s.startTime) = :year AND s.endTime IS NOT NULL")
	 Long findTotalStudyTimeByMonth(@Param("month") int month, @Param("year") int year);
	 
	 // 当月を絞り込む
	 @Query("SELECT s FROM StudyTime s WHERE MONTH(s.startTime) = :month AND YEAR(s.startTime) = :year")
	 List<StudyTime> findAllByMonthAndYear(@Param("month") int month, @Param("year") int year);
}

編集箇所は17~20行目です。

こちらの書き方は5章で図解使用して説明してますので、分からない方は見てみてください。「はじめに」の方にリンク貼ってあります。

上記の書き方は、Spring Data JPAの「JPQL」という書き方です。JPQLはオブジェクト指向のクエリ言語でデータベースのテーブルの代わりにエンティティを操作出来ます。

話が脱線するので、以下の記事で少しまとめましたので気になる方は見てみてください。

TECH-STEP | 現役エンジニアが日々のアウトプットを発信していきます!
【spring-jpa】一般的な書き方明示的なselect句の使用場面な/

StudyTimeService.javaの編集

上記のRepositoryで定義したクエリを使用してメソッドを定義します。

StudyTimeService.java
package com.app.progTrack.service;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;

import org.springframework.stereotype.Service;

import com.app.progTrack.entity.StudyTime;
import com.app.progTrack.repository.StudyTimeRepository;

@Service
public class StudyTimeService {
	private final StudyTimeRepository studyTimeRepository;
	
	public StudyTimeService(StudyTimeRepository studyTimeRepository) {
		this.studyTimeRepository = studyTimeRepository;
	}
	
	// 学習時間の打刻セット
	public void saveStudyTime(StudyTime studyTime) {

		studyTimeRepository.save(studyTime);
	}
	
	// セレクトボックスで選択された月のデータを取得する
	List<StudyTime> findAllByMonthAndYear(int month, int year) {
		return studyTimeRepository.findAllByMonthAndYear(month, year);
	}
	
	// 月毎の累計時間を取得する
	public long getTotalStudyTimeForMonth(int month, int year) {
		Long total = studyTimeRepository.findTotalStudyTimeByMonth(month, year);
		return total != null ? total : 0; // nullの場合は0を返す
	}
	
	// 最後のレコードを取得する
	public StudyTime getLastStudyTime() {
		List<StudyTime> studyTimes = studyTimeRepository.findAll();
		return studyTimes.isEmpty() ? null : studyTimes.get(studyTimes.size() - 1);
	}
	
	// UTCからJSTへ変換する処理
	public LocalDateTime convertToJST(LocalDateTime utcDateTime) {
		// LocalDateTimeをUTCのZoneTimeに変換
		ZonedDateTime utcZoned = utcDateTime.atZone(ZoneId.of("UTC"));
		// UTCからJSTへ同じ瞬間の時間を取得
		ZonedDateTime jstZonde = utcZoned.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
		// LocalDateTimeに変換する
		return jstZonde.toLocalDateTime();
	}
	
	// 学習中の判定をする
	public boolean isLiarningActive() {
		StudyTime lastStudyTime = getLastStudyTime(); // 最後のレコードを取得
		// lastStudyTimeがnullではない 且つ getIsLearningメソッドがtrueの場合に戻り値はtrueが返る
		return lastStudyTime != null && Boolean.TRUE.equals(lastStudyTime.getIsLearning());
	}
	
	// 一覧ページ表示用
	public List<StudyTime> getAllStudyTimes() {
		return studyTimeRepository.findAll();
	}
	// 編集用
	public StudyTime getStudyTime(Long id) {
		return studyTimeRepository.findById(id).orElse(null);
	}
	// 削除用
	public void deleteStudyTime(Long id) {
		studyTimeRepository.deleteById(id);
	}
}

編集箇所は27~30行目です。解説します。

StudyTimeService.java
// セレクトボックスで選択された月のデータを取得する
	public List<StudyTime> findAllByMonthAndYear(int month, int year) {
		return studyTimeRepository.findAllByMonthAndYear(month, year);
	}

「findAllByMonthAndYear」指定された月と年に一致するデータを検索するためのメソッドです。

StudyTimeController.javaの編集

大幅に「listStudyTimes」メソッドを編集しています。セレクトボックス用を作成するためのメソッドも追加しています。

StudyTimeController.java
package com.app.progTrack.controller;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.stereotype.Controller;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.app.progTrack.entity.StudyTime;
import com.app.progTrack.service.StudyTimeService;

@Controller
@RequestMapping("/studytime")
public class StudyTimeController {
	private final StudyTimeService studyTimeService;
	
	public StudyTimeController(StudyTimeService studyTimeService) {
		this.studyTimeService = studyTimeService;
	}
	
	@GetMapping
	public String listStudyTimes(Model model, @RequestParam(value = "month", required = false) Integer month, @RequestParam(value = "year", required = false) Integer year) {
		
		// 現在の月と前月の累計時間を取得する
		int currentMonth = LocalDate.now().getMonthValue();
		int currentYear = LocalDate.now().getYear();
		
		// 初期表示は当月の学習時間を取得する
		if (month == null || year == null) {
			month = currentMonth;
			year = currentYear;
		}
		
		// 学習時間を取得する
		List<StudyTime> studyTimes = studyTimeService.findAllByMonthAndYear(month, year);
		model.addAttribute("studyTimes", studyTimes);
		
		// 当月の学習時間を算出
		double _currentMonthTotal = studyTimeService.getTotalStudyTimeForMonth(month, year) / 60.0;
		
		// 現在の月 - 前月 = 0 なら 12を返す 0でないなら-1を引いた数字を返す
		int lastMonth = month - 1 == 0 ? 12 : month -1;
		int lastYear = month - 1 == 0 ? month - 1 : year;
		// 前月の学習時間を算出
		double _lastMonthTotal = studyTimeService.getTotalStudyTimeForMonth(lastMonth,  lastYear) / 60.0;
		
		// 小数点第二位を切り上げてから小数点第一位まで表示する
		String currentMonthTotal = String.format("%.1f", Math.ceil(_currentMonthTotal * 10) / 10);
		String lastMonthTotal = String.format("%.1f", Math.ceil(_lastMonthTotal * 10) / 10);
		
		model.addAttribute("currentMonthTotal", currentMonthTotal);
		model.addAttribute("lastMonthTotal", lastMonthTotal);
		
		// セレクトボックス用のデータ
		model.addAttribute("months", createMonthList());
		model.addAttribute("years", createYearList(currentYear));
		model.addAttribute("currentYear", year);
		model.addAttribute("currentMonth", month);
		
		return "studytime/studyTimeList";
	}
	
	// セレクトボックス用:月選択用
	private List<Integer> createMonthList() {
		return IntStream.rangeClosed(1, 12).boxed().collect(Collectors.toList());
	}
	
	// セレクトボックス用:日選択用
	private List<Integer> createYearList(int currentYear) {
		return IntStream.rangeClosed(currentYear - 5, currentYear).boxed().collect(Collectors.toList());
	}
	
	@GetMapping("/new")
	public String newStudyTimeForm(Model model) {
		
		// ベースとなる日付を取得する この場合はJST取得の日付 現在の時刻取得用
		LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
		// どういう型で用意するかを定義
		DateTimeFormatter dateFormateer = DateTimeFormatter.ofPattern("MM/dd"); // 日付で形成する
		DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); // 時間で形成する
		// 現在の時刻にあてはめる
		String date = currentDateTime.format(dateFormateer);
		String time = currentDateTime.format(timeFormatter);

		model.addAttribute("studyTimeObject", new StudyTime());
		model.addAttribute("date", date);
		model.addAttribute("time", time); //ページが読み込まれた時の初期表示の時刻表示用
		// 曜日を取得する
		model.addAttribute("currentDayOfWeek", currentDateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPAN));
		// 学習中フラグの状態を追加する
		model.addAttribute("isLearningActive", studyTimeService.isLiarningActive());
		return "studytime/studyTimeForm";
	}
	
	// 時刻を返すエンドポイント
	@GetMapping("/current-time")
	@ResponseBody
	public String getCurrentTime() {
		LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
		return currentDateTime.format(formatter);
	}
	
	@PostMapping
	public String createStudyTime(@ModelAttribute StudyTime studyTime, @RequestParam String action) {
		
		if ("start".equals(action)) {
			studyTime.setStartTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
			studyTime.setEndTime(null);
			studyTime.setIsLearning(true);
			studyTimeService.saveStudyTime(studyTime);
		} else if ("end".equals(action)) {
			// 直近の学習セッションを取得する
			StudyTime lastStudyTime = studyTimeService.getLastStudyTime();
			
			if (lastStudyTime != null) {
				lastStudyTime.setEndTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
				lastStudyTime.setIsLearning(false);
				studyTimeService.saveStudyTime(lastStudyTime);
			}
		}
		
		return "redirect:/studytime";
	}
	
	@GetMapping("/{id}/edit")
	public String editStudyTimeForm(@PathVariable Long id, Model model) {
		StudyTime studyTime = studyTimeService.getStudyTime(id);
		model.addAttribute("studyTimeObject", studyTime);
		return "studytime/edit";
	}
	
	@PostMapping("/{id}")
	public String updateStudyTime(@PathVariable Long id, @ModelAttribute StudyTime studyTime) {
		studyTime.setStudyTimesId(id);
		studyTimeService.saveStudyTime(studyTime);
		return "redirect:/studytime";
	}
	
	@PostMapping("/{id}/delete")
	public String deleteStudyTime(@PathVariable Long id) {
		studyTimeService.deleteStudyTime(id);
		return "redirect:/studytime";
	}
}

35~85行目まで、細かく見て編集ください。解説します。

StudyTimeController.java
@GetMapping
	public String listStudyTimes(Model model, @RequestParam(value = "month", required = false) Integer month, @RequestParam(value = "year", required = false) Integer year) {
  • @RequestParam(value = “month”)と設定する事で、urlに数値を埋め込む事が出来る
  • 「required = false」上記のパラメーターは必須ではない設定にする。必須にしたいなら「true」にすればよい
StudyTimeController.java
@GetMapping
	public String listStudyTimes(Model model, @RequestParam(value = "month", required = false) Integer month, @RequestParam(value = "year", required = false) Integer year) {

以下のコードで現在の日付を入れているため、その数字がurlになるイメージでよい。

StudyTimeController.java
// 初期表示は当月の学習時間を取得する
		if (month == null || year == null) {
			month = currentMonth;
			year = currentYear;
		}

今までは一覧ページ表示ようとしてこちらのメソッドを使用していました。こちらもServiceで定義しているメソッドでしたね。このメソッドを使用する事で、StudyTimeに入っている全データを.findAll()で取得していた流れですが…。

StudyTimeController.java
	// 一覧ページ表示用
	public List<StudyTime> getAllStudyTimes() {
		return studyTimeRepository.findAll();
	}

以下の作成したメソッドを使用します。そのため上記のコードは削除でOKです!以下のメソッドは引数を貰って、指定した年月を絞り込むクエリでしたね。引数には、「currentMonth」「currentYear」が入っているため、現在の年月で取得され結果的に、その年月のデータのみが表示されるという流れになります。

StudyTimeController.java
// 学習時間を取得する
		List<StudyTime> studyTimes = studyTimeService.findAllByMonthAndYear(month, year);
		model.addAttribute("studyTimes", studyTimes);

セレクトボックス用のメソッドを別で定義し、こちらをモデルに渡してviewに渡します。

StudyTimeController.java
// セレクトボックス用:月選択用
	private List<Integer> createMonthList() {
		return IntStream.rangeClosed(1, 12).boxed().collect(Collectors.toList());
	}
	
	// セレクトボックス用:日選択用
	private List<Integer> createYearList(int currentYear) {
		return IntStream.rangeClosed(currentYear - 5, currentYear).boxed().collect(Collectors.toList());
	}
  • IntStream.rangeClosed(1, 12)

直感で理解できると思いますが、1~12までを格納するよ!という意味です。

  • IntStream.rangeClosed(currentYear – 5, currentYear)

現在の年月から-5引いた年度を格納するよ!という意味です。


最後にこのパラメーターをmodelに渡してviewで使用できるようにします。

StudyTimeController.java
// セレクトボックス用のデータ
		model.addAttribute("months", createMonthList());
		model.addAttribute("years", createYearList(currentYear));
		model.addAttribute("currentYear", year);
		model.addAttribute("currentMonth", month);
studyTimeList.htmlの編集
studyTimeList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>学習時間一覧</title>
</head>
<body>
	<h1>学習時間一覧</h1>
	<a th:href="@{/studytime/new}">学習を開始する</a>
	
	<form action="/studytime" method="get">
		<select name="year">
			<th:block th:each="year : ${years}">
				<option th:value="${year}" th:text="${year}" th:selected="${year == currentYear}"></option>
			</th:block>
		</select>
		<select name="month">
			<th:block th:each="month : ${months}">
				<option th:value="${month}" th:text="${month}" th:selected="${month == currentMonth}"></option>
			</th:block>
		</select>
		
		<button type="submit">表示</button>
	</form>
	
	<p>当月の学習時間: <sapn th:text="${currentMonthTotal}"></sapn></p>
	<p>前月の学習時間: <sapn th:text="${lastMonthTotal}"></sapn></p>
	<table>
		<tr>
			<th>日付</th>
			<th>開始時間</th>
			<th>終了時間</th>
			<th>操作</th>
		</tr>
		<tr th:each="studyTime : ${studyTimes}">
			<td th:text="${studyTime.startTime.format(T(java.time.format.DateTimeFormatter).ofPattern('yyyy/MM/dd'))}"></td>
			<td th:text="${studyTime.startTime.format(T(java.time.format.DateTimeFormatter).ofPattern('HH時mm分'))}"></td>
			<td th:text="${studyTime.endTime != null ? studyTime.endTime.format(T(java.time.format.DateTimeFormatter).ofPattern('HH時mm分')): ''}"></td>
			<td>
				<a th:href="@{/studytime/{id}/edit(id=${studyTime.studyTimesId})}">編集</a>
				<form th:action="@{/studytime/{id}/delete(id=${studyTime.studyTimesId})}" method="post" style="display: inline;">
					<button type="submit">削除</button>
				</form>
			</td>
		</tr>
	</table>
</body>
</html>

11~24行目まで編集しました。

ここでは単純に渡されたデータを基にセレクトボックスを作成している流れとなります。


それではブラウザで確認してみましょう!!

これにより任意の年月での表示切替が可能になりました。

詳細画面のロジックを修正する

現在の詳細画面では、打刻の変更をするという目的で使用しますが、現行だと非常に使いづらいので、機能面をシンプルにしようと思います。

具体的には、Jsの処理を削除します。

edit.htmlの編集
edit.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>学習時間の編集</title>
</head>
<body>
	<h1>学習時間の編集</h1>
	<form th:action="@{/studytime/{id}(id=${studyTimeObject.studyTimesId})}" th:object="${studyTimeObject}" method="post">
		<label>開始時間:</label>
		<input type="datetime-local" th:field="*{startTime}" required />
		<label>終了時間:</label>
		<input type="datetime-local" th:field="*{endTime}" required />
		<button type="submit">変更</button>
	</form>
	
	<a th:href="@{/studytime(month=${currentMonth}, year=${currentYear})}">戻る</a>
</body>
</html>

ブラウザで確認してみましょう!

おわりに

一通りですが、学習アプリとして使う事が出来るのではないでしょうか。次回からは、Bootstrapを導入して見た目を良くしていきます。次回で学習時間側の実装に関しては仕上げとしようかなと思います。お読みいただきありがとうございました。

コメント