엄범

jdbcType : nullable column에 null이 들어갈 때?

  • MyBatis는 nullable 컬럼의 parameter로 null이 넘어왔을 때, jdbcType이 명시되어 있지 않으면 TypeException을 던진다.
    • setNull로 해당 타입에 맞는 null값(VARCHAR인 경우 "")을 넣어줘야 하는데, 뭘 넣어줄지 모르니까 예외가 발생하는 것
    • The JDBC Type is required by JDBC for all nullable columns, if null is passed as a value. You can investigate this yourself by reading the JavaDocs for the PreparedStatement.setNull() method.
      [www.mybatis.org/mybatis-3/sqlmap-xml.html]  
  • 반대로 null이 아닌 값이 넘어왔을 때는 해당 자바 타입의 setNonNullParameter를 호출하는데 여기서 parameter의 타입 -> DB 타입으로 변환해주어야 한다. 적절하게 변환하지 않으면 "부적합한 열 유형" 예외가 발생함.
  • 모든 nullable column에 jdbcType을 명시해주기 귀찮다면, 다음 방법으로 처리할 수 있음.

 

```java

sqlSessionFactory!!.configuration.jdbcTypeForNull = JdbcType.NULL

// xml base 설정도 가능

```

 

CustomTypeHandler 정의

```java

/* myBatis config typeHandler 로 등록되어 Boolean 을 Y, N, null 로 변환해준다. */

@MappedTypes(Boolean.class)

@MappedJdbcTypes(JdbcType.CHAR)

public class BaseBooleanTypeHandler extends BaseTypeHandler<Boolean> {

    ...

 

#{param, jdcbType=VARCHAR}

```

  • https://mybatis.org/mybatis-3/ko/configuration.html#typeHandlers  
  • 애너테이션 사용 시 주의. 꼼꼼하게 읽어보고 사용해야 함.
  • 마이바티스는 DB에서 꺼내온 데이터를 어떤 타입핸들러에서 처리할지를, [javaType, jdbcType] 두 개를 이용해 결정한다.
    • jdcbType을 쿼리에 명시하지 않은 경우, ``java javaType=[TheJavaType], jdbcType=null``조합을 사용한다.
  • 즉 기본으로 제공되는 AClassTypeHandler<A>와 내가 직접 정의한 AClassTypeHandler<A>가 모두 존재한다면, 후자를 사용하기 위해서는
    • @MappedJdbcTypes(TheJdbcType)을 사용하고, 쿼리 파라미터에 해당 jdbcType을 명시하거나,
      • ``java javaType=[TheJavaType], jdbcType=[TheJdbcType]`` 에 대해서 내 핸들러를 탄다.
    • 쿼리 파라미터에 아예 typeHandler를 적어서 내 핸들러를 타도록 명시하거나,
      • => 이 경우 typeAliases로 Handler가 등록 되어 있어야 prefix없이 쓴다.
    • @MappedJdbcTypes 애너테이션을 아예 사용하지 않고 @MappedTypes만 쓰거나,
      • 예를 들면, 모든 LocalDateTime 타입은 이 TypeHandler에서만 처리할거라면. 많이 쓰는 방법.
      • => 애너테이션을 안쓰면 AllJdbcType에 대한 핸들러가 정의된다.
      • 따라서 ``java javaType=[TheJavaType], jdbcType=[null&AllType]`` 이 내 핸들러를 탄다
    • @MappedJdcbTypes(value=TheJdcbType, includeNullJdbcType = true) 로 적어주어야 한다.
      • ``java javaType=[TheJavaType], jdbcType=[null&TheJdbcType]`` 에 대해서 내 핸들러를 탄다
      • 이건 조금 애매한 방법일 것 같은게... 애초에 핸들러를 2개 이상 유지해야 한다는 것은, jdbcType에 따라 다른 핸들러를 태워서 처리를 다르게 할 필요가 있기 때문이다.
      • db 상에 VARCHAR인 것도 jdbcType 명시 안하면 null로 넘어올거고, DATE인 것도 jdbcType 명시 안하면 null로 넘어오게 될텐데, 이러면 두 jdbcType 모두 includeNullJdbcType = true인 핸들러를 타게 된다.
      • 각기 다른 핸들러를 타기 위해서는 둘 중 하나의 db타입 파라미터에는 jdbcType을 꼭 명시해줘야 되는데... 그럴 바에야 두 타입 모두 jdbcType을 명시해주는게 더 나으니까.

 

