HHH-9955 - EnumType mapped via hbm.xml not always resolved properly as far as named/ordinal

This commit is contained in:
Steve Ebersole 2015-07-22 11:49:47 -05:00
parent 6bb8d03595
commit e678a5b8cf
1 changed files with 149 additions and 191 deletions

View File

@ -12,6 +12,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import javax.persistence.Enumerated; import javax.persistence.Enumerated;
import javax.persistence.MapKeyEnumerated; import javax.persistence.MapKeyEnumerated;
@ -61,138 +62,17 @@ public class EnumType implements EnhancedUserType, DynamicParameterizedType,Logg
private Class<? extends Enum> enumClass; private Class<? extends Enum> enumClass;
private EnumValueMapper enumValueMapper; private EnumValueMapper enumValueMapper;
private int sqlType = Types.INTEGER; // before any guessing private int sqlType;
@Override
public int[] sqlTypes() {
return new int[] { sqlType };
}
@Override
public Class<? extends Enum> returnedClass() {
return enumClass;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x == y;
}
@Override
public int hashCode(Object x) throws HibernateException {
return x == null ? 0 : x.hashCode();
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException {
if ( enumValueMapper == null ) {
resolveEnumValueMapper( rs, names[0] );
}
return enumValueMapper.getValue( rs, names );
}
private void resolveEnumValueMapper(ResultSet rs, String name) {
if ( enumValueMapper == null ) {
try {
resolveEnumValueMapper( rs.getMetaData().getColumnType( rs.findColumn( name ) ) );
}
catch (Exception e) {
// because some drivers do not implement this
LOG.debugf(
"JDBC driver threw exception calling java.sql.ResultSetMetaData.getColumnType; " +
"using fallback determination [%s] : %s",
enumClass.getName(),
e.getMessage()
);
// peek at the result value to guess type (this is legacy behavior)
try {
Object value = rs.getObject( name );
if ( Number.class.isInstance( value ) ) {
treatAsOrdinal();
}
else {
treatAsNamed();
}
}
catch (SQLException ignore) {
treatAsOrdinal();
}
}
}
}
private void resolveEnumValueMapper(int columnType) {
// fallback for cases where not enough parameter/parameterization information was passed in
if ( isOrdinal( columnType ) ) {
treatAsOrdinal();
}
else {
treatAsNamed();
}
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
if ( enumValueMapper == null ) {
resolveEnumValueMapper( st, index );
}
enumValueMapper.setValue( st, (Enum) value, index );
}
private void resolveEnumValueMapper(PreparedStatement st, int index) {
if ( enumValueMapper == null ) {
try {
resolveEnumValueMapper( st.getParameterMetaData().getParameterType( index ) );
}
catch (Exception e) {
// because some drivers do not implement this
LOG.debugf(
"JDBC driver threw exception calling java.sql.ParameterMetaData#getParameterType; " +
"falling back to ordinal-based enum mapping [%s] : %s",
enumClass.getName(),
e.getMessage()
);
// Originally, this was simply treatAsOrdinal(). But, for DBs that do not implement the above, enums
// were treated as ordinal even when the *.hbm.xml explicitly define the type sqlCode. By default,
// this is essentially the same anyway, since sqlType is defaulted to Integer.
resolveEnumValueMapper( sqlType );
}
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return ( Serializable ) value;
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
@Override @Override
public void setParameterValues(Properties parameters) { public void setParameterValues(Properties parameters) {
// IMPL NOTE: we handle 2 distinct cases here:
// 1) we are passed a ParameterType instance in the incoming Properties - generally
// speaking this indicates the annotation-binding case, and the passed ParameterType
// represents information about the attribute and annotation
// 2) we are not passed a ParameterType - generally this indicates a hbm.xml binding case.
final ParameterType reader = (ParameterType) parameters.get( PARAMETER_TYPE ); final ParameterType reader = (ParameterType) parameters.get( PARAMETER_TYPE );
// IMPL NOTE : be protective about not setting enumValueMapper (i.e. calling treatAsNamed/treatAsOrdinal)
// in cases where we do not have enough information. In such cases we do additional checks
// as part of nullSafeGet/nullSafeSet to query against the JDBC metadata to make the determination.
if ( reader != null ) { if ( reader != null ) {
enumClass = reader.getReturnedClass().asSubclass( Enum.class ); enumClass = reader.getReturnedClass().asSubclass( Enum.class );
@ -212,52 +92,24 @@ public class EnumType implements EnhancedUserType, DynamicParameterizedType,Logg
} }
if ( isOrdinal ) { if ( isOrdinal ) {
treatAsOrdinal(); this.enumValueMapper = new OrdinalEnumValueMapper();
} }
else { else {
treatAsNamed(); this.enumValueMapper = new NamedEnumValueMapper();
} }
sqlType = enumValueMapper.getSqlType(); sqlType = enumValueMapper.getSqlType();
} }
else { else {
String enumClassName = (String) parameters.get( ENUM ); final String enumClassName = (String) parameters.get( ENUM );
try { try {
enumClass = ReflectHelper.classForName( enumClassName, this.getClass() ).asSubclass( Enum.class ); enumClass = ReflectHelper.classForName( enumClassName, this.getClass() ).asSubclass( Enum.class );
} }
catch ( ClassNotFoundException exception ) { catch ( ClassNotFoundException exception ) {
throw new HibernateException( "Enum class not found", exception ); throw new HibernateException( "Enum class not found: " + enumClassName, exception );
} }
final Object useNamedSetting = parameters.get( NAMED ); this.enumValueMapper = interpretParameters( parameters );
if ( useNamedSetting != null ) { this.sqlType = enumValueMapper.getSqlType();
final boolean useNamed = ConfigurationHelper.getBoolean( NAMED, parameters );
if ( useNamed ) {
treatAsNamed();
}
else {
treatAsOrdinal();
}
sqlType = enumValueMapper.getSqlType();
}
}
final String type = (String) parameters.get( TYPE );
if ( type != null ) {
sqlType = Integer.decode( type );
}
}
private void treatAsOrdinal() {
if ( enumValueMapper == null || ! OrdinalEnumValueMapper.class.isInstance( enumValueMapper ) ) {
enumValueMapper = new OrdinalEnumValueMapper();
sqlType = enumValueMapper.getSqlType();
}
}
private void treatAsNamed() {
if ( enumValueMapper == null || ! NamedEnumValueMapper.class.isInstance( enumValueMapper ) ) {
enumValueMapper = new NamedEnumValueMapper();
sqlType = enumValueMapper.getSqlType();
} }
} }
@ -287,6 +139,131 @@ public class EnumType implements EnhancedUserType, DynamicParameterizedType,Logg
return null; return null;
} }
private EnumValueMapper interpretParameters(Properties parameters) {
if ( parameters.containsKey( NAMED ) ) {
final boolean useNamed = ConfigurationHelper.getBoolean( NAMED, parameters );
if ( useNamed ) {
return new NamedEnumValueMapper();
}
else {
return new OrdinalEnumValueMapper();
}
}
if ( parameters.containsKey( TYPE ) ) {
final int type = Integer.decode( (String) parameters.get( TYPE ) );
if ( isNumericType( type ) ) {
return new OrdinalEnumValueMapper();
}
else if ( isCharacterType( type ) ) {
return new OrdinalEnumValueMapper();
}
else {
throw new HibernateException(
String.format(
Locale.ENGLISH,
"Passed JDBC type code [%s] not recognized as numeric nor character",
type
)
);
}
}
// the fallback
return new OrdinalEnumValueMapper();
}
private boolean isCharacterType(int jdbcTypeCode) {
switch ( jdbcTypeCode ) {
case Types.CHAR:
case Types.LONGVARCHAR:
case Types.VARCHAR: {
return true;
}
default: {
return false;
}
}
}
private boolean isNumericType(int jdbcTypeCode) {
switch ( jdbcTypeCode ) {
case Types.INTEGER:
case Types.NUMERIC:
case Types.SMALLINT:
case Types.TINYINT:
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT: {
return true;
}
default:
return false;
}
}
@Override
public int[] sqlTypes() {
return new int[] { sqlType };
}
@Override
public Class<? extends Enum> returnedClass() {
return enumClass;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x == y;
}
@Override
public int hashCode(Object x) throws HibernateException {
return x == null ? 0 : x.hashCode();
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws SQLException {
if ( enumValueMapper == null ) {
throw new AssertionFailure( "EnumType (" + enumClass.getName() + ") not properly, fully configured" );
}
return enumValueMapper.getValue( rs, names );
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
if ( enumValueMapper == null ) {
throw new AssertionFailure( "EnumType (" + enumClass.getName() + ") not properly, fully configured" );
}
enumValueMapper.setValue( st, (Enum) value, index );
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return ( Serializable ) value;
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
@Override @Override
public String objectToSQLString(Object value) { public String objectToSQLString(Object value) {
return enumValueMapper.objectToSQLString( (Enum) value ); return enumValueMapper.objectToSQLString( (Enum) value );
@ -310,14 +287,18 @@ public class EnumType implements EnhancedUserType, DynamicParameterizedType,Logg
return value.toString(); return value.toString();
} }
private static interface EnumValueMapper extends Serializable { public boolean isOrdinal() {
public int getSqlType(); return enumValueMapper instanceof OrdinalEnumValueMapper;
public Enum getValue(ResultSet rs, String[] names) throws SQLException; }
public void setValue(PreparedStatement st, Enum value, int index) throws SQLException;
public String objectToSQLString(Enum value); private interface EnumValueMapper extends Serializable {
public String toXMLString(Enum value); int getSqlType();
public Enum fromXMLString(String xml); Enum getValue(ResultSet rs, String[] names) throws SQLException;
void setValue(PreparedStatement st, Enum value, int index) throws SQLException;
String objectToSQLString(Enum value);
String toXMLString(Enum value);
Enum fromXMLString(String xml);
} }
public abstract class EnumValueMapperSupport implements EnumValueMapper { public abstract class EnumValueMapperSupport implements EnumValueMapper {
@ -479,27 +460,4 @@ public class EnumType implements EnhancedUserType, DynamicParameterizedType,Logg
} }
} }
public boolean isOrdinal() {
return isOrdinal( sqlType );
}
private boolean isOrdinal(int paramType) {
switch ( paramType ) {
case Types.INTEGER:
case Types.NUMERIC:
case Types.SMALLINT:
case Types.TINYINT:
case Types.BIGINT:
case Types.DECIMAL: //for Oracle Driver
case Types.DOUBLE: //for Oracle Driver
case Types.FLOAT: //for Oracle Driver
return true;
case Types.CHAR:
case Types.LONGVARCHAR:
case Types.VARCHAR:
return false;
default:
throw new HibernateException( "Unable to persist an Enum in a column of SQL Type: " + paramType );
}
}
} }