SpringBootで時間取得の際にUTCからJSTへ変換し日付の形式を定型する方法を解説しています

SpringBoot

はじめに

前回はCRUD処理が出来る所までアプリケーションを作成できました。今回はこのアプリケーションをたたきとして、改修していきます。具体的には前回も挙げていた以下です。

  • 時間取得がUTCなのでJSTへ変換する処理
  • 学習開始ボタンを押して学習開始が出来るようにする※こちら実装するにはeditページを作成しないと成立しないので、そちらも作成する
  • 一覧画面など、日付と時間が一緒になっていて見づらいので見やすい画面に作成する
  • 現状は開始時間と終了時間を入力しないと先に進めないので、スキーマを含めて構成を練り直す

こちらが前回の記事となっております。まだお読みでない方はこちらからどうぞ!!

学習時間管理アプリ#3となります。

この記事を読むと日付と時間を打刻する仕組みを理解する事ができます。

コーディング

editページの作成

まず、こちらのページを作成する意図ですが、現在は学習時間を記録する事は出来ますが学習開始時間と学習終了時間を一緒に入力しないと完了ボタンを押して先に進む事が出来ません。

そのため、この仕様を修正する必要があります。具体的には学習時間を記録する時は、学習開始だけを打刻して記録する。そこから終了時間になったら学習終了を打刻する。このようにしてDBに登録が出来るように処理を実装します。

学習記録を編集する場合は、基本的に開始時間と終了時間をどちらも調整できる画面がよいので、現状の登録フォームを使いまわすために、画面をわけていくという意図です。

編集ページを作成する/edit.html

「src/main/resources/templates/studytime」の下に「edit.html」を作成します。

「edit.html」を編集していきます。

edit.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>学習時間の編集</title>
	<script>
		function setCurrentTime(field) {
			const now = new Date();
			const formattedTime = now.toISOString().slice(0,16);
			document.getElementById(field).value = formattedTime;
		}
	</script>
</head>
<body>
	<h1>学習時間の編集</h1>
	<form th:action="@{/studytime/{id}(id=${studyTimeObject.studyTimesId})}" th:object="${studyTimeObject}" method="post">
		<label>開始時間:</label>
		<input type="datetime-local" id="startTime" th:field="*{startTime}" required />
		<button type="button" onclick="setCurrentTime('startTime')">学習開始</button>
		
		<label>終了時間:</label>
		<input type="datetime-local" id="endTime" th:field="*{endTime}" required />
		<button type="button" onclick="setCurrentTime('endTime')">学習終了</button>
		
		<button type="submit">変更</button>
	</form>
	
	<a href="/studytime">戻る</a>
</body>
</html>
StudyTimeControllerを編集する

「StudyTimeController.java」を下記のように編集します。更新している部分はreturnで返している値をeditに返すように更新しています。

StudyTimeController.java
	@GetMapping("/{id}/edit")
	public String editStudyTimeForm(@PathVariable Long id, Model model) {
		StudyTime studyTime = studyTimeService.getStudyTime(id);
		model.addAttribute("studyTimeObject", studyTime);
		return "studytime/edit";
	}

時間取得がUTCなのでJSTへ変換する処理

ビジネスロジックを組んでいるのは「StudyTimeService.java」なので、この中の新しく学習時間を作成しているメソッドの中に組み込んであげるとよいかなと思います。

そのためには、UTCで取得している時間をJSTへ変換するメソッドを作成する必要があります。

「StudyTimeService.java」を編集します。

StudyTimeService.java
	// UTCからJSTへ変換する処理
	private 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();
	}

この変換するメソッドを活用し、現在の時刻を取得できるように書き換えます。

StudyTimeService.java
	// 学習時間の打刻セットロジック
	public void saveStudyTime(StudyTime studyTime) {

		if (studyTime.getStartTime() == null) {
			studyTime.setStartTime(convertToJST(LocalDateTime.now()));
		}
		
		if (studyTime.getEndTime() == null) {
			studyTime.setEndTime(convertToJST(LocalDateTime.now()));
		}
		
		studyTimeRepository.save(studyTime);
	}

