====== StringBoolean Hibernate UserType ======
boolean을 DB상에서는 문자열로 매핑하는 [[java:hibernate:usertype|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은 사용할 일을 만들지 말자.**
또한, [[java:jpa:converter|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 로 지정하여 해당 값을 리턴한다.