Search code examples
javahibernatejpahibernate-mapping

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?


Solution

  • 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.