让Hibernate和SQL Server与VARCHAR和NVARCHAR一起玩得很好


我目前正在大型数据库的某些表中启用UTF-8字符。 这些表已经是MS-SQL类型的NVARCHAR。 另外,我还有几个使用VARCHAR的字段。

Hibernate与JDBC驱动程序的交互存在一个众所周知的问题(参见例如, 在hibernate中映射到varchar和nvarchar )。 简而言之,无论基础SQL类型如何,Hibernate / JDBC都会生成将所有字符串作为Unicode传递的SQL。 当数据库中的非unicode(varchar)字段与Unicode输入字符串进行比较时,该列的指示与编码不匹配,因此执行全表扫描。 在JDBC驱动程序(JTDS和MS版本)中,有一个参数可以将Unicode字符串作为ASCII传递,但这是一个全有或全无的命题,它不允许将国际字符输入到数据库中。

我在这个问题上看到的大多数post都提出了两个解决方案中的一个 – 1)将数据库中的所有内容更改为NVARCHAR或2)设置sendStringParametersAsUnicode = false,我的问题是这个 – 是否有任何已知的解决方案来使用VARCHAR和NVARCHAR一起玩得很好? 由于下游依赖性和其他外部问题,我的环境将一切都更改为NVARCHAR是一个巨大的问题。

公共类SQLServerUnicodeDialect扩展org.hibernate.dialect.SQLServerDialect {public SQLServerUnicodeDialect(){super();  registerColumnType(Types.CHAR,“nchar(1)”);  registerColumnType(Types.LONGVARCHAR,“nvarchar(max)”);  registerColumnType(Types.VARCHAR,4000,“nvarchar($ l)”);  registerColumnType(Types.VARCHAR,“nvarchar(max)”);  registerColumnType(Types.CLOB,“nvarchar(max)”);  registerColumnType(Types.NCHAR,“nchar(1)”);  registerColumnType(Types.LONGNVARCHAR,“nvarchar(max)”);  registerColumnType(Types.NVARCHAR,4000,“nvarchar($ l)”);  registerColumnType(Types.NVARCHAR,“nvarchar(max)”);  registerColumnType(Types.NCLOB,“nvarchar(max)”);  registerHibernateType(Types.NCHAR,StandardBasicTypes.CHARACTER.getName());  registerHibernateType(Types.LONGNVARCHAR,StandardBasicTypes.TEXT.getName());  registerHibernateType(Types.NVARCHAR,StandardBasicTypes.STRING.getName());  registerHibernateType(Types.NCLOB,StandardBasicTypes.CLOB.getName());  }} 

一想到..

隐藏索引视图后面的varchar列。 视图转换为nvarchar。 这允许您在相同数据上维护2个接口。

这同样适用于另一种方式…使用视图为你的下游东西,但这些转换为varchar(你的所有表现在都是nvarchar)。 在这种情况下,不需要索引它们。 具有varchar值的WHERE子句(与nvarchar列进行比较)将扩展为nvarchar并将使用索引

与JDBC驱动程序的工作方式相比,这不是Hibernate问题。 在实践中,我认为唯一会出现的问题(除了明显的数据损坏,如果你将Unicode数据写入varchar列)是你在查询尝试匹配字符串时。

SQL Server将在SQL语句中隐式地将nvarchar转换为varchar,但是当您在where子句中使用字符串运行查询时,如果类型不完全匹配,则不会找到现有索引。

所以,例如

SELECT * FROM Person WHERE last_name = N'Smith' 

如果last_name字段被定义为varchar并且其上有索引,则将导致表扫描。

此性能问题的另一个解决方法是在执行查询之前使用存储过程进行类型转换。

我决定尝试将其作为可以在不触及数据库的情况下工作的黑客。 为此,我为NVARCHAR字段创建了一个自定义类型。 这需要JDBC 4驱动程序(使用Microsoft的驱动程序)和Hibernate 3.6.0。 sendStringParametersAsUnicode为false。

这是方法,我仍然在validation它的正确性 – 任何有经验的人都会欢迎我的评论

添加新的Dialect以支持新的数据类型

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

添加新类型。 注意setNString中的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; } } 

更新所有NVARCHAR字段的映射

     

原始SQL之前(使用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 

之后:

  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 

似乎对“SELECT ..”的工作方式类似。

  1. 从hibernate-core 4.3.0.Final复制StringNVarcharType.java和NVarcharTypeDescriptor.java类。

  2. StringNVarcharType.hbm.xml内容

  3. 在Maven中使用以下依赖项:

      com.mchange c3p0 0.9.5-pre6    org.hibernate hibernate-c3p0 3.6.10.Final   c3p0 c3p0    
  4. 让hibernate知道映射:

            
  5. 在具有nvarchar2数据库列类型的* .hbm.xml映射文件中使用nstring属性类型。

参考文献:

  1. http://alenovarini.wikidot.com/mapping-a-custom-type-in​​-hibernate
  2. http://blog.xebia.com/2009/11/09/understanding-and-writing-hibernate-user-types/