CustomTypeHandler for Enum

`` E[] enumConstants``를 유지할지,

`` Map<String, E> enumConstantDirectory``를 유지할지는 상황에 따라서. Enum은 둘 다 가지고 있긴 함.

 

방법 1

 

```java

@Getter

@RequiredArgsConstructor

public enum ExampleEnum implements EnumUsingDbCode {
  EX1,
  EX2
  ;

  @Override
  public String getDbCode() {
    return this.name().toLowerCase();
  }}

```

```java

/**

얘를 ExampleEnum 안에 안두고 따로 둔 것은

1. setTypeHandlersPackage 관리 때문에 특정 패키지에 몰아두는게 나을 수 있음.

2. ExampleEnum은 POJO로 두는게 깔끔함.

*/

@MappedTypes(ExampleEnum.class)
public class ExampleEnumTypeHandler extends EnumUsingDbCodeTypeHandler<ExampleEnum> {
  public ExampleEnumTypeHandler() {
    super(ExampleEnum.class);
  }
}

```

```java

public abstract class EnumUsingDbCodeTypeHandler<E extends Enum<E>> extends BaseTypeHandler<EnumUsingDbCode> {

  private final Class<E> type;

  private final EnumUsingDbCode[] enums;

  protected EnumUsingDbCodeTypeHandler(Class<E> type) {
    this.type = type;

    this.enums = (EnumUsingDbCode[]) type.getEnumConstants();
  }

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, EnumUsingDbCode enumUsingDbCode, JdbcType jdbcType) throws SQLException {
    ps.setString(i, enumUsingDbCode.getDbCode());
  }

  @Override
  public EnumUsingDbCode getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String dbCode = rs.getString(columnName);
    return getEnumDbCode(dbCode);
  }

  @Override
  public EnumUsingDbCode getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String dbCode = rs.getString(columnIndex);
    return getEnumDbCode(dbCode);
  }

  @Override
  public EnumUsingDbCode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String dbCode = cs.getString(columnIndex);
    return getEnumDbCode(dbCode);
  }

 

  private EnumUsingDbCode getEnumDbCode(String dbCode) throws SQLException {
    return Stream.of(this.enums)
      .filter(enumUsingDbCode -> enumUsingDbCode.getDbCode().equals(dbCode))
      .findFirst()
      .orElseThrow(SQLException::new);
  }
}

```

 

방법 2

github.com/mybatis/mybatis-3/issues/42  

 

```java

@MappedType(...)

public class EnumUsingDbCodeTypeHandler<E extends Enum<E> & EnumUsingDbCode> extends BaseTypeHandler<E> {

```

  • 이렇게 하면, 타입 핸들러는 매번 정의 안해줘도 되지만 MappedType에 이 핸들러를 탈 대상 Enum을 추가해주어야 한다는 것은 똑같다.
  • 자동으로 해당 타입과 하위타입 클래스들을 스캔해서 TypeHandler를 생성해주는게 아니기 때문에...

 

방법 3 : 추천

  • 근데 방법1,2 가 별로 마음에 들지 않는다. 이 방법 말고 다른 방법은 없나?
  • " Mybatis는 ~mapper.xml에 등장하는 모든 Enum을 스캔해서 각각을 파라미터로 기본으로 제공되는 EnumTypeHandler<E>를 생성해준다. " 는 점을 이용하면 별도로 TypeHandler를 정의 할 필요가 없을 것 같은데...
  • github.com/mybatis/mybatis-3/issues/970  
  • mybatis.org/mybatis-3/ko/configuration.html

아래 설정을 이용해서 기본으로 제공되는 EnumTypeHandler 대신, Custom 타입핸들러를 기본 EnumTypeHandler로 만들 수 있음!

```java

// MyBatis 코드에 다음과 같은 부분이 있음. (>= 3.4.5)

Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));

configuration.setDefaultEnumTypeHandler(typeHandler);

 

// in config.xml

<setting name="defaultEnumTypeHandler" value="~~~.~~~.DefaultEnumTypeHandler"/>

```

 

코드가 길어서 gist로 대체