【MySQL+Java】MySQLのDATETIME型はミリ秒指定しないとJavaオブジェクトと1秒の誤差が発生する場合がある

### 【概要】
Spring Bootを使用してJavaで生成したDateオブジェクトをMySQLに登録します。
Javaで生成したDateオブジェクトの時刻と、MySQLに登録したデータの時刻を比較すると1秒の誤差が発生する場合があることをJUnitを使って確認します。
MySQLのDATETIME型でミリ秒指定する場合としない場合の2パターンで確認します。

### 【環境】
Java 17
Spring Boot 3.0.6
JUnit 5
MySQL 8.0.32

### 【事前準備】
MySQLはインストールが必要で、下記コマンドでデータベースとテーブルを作成してください。

```sql
create database mysql_datetime_test

# 同じテーブル名で確認するので、稼働確認する際は一度テーブル削除してから作成をお願いします
# ミリ秒指定する場合
create table user (id integer, date datetime(3));
# ミリ秒指定しない場合
create table user (id integer, date datetime);
```

### 【コード】
[GitHub](https://github.com/nobu12/mysql-second-not-match)にアップロードしています。

#### プロパティファイル
```properties:application.properties
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/mysql_datetime_test
spring.datasource.username=[接続ユーザ名を入力してください]
spring.datasource.password=[接続パスワードを入力してください]
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
```

#### モデルクラス
idとdateのみを持ちます。idを持たせている理由はidを指定してデータ取得するためです。

```java:User.java
package com.example.demo.model;

import java.util.Date;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class User {
	@Id
	private int id;

	private Date date;

	public int getId() {
		return id;
	}

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

	public Date getDate() {
		return date;
	}

	public void setDate(Date date) {
		this.date = date;
	}
}
```

#### リポジトリークラス
継承しているCrudRepositoryクラスのメソッドを使用するため、メソッドは作りません。
```java:UserRepository.java
package com.example.demo.repository;

import org.springframework.data.repository.CrudRepository;

import com.example.demo.model.User;

public interface UserRepository extends CrudRepository<User, Integer> {

}
```

#### テストクラス
Javaで生成したDateと、MySQLにデータ登録後に取得したDateをフォーマットして秒単位まで比較することで同じ時刻になっているかどうか確認します。
ミリ秒を指定しない場合のテーブル定義では、タイミングによってテストOKとなることがあるため同じテストケースを1000回実行して一部テストNGとなることを確認します。
時刻の結果を把握するため、コンソール出力する処理も入れています。

```java:
package com.example.demo.repository;

import static org.junit.jupiter.api.Assertions.*;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.junit.jupiter.api.RepeatedTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.demo.model.User;

@SpringBootTest
public class UserRepositoryTest {

	@Autowired
	private UserRepository userRepository;

	SimpleDateFormat secondSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	SimpleDateFormat miliSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

	@RepeatedTest(1000)
	public void Javaで生成したDateとMySQLに登録したDateを比較する() throws ParseException {
		Date date = new Date();

		User user = new User();
		user.setId(1);
		user.setDate(date);
		userRepository.save(user);

		// コンソール出力用
		String millformatedJavaDate = miliSdf.format(date);
		String millformatedMySqlDate = miliSdf.format(userRepository.findById(1).get().getDate());
		System.out.println(millformatedJavaDate + "\t" + millformatedMySqlDate);
		
		// JUnit用
		String secondformatedJavaDate = secondSdf.format(date);
		String secondformatedMySqlDate = secondSdf.format(userRepository.findById(1).get().getDate());
		assertEquals(secondformatedJavaDate, secondformatedMySqlDate);
	}
}
```


### 【実行結果】
#### ミリ秒指定する場合

##### JUnit結果
全てのテストケースでテストOKとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/97849f88-2d51-0b5b-7df1-9b6eaeafc3ac.png)

##### コンソール出力結果（一部抜粋）
時刻がミリ秒単位でも同じことが分かります。
```text
2023-05-27 22:01:28.885	2023-05-27 22:01:28.885
2023-05-27 22:01:28.887	2023-05-27 22:01:28.887
2023-05-27 22:01:28.890	2023-05-27 22:01:28.890
2023-05-27 22:01:28.893	2023-05-27 22:01:28.893
2023-05-27 22:01:28.895	2023-05-27 22:01:28.895
2023-05-27 22:01:28.898	2023-05-27 22:01:28.898
2023-05-27 22:01:28.901	2023-05-27 22:01:28.901
2023-05-27 22:01:28.902	2023-05-27 22:01:28.902
2023-05-27 22:01:28.905	2023-05-27 22:01:28.905
2023-05-27 22:01:28.908	2023-05-27 22:01:28.908
```

#### ミリ秒指定しない場合

##### JUnit結果
一部のテストケース(523件)でテストNGとなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/b7f556f6-c8e6-8e68-469c-8f666770af20.png)

##### コンソール出力結果（一部抜粋）
秒数だけに注目すると1秒の誤差があるケースがあるのが分かります。
（JUnitでは秒単位までの比較をしているので、秒単位で誤差のあるテストケースのみテストNGとなります）
MySQLから取得したデータには時刻がミリ秒まで入っていないことが分かります。
```text
2023-05-27 22:04:11.984	2023-05-27 22:04:12.000
2023-05-27 22:04:11.987	2023-05-27 22:04:12.000
2023-05-27 22:04:11.989	2023-05-27 22:04:12.000
2023-05-27 22:04:11.995	2023-05-27 22:04:12.000
2023-05-27 22:04:11.998	2023-05-27 22:04:12.000
2023-05-27 22:04:12.000	2023-05-27 22:04:12.000
2023-05-27 22:04:12.003	2023-05-27 22:04:12.000
2023-05-27 22:04:12.005	2023-05-27 22:04:12.000
2023-05-27 22:04:12.008	2023-05-27 22:04:12.000
2023-05-27 22:04:12.010	2023-05-27 22:04:12.000
```

### 【参考サイト】
[Spring Boot JPA で MySQL データアクセス - 公式サンプルコード](https://spring.pleiades.io/guides/gs/accessing-data-mysql/)