Our problem is that we can't get data (which includes empty strings with length 0) from a legacy database due to a StringIndexOutOfBoundsExceptiion
originating from Hibernate's CharacterTypeDescriptor
. We would like to change Hibernate's behavior to properly resolve empty strings.
Example data:
1, 'Berlin', 17277, '', 'aUser'
2, 'London', 17277, '', 'anotherUser'
We use hibernate with javax.persistence.Query
.
String sql = "SELECT * FROM table";
Query query = entityManager.createNativeQuery(sql);
List resultList = query.getResultList();
This leads to a StringIndexOutOfBoundsException
with its root being the following code from Hibernate:
if ( String.class.isInstance( value ) ) {
final String str = (String) value;
return Character.valueOf( str.charAt(0) ); // this fails, as there is no char at position 0
}
This was confirmed by a post on the hibernate forums.
We have no option of upgrading hibernate from this buggy version and look for a way to change Hibernate's mapping.
We can not use PreparedStatements or plain JDBC-Connections nor JPA-Entities.
Altering the legacy database is not possible either. The SQL statement works flawlessly using DBVisualizer.
Is there anway to change Hibernate's way of mapping strings?
The
CharacterType
presented in this answer is available via the hibernate-types project, so there is no need to write it yourself.
First, you need to define an ImmutableType
:
public abstract class ImmutableType<T> implements UserType {
private final Class<T> clazz;
protected ImmutableType(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public Object nullSafeGet(
ResultSet rs,
String[] names,
SharedSessionContractImplementor session,
Object owner)
throws SQLException {
return get(rs, names, session, owner);
}
@Override
public void nullSafeSet(
PreparedStatement st,
Object value,
int index,
SharedSessionContractImplementor session)
throws SQLException {
set(st, clazz.cast(value), index, session);
}
protected abstract T get(
ResultSet rs,
String[] names,
SharedSessionContractImplementor session,
Object owner) throws SQLException;
protected abstract void set(
PreparedStatement st,
T value,
int index,
SharedSessionContractImplementor session)
throws SQLException;
@Override
public Class<T> returnedClass() {
return clazz;
}
@Override
public boolean equals(Object x, Object y) {
return Objects.equals(x, y);
}
@Override
public int hashCode(Object x) {
return x.hashCode();
}
@Override
public Object deepCopy(Object value) {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object o) {
return (Serializable) o;
}
@Override
public Object assemble(
Serializable cached,
Object owner) {
return cached;
}
@Override
public Object replace(
Object o,
Object target,
Object owner) {
return o;
}
}
Now, we can move to defining the actual CharacterType
:
public class CharacterType
extends ImmutableType<Character> {
public CharacterType() {
super(Character.class);
}
@Override
public int[] sqlTypes() {
return new int[]{Types.CHAR};
}
@Override
public Character get(
ResultSet rs,
String[] names,
SharedSessionContractImplementor session,
Object owner)
throws SQLException {
String value = rs.getString(names[0]);
return (value != null && value.length() > 0) ?
value.charAt(0) : null;
}
@Override
public void set(
PreparedStatement st,
Character value,
int index,
SharedSessionContractImplementor session)
throws SQLException {
if (value == null) {
st.setNull(index, Types.CHAR);
} else {
st.setString(index, String.valueOf(value));
}
}
}
The entity mapping looks like this:
@Entity(name = "Event")
@Table(name = "event")
public class Event {
@Id
@GeneratedValue
private Long id;
@Type(type = "com.vladmihalcea.book.hpjp.hibernate.type.CharacterType")
@Column(name = "event_type")
private Character type;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Character getType() {
return type;
}
public void setType(Character type) {
this.type = type;
}
}
And let's say we have these table rows:
INSERT INTO event (id, event_type) VALUES (1, 'abc');
INSERT INTO event (id, event_type) VALUES (2, '');
INSERT INTO event (id, event_type) VALUES (3, 'b');
When reading all entities:
doInJPA(entityManager -> {
List<Event> events = entityManager.createQuery(
"select e from Event e", Event.class)
.getResultList();
for(Event event : events) {
LOGGER.info("Event type: {}", event.getType());
}
});
You'll get the expected output:
Event type: a
Event type:
Event type: b
Check out the source code on GitHub.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With