Search code examples
javasql-serverhibernatejdbc

Getting Hibernate and SQL Server to play nice with VARCHAR and NVARCHAR



I'm currently in the process of enabling UTF-8 characters in some tables of a large database. These tables are already of MS-SQL type NVARCHAR. Additionally, I have several fields using VARCHAR as well.

There is a well known issue with Hibernate's interactions with the JDBC driver (see e.g., Mapping to varchar and nvarchar in hibernate) . In short, Hibernate/JDBC generates SQL that passes all strings as Unicode, regardless of the underlying SQL type. When a non-unicode (varchar) field in the database is compared to a Unicode input string, the indicies for that column do not match the encoding so a full table scan is performed. In the JDBC driver (both JTDS and MS versions) there is a parameter to pass Unicode strings as ASCII, but this is an all or nothing proposition that disallows international characters from being input into the the database.

Most posts I've seen on this issue have come up with one of two solutions - 1) change everything in the database to NVARCHAR or 2) set the sendStringParametersAsUnicode=false, My question then is this - is there any known solution for having VARCHAR and NVARCHAR play nicely together? It is a huge issue for my environment to change everything to NVARCHAR because of downstream dependencies and other external issues.


Solution

  • I decided to try this as a hack that might work without touching the database. To do this I created a custom type for NVARCHAR fields. This requires JDBC 4 drivers (using the ones from Microsoft) and Hibernate 3.6.0. The sendStringParametersAsUnicode is false.

    Here's the approach, I'm still verifying its correctness - any comments from folks with more experience than I are welcome

    Add a new Dialect to support the new datatype

    public class SQLAddNVarCharDialect extends SQLServerDialect {
    
        public SQLAddNVarCharDialect(){
            super();
    
            registerColumnType( Types.NVARCHAR, 8000, "nvarchar($1)" );     
            registerColumnType( Types.NVARCHAR,  "nvarchar(255)" );     
        }
    }
    

    Add the new Type. Notice the setNString in nullSafeSet

    public class NStringUserType implements UserType  {
    
        @Override
        public Object assemble(Serializable arg0, Object owner)
                throws HibernateException {
    
            return deepCopy(arg0);
        }
    
        @Override
        public Object deepCopy(Object arg0) throws HibernateException {
            if(arg0==null) return null;
            return arg0.toString();
        }
    
        @Override
        public Serializable disassemble(Object arg0) throws HibernateException {
            return (Serializable)deepCopy(arg0);
        }
    
        @Override
        public boolean equals(Object arg0, Object arg1) throws HibernateException {
            if(arg0 == null )
                return arg1 == null;
            return arg0.equals(arg1);
        }
    
        @Override
        public int hashCode(Object arg0) throws HibernateException {
            return arg0.hashCode();
        }
    
        @Override
        public boolean isMutable() {
            return false;
        }
    
    
        @Override
        public void nullSafeSet(PreparedStatement st, Object value, int index)
                throws HibernateException, SQLException {
            if(value == null)
                st.setNull(index,Types.NVARCHAR);
            else
                st.setNString(index, value.toString());
        }
    
        @Override
        public Object replace(Object arg0, Object target, Object owner)
                throws HibernateException {
            return deepCopy(arg0);
        }
    
        @Override
        public Class returnedClass() {
            return String.class;
        }
    
        @Override
        public int[] sqlTypes() {
            return new int[]{Types.NVARCHAR};
        }
    
    
        @Override
        public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner)
                throws HibernateException, SQLException {
            String result = resultSet.getString(names[0]);
            return result == null || result.trim().length() == 0 
                ? null : result;
        }
    
    }
    

    Update mappings for all NVARCHAR fields

        <property name="firstName" type="NStringUserType">
            <column name="firstName" length="40" not-null="false" />
        </property>    
    

    Raw SQL before (with sendUnicode..=true):

     exec sp_prepexec @p1 output,N'@P0 nvarchar(4000),@P1 datetime,@P2 varchar(8000),@P3 nvarchar(4000),@P4 nvarchar(4000),@P5 nvarchar(4000),@P6 nvarchar(4000)... ,N'update Account set ... where AccountId=@P35    
    

    And after:

     exec sp_prepexec @p1 output,N'@P0 varchar(8000),@P1  .... @P6 nvarchar(4000),@P7 ... ,N'update Account set ... Validated=@P4, prefix=@P5, firstName=@P6 ... where AccountId=@P35    
    

    Seems to work similarly for 'SELECT.."