Hibernate 6. Broken custom types

Hi.

I’ve updated to Hibernate 6.0.3.Final from 5.6.3.Final and got error when i tried to execute query with boolean field, that defined by Custom User Type. Hibernate query builder did not consider, that field described by Custom type and processed it just like Boolean

Example.
I have entity with field, that processed different string values to “true” or “false”. In real life i’ve used Oracle and Y/N, but H2Database successfully processed this values, so i choosed ‘#’ and ‘&’ as symbols for “true” and “false”

@Entity
@Table(name = "CS_FOO")
public class Foo {

	private Long id;
	private boolean flag;

	@Id
	@SequenceGenerator(name = "CS_SEQ", sequenceName = "CS_SEQ")
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CS_SEQ")
	@Column(name = "ID")
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@Column(name = "flag", updatable = false)
	@Type(
			value = StringBooleanType.class,
			parameters = {
					@org.hibernate.annotations.Parameter(name = "true", value = "#"),
					@org.hibernate.annotations.Parameter(name = "false", value = "&"),
			}
	)
	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}

So there is implementation of StringBooleanType

public class StringBooleanType implements EnhancedUserType<Boolean>, ParameterizedType {

	private static final String T_VALUES = "true";
	private static final String F_VALUES = "false";
	private static final String NULLABLE = "nullable";
	private static final String CASE_SENSITIVE = "caseSensitive";

	private static final String[] DEFAULT_T_VALUES = {
			"TRUE",
			"T",
			"YES",
			"Y",
			"ON",
			"1"
	};

	private static final String[] DEFAULT_F_VALUES = {
			"FALSE",
			"F",
			"NO",
			"N",
			"OFF",
			"0"
	};
	public static final String NAME = "StringBoolean";

	private String[] tValues = DEFAULT_T_VALUES;
	private String[] fValues = DEFAULT_F_VALUES;
	private boolean caseSensitive;
	private boolean nullable;

	// ParameterizedType

	@Override
	public void setParameterValues(Properties parameters) {
		String tsProperty = parameters.getProperty(T_VALUES);
		if (tsProperty != null) {
			tValues = split(tsProperty);
			if (tValues.length == 0)
				tValues = DEFAULT_T_VALUES;
		}

		String fsProperty = parameters.getProperty(F_VALUES);
		if (fsProperty != null) {
			fValues = split(fsProperty);
		}

		caseSensitive = "true".equalsIgnoreCase(parameters.getProperty(CASE_SENSITIVE));
		nullable = "true".equalsIgnoreCase(parameters.getProperty(NULLABLE));
	}

	// UserType


	@Override
	public int getSqlType() {
		return Types.VARCHAR;
	}

	@Override
	public Class<Boolean> returnedClass() {
		return Boolean.class;
	}

	@Override
	public boolean equals(Boolean obj1, Boolean obj2) {
		if (obj1 == obj2)
			return true;
		if (nullable) {
			if (obj1 == null || obj2 == null)
				return false;
		} else {
			if (obj1 == null)
				return obj2.equals(Boolean.FALSE);
			if (obj2 == null)
				return obj1.equals(Boolean.FALSE);
		}
		return obj1.equals(obj2);
	}

	@Override
	public int hashCode(Boolean obj) throws HibernateException {
		return obj == null ? 0 : obj.hashCode();
	}

	@Override
	public Boolean nullSafeGet(ResultSet result, int pos, SharedSessionContractImplementor sessionImplementor, Object o) throws HibernateException, SQLException {
		String value = result.getString(pos);
		return valueOf(result.wasNull() ? null : value);
	}

	@Override
	public void nullSafeSet(PreparedStatement stmt, Boolean value, int index, SharedSessionContractImplementor sessionImplementor) throws HibernateException, SQLException {
		if (value == null) {
			stmt.setNull(index, Types.VARCHAR);
		} else {
			stmt.setString(index, toString(value));
		}
	}

	@Override
	public Boolean deepCopy(Boolean value) {
		return value;
	}

	@Override
	public boolean isMutable() {
		return false;
	}

	@Override
	public Serializable disassemble(Boolean value) {
		return value;
	}

	@Override
	public Boolean assemble(Serializable cached, Object owner) {
		return (Boolean) cached;
	}

	// EnhancedUserType
	@Override
	public String toSqlLiteral(Boolean value) {
		String text = toString(value);
		return text == null ? "NULL" : "'" + text + "'";
	}

	@Override
	public Boolean fromStringValue(CharSequence sequence) throws HibernateException {
		return valueOf(sequence);
	}

	// Privates

	private Boolean valueOf(CharSequence source) {
		if (source == null) {
			return nullable ? null : Boolean.FALSE;
		}
		String text = (String) source;

		if (caseSensitive) {
			for (String t : tValues) {
				if (t.equals(text)) {
					return Boolean.TRUE;
				}
			}

			if (nullable) {
				for (String f : fValues) {
					if (f.equals(text)) {
						return Boolean.FALSE;
					}
				}

				return null;
			}
		} else {
			for (String t : tValues) {
				if (t.equalsIgnoreCase(text)) {
					return Boolean.TRUE;
				}
			}

			if (nullable) {
				for (String f : fValues) {
					if (f.equalsIgnoreCase(text)) {
						return Boolean.FALSE;
					}
				}

				return null;
			}
		}

		return Boolean.FALSE;
	}

	@Override
	public String toString(Boolean value) {
		if (value == null) {
			return nullable ? null : fValues[0];
		}
		return value ? tValues[0] : fValues[0];
	}

	public static String[] split(String text) {
		StringTokenizer tokenizer = new StringTokenizer(text, " \t,;|");
		List<String> list = new ArrayList<>();
		while (tokenizer.hasMoreTokens()) {
			list.add(tokenizer.nextToken());
		}
		return list.toArray(new String[0]);
	}

}

Now, i want to select items, that have different values of field flag:

query = s.createQuery("select f from Foo f where f.flag", Foo.class);
or
query = s.createQuery("select f from Foo f where not(f.flag)", Foo.class);

I expect, that query analyzer will handle field’s type and tried to convert it by provided methods (for 5.6.3 worked version was f.flag=1 or f.flag=0)

But currently it raised exception “Wrong number” for my production Oracle and Data conversion error converting “CHARACTER VARYING to BOOLEAN” for H2

Best regards. Alexey

Update to 6.2.1.Final please and report again if you still have an issue.

It reprodices for 6.2.1.Final

I’ve created issue in JIRA [HHH-16504] - Hibernate JIRA

1 Like

@bostandyksoft Why don’t you utilize org.hibernate.type.YesNoConverter?

@Convert(converter = YesNoConverter.class)
private boolean active;

Point is that we would have our custom type, that can use different values for Yes/No values.

Implementation of YesNoConverter is quite simple:

public class YesNoConverter implements 
    AttributeConverter<Boolean, Character>,
    BasicValueConverter<Boolean, Character> {

It could be used as an example, but I don’t understand why you went with EnhancedUserType instead…

I.e. for Hibernate 6 should be used converters rather than custom types?

UserType is considered legacy at this point. You should rather use AttributeConverter if possible. If you need to change how JDBC binding/extracting should work, consider using a custom JdbcType. If you need to change the mutability aspects of the Java type, us a custom JavaType. You can configure these types via @JavaType(MyJavaType.class) and @JdbcType(MyJdbcType.class).

1 Like