これで引数で受け取ったLocalDateTimeをUTCからJSTへ変換する事が出来るようになりました。以下は「StudyTimeService.java」の全てのコードです。確認ください。

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) {

		if (studyTime.getStartTime() == null) {
			studyTime.setStartTime(convertToJST(LocalDateTime.now()));
		}
		
		if (studyTime.getEndTime() == null) {
			studyTime.setEndTime(convertToJST(LocalDateTime.now()));
		}
		
		studyTimeRepository.save(studyTime);
	}
	
	// UTCからJSTへ変換する処理
	private 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 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);
	}
}

画面でも確認してみます。

しっかり取得出来てますね!いい感じです。

学習開始ボタンを押して学習開始が出来るようにする

まずは何をやりたいのかを明確にしていきましょう!

  1. viewからのデータ送信
  • ユーザーがフォームで学習開始または終了のボタンをクリックすると、startTimeまたはendTimeが設定されたStudyTimeオブジェクトがコントローラーに送信されます。

2.コントローラーでの処理

  • コントローラーのcreateStudyTimeメソッドにおいて、受け取ったStudyTimeオブジェクトには、ユーザーが入力した日付と時間が含まれています。この時点で、これらの時間はUTCとして処理されます(もともとサーバー側の時間としてUTCが使われているため)

3.UTCからJSTへ変換

  • 受け取ったstartTimeおよびendTimenullでないことを確認し、convertToJSTメソッドを呼び出して、UTCからJSTに変換します。この変換によって、時間が日本標準時に適した形式になります。

4.DBへ保存

  • 変換されたJSTの時間がStudyTimeオブジェクトにセットされ、最終的にDBに保存されます。
viewからのデータ送信

「studyTimeForm.html」を編集します。

studyTimeForm.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}"  method="post">
		<button type="submit" name="action" value="start">学習開始</button>
		
		<button type="submit" name="action" value="end">学習終了</button>
	</form>
	
	<a href="/studytime">戻る</a>
</body>
</html>
コントローラーでの処理

「StudyTimeController.java」を編集します。

StudyTimeController.java
	@PostMapping
	public String createStudyTime(@ModelAttribute StudyTime studyTime, @RequestParam String action) {
		
		if ("start".equals(action)) {
			studyTime.setStartTime(LocalDateTime.now());
			studyTime.setEndTime(null);
		} else if ("end".equals(action)) {
			studyTime.setEndTime(LocalDateTime.now());
		}
		
		studyTimeService.saveStudyTime(studyTime);
		return "redirect:/studytime";
	}
UTCからJSTへ変換されDBへ保存

「StudyTimeService.java」を編集します。

StudyTimeService.java
	// 学習時間の打刻セットロジック
	public void saveStudyTime(StudyTime studyTime) {

		if (studyTime.getStartTime() != null) {
			studyTime.setStartTime(convertToJST(LocalDateTime.now()));
		}
		
		if (studyTime.getEndTime() != null) {
			studyTime.setEndTime(convertToJST(LocalDateTime.now()));
		}
		
		studyTimeRepository.save(studyTime);
	}

コードが記述出来たので、ブラウザで確認してみましょう。表示された画面の「学習開始」ボタンを早速押下してみると、以下のエラーが発生。

HTML
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sat Oct 12 13:59:57 JST 2024
There was an unexpected error (type=Internal Server Error, status=500).
could not execute statement [Column 'end_time' cannot be null] [insert into study_times (end_time,start_time) values (?,?)]; SQL [insert into study_times (end_time,start_time) values (?,?)]; constraint [null]
org.springframework.dao.DataIntegrityViolationException: could not execute statement [Column 'end_time' cannot be null] [insert into study_times (end_time,start_time) values (?,?)]; SQL [insert into study_times (end_time,start_time) values (?,?)]; constraint [null]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:290)
	at va:184)
	at .springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.ja省略:255)
