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

时间:2011-03-08 19:28:40

标签: java sql-server hibernate jdbc


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

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

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

6 个答案:

答案 0 :(得分:4)

public class SQLServerUnicodeDialect extends 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() );
    }
}

答案 1 :(得分:3)

一想法......

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

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

答案 2 :(得分:1)

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

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

所以,例如

SELECT * FROM Person WHERE last_name = N'Smith'
如果将last_name字段定义为varchar并且其上有索引,

将导致表扫描。

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

答案 3 :(得分:1)

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

这是方法,我仍然在验证它的正确性 - 任何经验丰富的人的评论都欢迎

添加新的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字段的映射

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

之前的Raw 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 ..”的工作方式类似。

答案 4 :(得分:1)

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

  2. StringNVarcharType.hbm.xml内容

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

    <dependency>
        <groupId>com.mchange</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.5-pre6</version> <!-- Make sure you don't use the default dependency version found in hibernate-c3p0! -->
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-c3p0</artifactId>
        <version>3.6.10.Final</version>
        <exclusions>
            <exclusion>
                <artifactId>c3p0</artifactId>
                <groupId>c3p0</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    
  4. 让hibernate知道映射:

    <!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
    <hibernate-configuration>
        <session-factory>
            <mapping resource="StringNVarcharType.hbm.xml" />
    
            <!-- Continue with your other mappings here -->
        </session-factory>
    </hibernate-configuration>
    
  5. 在* .hbm.xml映射文件中使用nstring属性类型,其中包含nvarchar2数据库列类型。

  6. 参考文献:

    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/

答案 5 :(得分:0)

我遇到了这个问题,这是解决该问题的最简单方法。只需将以下参数添加到您的连接字符串中: sendStringParametersAsUnicode=false

实际上,"com.microsoft.sqlserver.jdbc.Parameter#getSSPAUJDBCType" 负责将每个字符串转换为 NVARCHAR,您可以忽略大小写。