boolean을 DB상에서는 문자열로 매핑하는 Hibernate User Type. Hibernate는 기본적으로 “yes_no”와 “true_false”라는 문자열 Boolean 매핑이 이미 존재한다. 하지만 이들는 “Y/N”, “T/F” 이외의 알 수 없는 값이 들어왔을 때 오류를 발생시킨다. 특히 Empty 문자열에 대해서도 null이 아닌 에러로 처리한다.
데이터가 망가진 상태의 Legacy DB와 매핑할 필요가 있을 때는(근본적으로 데이터를 고치는게 맞지만) 문제 가능성이 있어서 따로 만든 UserType. 이 UserType은 legacy 매핑시에만 사용하고 신규 DB를 구축할 때는 절대로 저런일이 발생할 수 없도록 BOOLEAN NOT NULL 컬럼으로 만드는 것이 애초에 좋다.
한 마디로… 이런 UserType은 사용할 일을 만들지 말자.
또한, JPA Converter를 사용하게 하는게 나을 듯 하다.
public class StringBooleanUserType implements UserType, ParameterizedType { public static final String PARAM_TRUE_VALUE = "trueValue"; public static final String DEFAULT_TRUE_VALUE = "Y"; public static final String PARAM_FALSE_VALUE = "falseValue"; public static final String DEFAULT_FALSE_VALUE = "N"; /** * DB상의 데이터가 알 수 없는 값일때(빈 문자열 포함) unknownResult는 * "true", "false", "null"을 문자열로 기입한다. * "null"이 기본값이며 이때는 알 수 없는 값이 들어오면 null을 리턴하고, * "true", "false"로 할 경우 해당 Boolean으로 변환한다. */ public static final String PARAM_UNKNOWN_RESULT = "unknownResult"; public static final String DEFAULT_UNKNOWN_RESULT = "null"; /** * 대소문자 무시할 지 여부를 true/false로 지정한다. * 기본값은 true로 대소문자를 무시하고 비교한다. */ public static final String PARAM_IGNORE_CASE = "ignoreCase"; public static final String DEFAULT_IGNORE_CASE = "true"; private AbstractSingleColumnStandardBasicType type; private int[] sqlTypes = null; private String trueValue = null; private String falseValue = null; private Boolean unknownResult = null; private boolean ignoreCase = true; @Override public void setParameterValues(Properties parameters) { if (parameters == null) { parameters = new Properties(); } trueValue = parameters.getProperty(PARAM_TRUE_VALUE, DEFAULT_TRUE_VALUE); falseValue = parameters.getProperty(PARAM_FALSE_VALUE, DEFAULT_FALSE_VALUE); unknownResult = populateUnknownResult(parameters.getProperty(PARAM_UNKNOWN_RESULT, DEFAULT_UNKNOWN_RESULT)); ignoreCase = Boolean.valueOf(parameters.getProperty(PARAM_IGNORE_CASE, DEFAULT_IGNORE_CASE)); populateSqlTypes(); } Boolean populateUnknownResult(String unknownResultString) { if ("true".equalsIgnoreCase(unknownResultString)) { return Boolean.TRUE; } else if ("false".equalsIgnoreCase(unknownResultString)) { return Boolean.FALSE; } else if ("null".equalsIgnoreCase(unknownResultString)) { return null; } throw new IllegalArgumentException( format("[%s] is illegal unknownResult value. Only 'true', 'false', 'null' are allowed.", unknownResultString)); } private void populateSqlTypes() { TypeResolver tr = new TypeResolver(); String stringClassName = String.class.getName(); // type은 org.hibernate.type.StringType 으로 사실상 고정값임. type = (AbstractSingleColumnStandardBasicType) tr.basic(stringClassName); sqlTypes = new int[] { type.sqlType() }; } @Override public int[] sqlTypes() { return Arrays.copyOf(sqlTypes, sqlTypes.length); } @Override public Class returnedClass() { return Boolean.class; } @Override public boolean equals(Object x, Object y) { return Objects.equals(x, y); } @Override public int hashCode(Object x) { return Objects.hashCode(x); } @Override public Object deepCopy(Object value) { return value; } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object value) { return (Serializable) value; } @Override public Object assemble(Serializable cached, Object owner) { return cached; } @Override public Object replace(Object original, Object target, Object owner) { return original; } @Override public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException { String stringBooleanValue = (String) type.get(rs, names[0], session); if (stringBooleanValue == null) { log.trace("Found [{}] as column [{}] original value [{}]", null, names[0], stringBooleanValue); return null; } if (stringEqualsWithCaseCheck(trueValue, stringBooleanValue)) { log.trace("Found [{}] as column [{}] original value [{}]", true, names[0], stringBooleanValue); return true; } if (stringEqualsWithCaseCheck(falseValue, stringBooleanValue)) { log.trace("Found [{}] as column [{}] original value [{}]", false, names[0], stringBooleanValue); return false; } log.trace("Found [{}] as column [{}] original value [{}]", unknownResult, names[0], stringBooleanValue); return unknownResult; } private boolean stringEqualsWithCaseCheck(String value1, String value2) { if (ignoreCase) { return StringUtils.equalsIgnoreCase(value1, value2); } return StringUtils.equals(value1, value2); } /** * 데이터베이스로 값을 저장하기 위해 Boolean을 문자열로 변환. */ @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws SQLException { String columnValue = null; if (Boolean.TRUE.equals(value)) { columnValue = trueValue; } else if (Boolean.FALSE.equals(value)) { columnValue = falseValue; } log.trace("binding parameter [{}] as [{}] - [{}] original value [{}]", index, JdbcTypeNameMapper.getTypeName(sqlTypes[0]), columnValue, value); st.setObject(index, columnValue); } }
@TypeDefs({ @TypeDef(name = "loose_yes_no", typeClass = StringBooleanUserType.class, parameters = { @Parameter(name = StringBooleanUserType.PARAM_TRUE_VALUE, value = "Y"), @Parameter(name = StringBooleanUserType.PARAM_FALSE_VALUE, value = "N"), @Parameter(name = StringBooleanUserType.PARAM_IGNORE_CASE, value = "true"), @Parameter(name = StringBooleanUserType.PARAM_UNKNOWN_RESULT, value = "null") } ), @TypeDef(name = "primitive_loose_yes_no", typeClass = StringBooleanUserType.class, parameters = { @Parameter(name = StringBooleanUserType.PARAM_TRUE_VALUE, value = "true"), @Parameter(name = StringBooleanUserType.PARAM_FALSE_VALUE, value = "false"), @Parameter(name = StringBooleanUserType.PARAM_IGNORE_CASE, value = "false"), @Parameter(name = StringBooleanUserType.PARAM_UNKNOWN_RESULT, value = "false") } ) })
PARAM_TRUE_VALUE
: true
에 대응하는 문자열PARAM_FALSE_VALUE
: false
에 대응하는 문자열PARAM_IGNORE_CASE
: DB에서 값 읽을 때 대소문자 무시여부. 단, WHERE 조건으로 줄 때는 딱 지정된 문자열로 비교 조건을 날리므로 대소문자가 일치해야 한다.PARAM_UNKNOWN_RESULT
: 알 수 없는 값(빈 문자열 등)에 대해서 true, false, null 로 지정하여 해당 값을 리턴한다.