alina.core.ApplicationFilterChain.internaent.internal.DefaultPersisltPersistEventListener.java:379)

	... 115 more

[Column ‘end_time’ cannot be null]とあるようにnullになっているから、DBに登録できないよ!言われています。なので、スキーマの定義を修正していく必要があります。

現状は開始時間と終了時間を入力しないと先に進めないので、スキーマを含めて構成を練り直す

スキーマの変更

MySQL Command LineでDDLを流してスキーマを変更する。以下はコマンドラインで入力したコマンドです。これにより、nullを許容する事が出来るので、エラーは回避できるはずです。

MySQL Command Line
-- DBを選択する
USE prog_track_db

-- スキーマを変更する
ALTER TABLE study_times MODIFY start_time DATETIME NULL;
ALTER TABLE study_times MODIFY end_time DATETIME NULL;
1レコードに開始時間と終了時間が入るように修正をする

現在は下記のように開始時間と終了時間が違うレコードに入っています。こちらを回避するための実装を考えます。


1. 学習時間の状態管理

StudyTimeオブジェクトの状態を管理するために、学習が開始された時点で新しいレコードを作成し、その後、同じレコードに学習終了時間を設定できるようにします。

2. 学習開始時の処理

学習を開始する際には、StudyTimeオブジェクトを新規作成し、startTimeを設定してDBに保存します。この際、endTimenullのままにしておきます。

3. 学習終了時の処理

学習が終了した時には、学習を開始した時に作成したStudyTimeオブジェクトを取得し、そのオブジェクトのendTimeを設定してDBに更新します。

「StudyTimeController.java」を修正していきます。

StudyTimeController.java
	@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);
			studyTimeService.saveStudyTime(studyTime);
		} else if ("end".equals(action)) {
			// 直近の学習セッションを取得する
			StudyTime lastStudyTime = studyTimeService.getLastStudyTime();
			
			if (lastStudyTime != null) {
				lastStudyTime.setEndTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
				studyTimeService.saveStudyTime(lastStudyTime);
			}
		}
		
		return "redirect:/studytime";
	}

「StudyTimeService.java」に新しいメソッドを追加します。saveStudyTimeメソッドの処理内容を消去する。この処理を消去する理由はデバッグ上ではJSTで変換された時間が入ってくるのにブラウザ上では、UTCとなってしまうためです。

なぜ、そうなるかというとDBの値を確認するとUTCで保存されていたからという理由です。このように処理ではしっかり値が入るが画面確認すると思った値と違う場合はDBの値を確認して、DBの値が違うならば、原因をDBの保存しているメソッドに絞るというのがよいアプローチになりそうです。

StudyTimeService.java
	// 学習時間の打刻セット
	public void saveStudyTime(StudyTime studyTime) {

		studyTimeRepository.save(studyTime);
	}
	
	// 最後のレコードを取得する
	public StudyTime getLastStudyTime() {
		List<StudyTime> studyTimes = studyTimeRepository.findAll();
		return studyTimes.isEmpty() ? null : studyTimes.get(studyTimes.size() - 1);
	}

コードの修正が完了したので、ブラウザで確認してみましょう!

一覧画面など、日付と時間が一緒になっていて見づらいので見やすい画面に作成する

Thymeleafを使用して取得したデータを加工して表示する事が出来ます。「#dates.format」を使用して変換する事も可能みたいなのですが、バージョンによってはうまくいかないようで…筆者もそちら側でした。エラーとなってしまったので…。

「java.time.format.DateTimeFormatter」を使用して変換しました。

「studyTimeList.html」を編集します。

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>
	<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>

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

これで学習時間が打刻出来るようになりました。

おわりに

まだまだ、改修の余地はあります。例えば、開始時間を押してるのに、また開始時間を押したらどうなるかや編集ボタンの画面の刷新なども必要です。機能的にまだまだ増やせそうなので、このまま改修をしていきたいと考えております。

お読みいただきありがとうございました。

コメント