Search code examples
springstringhibernateparametersspring-data-jpa

How to make Hibernate use setFixedCHAR instead of setString


Can I somehow modify the way Hibernate binds parameters to the query?

For example, I want hibernate to use OracleResultSet.setFixedChar() when executing on an string column, instead of rs.setString() when executing a JPA query via Spring data.

This is how I would do it without Hibernate:

try(PreparedStatement ps = con.executeQuery("...")) {
   if(ps.isWrapped(OraclePreparedStatement.class) {
      ps.unwrap(OraclePreparedStatement.class).setFixedCHAR(0, myStringField);
   } else {
      ps.setString(0, myStringField);
   }

   try(ResultSet rs = ps.getResultSet()) {
      while(rs.next()) {
         ... do stuff ...
      }
   }
}

Repository method (Spring data JPA):

List<Object> findByMyStringField(String myStringField);

How can I influence how Hibernate binds my variable. With the above example setString is used always.

As background: the problem is that all our Legacy DB's use CHAR columns and not VARCHAR2, so we have to deal with whitespace and setFixedCHAR should do exactly what we would want.


Solution

  • Found a solution by implementing a SqlTypeDescriptor & Custom Dialect:

      @Autowired
      private DataSource source;
    
      @Bean
      public HibernateJpaVendorAdapter getHibernateJPAVendorAdapter() {
        return new CustomHibernateJpaVendorAdaptor();
      }
    
      private static class CustomHibernateJpaVendorAdaptor extends HibernateJpaVendorAdapter {
    
        @Override
        protected Class<?> determineDatabaseDialectClass(Database database) {
          // if HSQL is copied from Spring Sourcecode to keep everything the same
          if (Database.HSQL.equals(database)) {
            return CustomHsqlDialect.class;
          }
          try {
            if (source.isWrapperFor(OracleDataSource.class)) {
              return CustomOracleDialect.class;
            }
          } catch (SQLException e) {
          }
          return super.determineDatabaseDialectClass(database);
        }
    
        private class CustomHsqlDialect extends HSQLDialect {
    
          public CustomHsqlDialect() {
            registerColumnType(Types.BOOLEAN, "boolean");
            registerHibernateType(Types.BOOLEAN, "boolean");
          }
        }
      }
    
      @NoArgsConstructor
      public static class CustomOracleDialect extends Oracle12cDialect {
    
        private static final OracleCharFix INSTANCE = new OracleCharFix();
    
    
        @Override
        protected SqlTypeDescriptor getSqlTypeDescriptorOverride(final int sqlCode) {
          switch (sqlCode) {
            case Types.VARCHAR:
              return INSTANCE;
            default:
              return super.getSqlTypeDescriptorOverride(sqlCode);
          }
        }
      }
    
      @Slf4j
      private static class OracleCharFix extends CharTypeDescriptor {
    
        @Override
        public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
          return new BasicBinder<>(javaTypeDescriptor, this) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
              throws SQLException {
              if (st.isWrapperFor(OraclePreparedStatement.class)) {
                OraclePreparedStatement ops = st.unwrap(OraclePreparedStatement.class);
                if (ops.getParameterMetaData().getParameterType(index) == Types.CHAR) {
                  ops.setFixedCHAR(index, javaTypeDescriptor.unwrap(value, String.class, options));
                } else {
                  st.setString(index, javaTypeDescriptor.unwrap(value, String.class, options));
                }
              } else {
                st.setString(index, javaTypeDescriptor.unwrap(value, String.class, options));
              }
            }
    
            @Override
            protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
              throws SQLException {
                //Is nolonger used by Hibernate in the current Version
                st.setString(name, javaTypeDescriptor.unwrap(value, String.class, options));
            }
    
            private boolean checkIfCHARByName(ResultSetMetaData metadata, String name)
              throws SQLException {
              for (int i = 1; i <= metadata.getColumnCount(); i++) {
                if (metadata.getColumnType(i) == Types.CHAR && Objects.equals(metadata.getColumnName(i), name)) {
                  return true;
                }
              }
              return false;
            }
          };
        }