[230718] mysql datetime timestamp type mapping (jdbc)

Table of Contents

1 동기

어떻게 mysql의 timestamp와 datetime은 모두 하나의 타입으로 변환이 될 수 있을까? 이것을 이해하려면 우리는 JDBC의 타입 매핑을 알아야 한다.

2 JDBC 타입 매핑

우리는 aws-mysql-jdbc 를 사용하고 있다. https://github.com/awslabs/aws-mysql-jdbc

실제로 data class에 매핑을 하기 전에 JDBC이기 때문에 ResultSet을 통해서 데이터를 가져온다. 그리고 인스턴스를 만들기 위한 인자를 읽을 때마다 그에 맞는 타입으로 변환할 것이다.

2.1 DefaultResultSetHandler.createUsingConstructor

private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
  boolean foundValues = false;
  for (int i = 0; i < constructor.getParameterTypes().length; i++) {
    Class<?> parameterType = constructor.getParameterTypes()[i];
    String columnName = rsw.getColumnNames().get(i);
    TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
    Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
    constructorArgTypes.add(parameterType);
    constructorArgs.add(value);
    foundValues = value != null || foundValues;
  }
  return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}

이 코드가 바로 그것을 하는 일이다. 잘보면 그때그때 constructor.getParameterTypes()[i] 로 파라미터의 타입을 가져온다. 이후 TypeHandler 를 가져온다.

위 메소드에서 가장 중요한 곳은 typeHandler.getResult 이다. 이것이 바로 타입을 변환하는 곳이다.

data class의 타입이 Timestamp인 경우 BaseTypeHandler.getResult(ResultSet, rs, String columnName) 의 구현체는 SqlTimestampHandler.getNullableResult 여기서 rs는 HikariProxyResultSet , columnName은 실제 컬럼명이다.

2.2 HikariProxyResultSet

HikariProxyResultSet.getTimesamp(String var1) 의 구현체를 보자.

public final class HikariProxyResultSet extends ProxyResultSet implements Wrapper, AutoCloseable, ResultSet {
  // ...
    public Timestamp getTimestamp(String var1) throws SQLException {
	try {
	    return super.delegate.getTimestamp(var1);
	} catch (SQLException var3) {
	    throw this.checkException(var3);
	}
    }
}

HikariProxyResultSet의 super는 누구일까? 디버그결과 ResultSetImpl 임이 밝혀졌다. 그럼 ResultSetImpl.getTimestamp 를 보자.

public Timestamp getTimestamp(String columnName) throws SQLException {
    try {
	return this.getTimestamp(this.findColumn(columnName));
    } catch (CJException var3) {
	throw SQLExceptionsMapping.translateException(var3, this.getExceptionInterceptor());
    }
}


public Timestamp getTimestamp(int columnIndex) throws SQLException {
    try {
	this.checkRowPos();
	this.checkColumnBounds(columnIndex);
	return (Timestamp)this.thisRow.getValue(columnIndex - 1, this.defaultTimestampValueFactory);
    } catch (CJException var3) {
	throw SQLExceptionsMapping.translateException(var3, this.getExceptionInterceptor());
    }
}

columnNameindex 를 찾아서 가져온다. this.thisRow.getValue(columnIndex - 1, this.defaultTimestampValueFactory) 중요하다. this.thisRow 는 무엇일까? Row 라는 인터페이스의 구현체이다. 디버그결과 ByteArrayRow 구현체다.

2.3 ByteArrayRow

public class ByteArrayRow extends AbstractResultsetRow {
    public <T> T getValue(int columnIndex, ValueFactory<T> vf) {
	byte[] columnData = this.internalRowData[columnIndex];
	int length = columnData == null ? 0 : columnData.length;
	return this.getValueFromBytes(columnIndex, columnData, 0, length, vf);
    }

    protected <T> T getValueFromBytes(int columnIndex, byte[] bytes, int offset, int length, ValueFactory<T> vf) {
	if (this.getNull(columnIndex)) {
	    return vf.createFromNull();
	} else {
	    T retVal = this.decodeAndCreateReturnValue(columnIndex, bytes, offset, length, vf);
	    this.wasNull = retVal == null;
	    return retVal;
	}
    }
}

this.decodeAndCreateReturnValue(columnIndex, bytes, offset, length, vf) 가 마지막일 것이다. 이곳에서 실제로 데이터를 가져와서 Timestamp로 리턴한다.

2.4 AbstractResultSetRow

이 코드는 AbstractResultSetRow.class 에 있다.

