Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to change the Hibernate CharacterTypeDescriptor to handle empty column values

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?

like image 244
michaelbahr Avatar asked Sep 21 '16 11:09

michaelbahr


1 Answers

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.

like image 143
Vlad Mihalcea Avatar answered Oct 21 '22 03:10

Vlad Mihalcea