이번 글에서는 DateTime 데이터를
Database에 저장하는 과정에 대해 다뤄보겠습니다.
예제 코드는 다음 환경에서 실행하였습니다.
- spring boot starter data jpa:2.4.5
- MySQL 8.0.22
가장 먼저 DB 설정과 DB Schema를 살펴보겠습니다.
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/example?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=mysqlpassword
위의 설정 중 serverTimezone을 보시면 UTC로 설정되어 있어 있습니다.
CREATE TABLE `purchase` (
`id` decimal(19,2) NOT NULL,
`amount` int DEFAULT NULL,
`created_date_time` datetime(6) DEFAULT NULL,
`purchase_date_time` datetime(6) DEFAULT NULL,
`player_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK56apcx95fvl79c8240ew525fi` (`player_id`),
CONSTRAINT `FK56apcx95fvl79c8240ew525fi` FOREIGN KEY (`player_id`) REFERENCES `player` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
다음은 Schema를 살펴보겠습니다.
(참고로 해당 schema는 JPA를 통해 자동으로 생성한 것으로 이를 생성한 entity는 다음과 같습니다.)
public class Purchase {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private BigInteger id;
@ManyToOne
@JoinColumn(referencedColumnName = "id")
private Player player;
@Column
private int amount;
@Column
private OffsetDateTime purchaseDateTime;
@Column
@CreatedDate
private LocalDateTime createdDateTime;
}
OffsetDateTime과 LocalDateTime 모두 datetime type으로 생성한 것을 알 수 있습니다.
그럼 여기서 서로 다른 offset 값을 전달하면 이를 어떻게 저장할까요?
//...
OffsetDateTime kstDateTime = OffsetDateTime.parse("2021-06-06T22:00:00+09:00");
OffsetDateTime utcDateTime = OffsetDateTime.parse("2021-06-06T22:00:00+00:00");
Purchase kstPurchase = new Purchase(player.get(), purchase.getAmount(), kstDateTime);
Purchase utcPurchase = new Purchase(player.get(), purchase.getAmount(), utcDateTime);
Purchase kstResult = purchaseRepository.save(kstPurchase);
Purchase utcResult = purchaseRepository.save(utcPurchase);
//...
위 코드에서는 서로 다른 offset을 가지는 두 시간을
결제 시간(purchaseDateTime)으로 설정하여 database에 저장하였습니다.
결과는 다음과 같습니다.
mysql> select * from purchase;
+-----+--------+----------------------------+----------------------------+-----------+
| id | amount | created_date_time | purchase_date_time | player_id |
+-----+--------+----------------------------+----------------------------+-----------+
| 2 | 1000 | 2021-06-08 11:08:58.801927 | 2021-06-06 13:00:00.000000 | 1 |
| 3 | 1000 | 2021-06-08 11:08:58.827928 | 2021-06-06 22:00:00.000000 | 1 |
+-----+--------+----------------------------+----------------------------+-----------+
2 rows in set (0.00 sec)
2021-06-06T22:00:00+09:00을 UTC(offset +00:00) 시간인
2021-06-06T13:00:00 으로 치환하여 저장한 것을 알 수 있습니다.
위와 같이 database에 저장된 이유는 위에서 살펴본,
spring.datasource.url=jdbc:mysql://localhost:3306/example?serverTimezone=UTC&characterEncoding=UTF-8
serverTimezone을 UTC로 설정했기 때문입니다.
그렇다면 해당 내용 검증을 위해서
이번엔 serverTimezone을 Asia/Seoul로 설정하고 동일한 로직을 수행해 보겠습니다.
spring.datasource.url=jdbc:mysql://localhost:3306/jpa?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
+-----+--------+----------------------------+----------------------------+-----------+
| id | amount | created_date_time | purchase_date_time | player_id |
+-----+--------+----------------------------+----------------------------+-----------+
| 4 | 1000 | 2021-06-24 21:09:57.501449 | 2021-06-06 22:00:00.000000 | 1 |
| 5 | 1000 | 2021-06-24 21:09:57.593425 | 2021-06-07 07:00:00.000000 | 1 |
+-----+--------+----------------------------+----------------------------+-----------+
우리가 예상한대로 위의 예제와는 반대로 UTC 시간인 2021-06-06T22:00:00+00:00 을
KST(offset +09:00)인 2021-06-07T07:00:00 으로 변경한 것을 확인할 수 있습니다.
그럼 hibernate에서는 어떻게 datetime 값을 변환하고 있을까요?
코드를 살펴보겠습니다.
//AbstractEntityPersister::dehydrate
//...
for ( int i = 0; i < entityMetamodel.getPropertySpan(); i++ ) {
if ( includeProperty[i] && isPropertyOfTable( i, j ) && !lobProperties.contains( i ) ) {
getPropertyTypes()[i].nullSafeSet( ps, fields[i], index, includeColumns[i], session );
index += ArrayHelper.countTrue( includeColumns[i] );
}
}
//...
dehydrate 메소드는 우리가 전달해준 값을 이용해 database에 저장할 수 있는 쿼리를 완성합니다.
(왜 메소드 이름이 dehydrate인지는 모르겠네요. 혹시 아시는 분이 계시다면 댓글 부탁드립니다!)
위 코드의 세번째 줄을 보면, 먼저 각 property의 타입을 가져오는데,
해당 타입에는 SqlTypeDescriptor, JavaTypeDescriptor 정보가 포함되어 있습니다.
우리가 관심있게 보고있는 날짜 데이터, OffsetDateTimeType의 경우,
public OffsetDateTimeType() {
super( TimestampTypeDescriptor.INSTANCE, OffsetDateTimeJavaDescriptor.INSTANCE );
}
위와 같이,
- TimestampTypeDescriptor(SqlTypeDescriptor)
- OffsetDateTimeJavaDescriptor(JavaTypeDescriptor)
로 각각 정의 되어 있습니다.
이 SqlTypeDescriptor, JavaTypeDescriptor 하는 역할을 조금 더 자세히 살펴보면,
JavaTypeDescriptor 타입에 해당 하는 값을 변환해주고,
SqlTypeDescriptor는 위에서 변환된 값을 쿼리에 적용합니다.
//TimestampTypeDescriptor::getBinder::
return new BasicBinder<X>( javaTypeDescriptor, this ) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
final Timestamp timestamp = javaTypeDescriptor.unwrap( value, Timestamp.class, options );
if ( value instanceof Calendar ) {
st.setTimestamp( index, timestamp, (Calendar) value );
}
else if ( options.getJdbcTimeZone() != null ) {
st.setTimestamp( index, timestamp, Calendar.getInstance( options.getJdbcTimeZone() ) );
}
else {
st.setTimestamp( index, timestamp );
}
}
//...
그럼 여기까지 DateTime이 저장되는 과정에 대한 글이었습니다.
'전체보기 > Spring' 카테고리의 다른 글
상속과 @Data의 warning, 그리고 @EqualsAndHashCode (0) | 2022.09.04 |
---|---|
[JPA] JpaRepository vs CrudRepository(1) - Paging과 Sorting / QueryExampleExecutor (0) | 2022.01.01 |
[Spring Boot] DateTime 다뤄보기 (0) | 2021.06.02 |
[Spring Boot] Custom Constraint / Validation 파헤치기 (0) | 2020.08.26 |
[Spring Boot]HikariCP 모니터링 - InstanceAlreadyExistsException 해결 (0) | 2020.07.19 |