private <T> T decodeAndCreateReturnValue(int columnIndex, byte[] bytes, int offset, int length, ValueFactory<T> vf) {
    Field f = this.metadata.getFields()[columnIndex];
    switch (f.getMysqlTypeId()) {
    case 0: // ...
    case 7:
	return this.valueDecoder.decodeTimestamp(bytes, offset, length, f.getDecimals(), vf);
    // ...
    case 12:
	return this.valueDecoder.decodeDatetime(bytes, offset, length, f.getDecimals(), vf);
    }

이 코드가 핵심이다. 안으로 들어가자.

this.valueDecoder 의 구현체는 MysqlBinaryValueDecoder

2.5 MysqlBinaryValueDecoder

아래는 timestamp 컬럼타입과 datetime 컬럼타입을 decode하는 방식이다. 결론적으로 둘다 year, month, day hours, minutes, seconds, nanos, scale 을 가져와서 InternalTimestamp를 생성한다.

public <T> T decodeTimestamp(byte[] bytes, int offset, int length, int scale, ValueFactory<T> vf) {
	if (length == 0) {
	    return vf.createFromTimestamp(new InternalTimestamp());
	} else if (length != 4 && length != 11 && length != 7) {
	    throw new DataReadException(Messages.getString("ResultSet.InvalidLengthForType", new Object[]{length, "TIMESTAMP"}));
	} else {
	    int year = false;
	    int month = false;
	    int day = false;
	    int hours = 0;
	    int minutes = 0;
	    int seconds = 0;
	    int nanos = 0;
	    int year = bytes[offset + 0] & 255 | (bytes[offset + 1] & 255) << 8;
	    int month = bytes[offset + 2];
	    int day = bytes[offset + 3];
	    if (length > 4) {
		hours = bytes[offset + 4];
		minutes = bytes[offset + 5];
		seconds = bytes[offset + 6];
	    }

	    if (length > 7) {
		nanos = 1000 * (bytes[offset + 7] & 255 | (bytes[offset + 8] & 255) << 8 | (bytes[offset + 9] & 255) << 16 | (bytes[offset + 10] & 255) << 24);
	    }

	    return vf.createFromTimestamp(new InternalTimestamp(year, month, day, hours, minutes, seconds, nanos, scale));
	}
    }

    public <T> T decodeDatetime(byte[] bytes, int offset, int length, int scale, ValueFactory<T> vf) {
	if (length == 0) {
	    return vf.createFromTimestamp(new InternalTimestamp());
	} else if (length != 4 && length != 11 && length != 7) {
	    throw new DataReadException(Messages.getString("ResultSet.InvalidLengthForType", new Object[]{length, "TIMESTAMP"}));
	} else {
	    int year = false;
	    int month = false;
	    int day = false;
	    int hours = 0;
	    int minutes = 0;
	    int seconds = 0;
	    int nanos = 0;
	    int year = bytes[offset + 0] & 255 | (bytes[offset + 1] & 255) << 8;
	    int month = bytes[offset + 2];
	    int day = bytes[offset + 3];
	    if (length > 4) {
		hours = bytes[offset + 4];
		minutes = bytes[offset + 5];
		seconds = bytes[offset + 6];
	    }

	    if (length > 7) {
		nanos = 1000 * (bytes[offset + 7] & 255 | (bytes[offset + 8] & 255) << 8 | (bytes[offset + 9] & 255) << 16 | (bytes[offset + 10] & 255) << 24);
	    }

	    return vf.createFromDatetime(new InternalTimestamp(year, month, day, hours, minutes, seconds, nanos, scale));
	}
    }

근데 나의 entity나 data class에는 어떻게 변환되는 것일까? 그것은 제네릭 T의 역할이다. T로 변환해줄 수 있는 녀석은 vf 이다. 만약에 나의 data class가 LocalDateTime이라면 vfLocalDateTimeValueFactory 이다. Timestamp였다면 vfSqlTimestampValueFactory 이다.

3 즉, 정리하면

  • mysql의 timestamp 컬럼타입(type id = 7)은 MysqlBinaryValueDecoder 를 통해 InternalTimestamp 로 변환된다.
  • mysql의 datetime 컬럼타입(type id = 12)은 MysqlBinaryValueDecoder 를 통해 InternalTimestamp 로 변환된다.
  • data class의 필드 타입이 LocalDateTime이라면 LocalDateTimeValueFactory 를 통해 LocalDateTime 으로 변환된다.
  • data class의 필드 타입이 Timestamp라면 SqlTimestampValueFactory 를 통해 Timestamp 로 변환된다.

4 참고문헌

Date: 2023-07-18 Tue 00:00

Author: 남영환

Created: 2024-01-04 Thu 09:13

Emacs 27.2 (Org mode 9.4.4)

Validate