让 Hibernate 和 SQL Server 与 VARCHAR 和 NVARCHAR 配合使用

Posted

技术标签:

【中文标题】让 Hibernate 和 SQL Server 与 VARCHAR 和 NVARCHAR 配合使用【英文标题】:Getting Hibernate and SQL Server to play nice with VARCHAR and NVARCHAR 【发布时间】:2011-07-11 08:55:36 【问题描述】:

我目前正在大型数据库的某些表中启用 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 是一个巨大问题。

【问题讨论】:

【参考方案1】: 公共类 SQLServerUnicodeDialect 扩展 org.hibernate.dialect.SQLServerDialect 公共 SQLServerUnicodeDialect() 极好的(); 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());

【讨论】:

还应选择特定的 sqlserverdialect 类(例如 sqlserverdialect2008 类用于 sql server 2008)【参考方案2】:

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

这是方法,我仍在验证它的正确性 - 欢迎任何比我有更多经验的人的 cmets

添加新的方言以支持新的数据类型

public class SQLAddNVarCharDialect extends SQLServerDialect 

    public SQLAddNVarCharDialect()
        super();

        registerColumnType( Types.NVARCHAR, 8000, "nvarchar($1)" );     
        registerColumnType( Types.NVARCHAR,  "nvarchar(255)" );     
    

添加新类型。注意nullSafeSet中的setNString

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>    

之前的原始 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.." 的工作方式类似

【讨论】:

【参考方案3】:

一个想法..

将您的 varchar 列隐藏在索引视图后面。视图投射到 nvarchar。这允许您在同一数据上维护 2 个接口。

这同样适用于其他方式...为您的下游内容使用视图,但这些视图转换为 varchar(您的所有表现在都是 nvarchar)。在这种情况下,不需要索引它们。具有 varchar 值的 WHERE 子句(与 nvarchar 列相比)将扩大到 nvarchar 并且将使用索引

【讨论】:

【参考方案4】:

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

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

所以,例如

SELECT * FROM Person WHERE last_name = N'Smith'

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

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

【讨论】:

这可能有效,但使用存储过程在很大程度上抵消了使用 Hibernate 的许多好处 :) 我完全同意。我怀疑如果你想在混合 varchar/nvarchar 环境中使用 Hibernate,它会带来很多困难。转换您的架构并使用视图中的数据类型转换解决下游依赖关系可能更简单。【参考方案5】:

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

    StringNVarcharType.hbm.xml 内容

    在 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>
    

    让休眠知道映射:

    <!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>
    

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

参考资料:

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

【讨论】:

【参考方案6】:

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

其实“com.microsoft.sqlserver.jdbc.Parameter#getSSPAUJDBCType”负责把每一个字符串都转换成NVARCHAR,大小写可以忽略。

【讨论】:

这是一个“全有或全无”的解决方案。问题是是否有适用于 varchar 和 nvarchar 的解决方案。

以上是关于让 Hibernate 和 SQL Server 与 VARCHAR 和 NVARCHAR 配合使用的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate + JPA + jTDS + SQL Server = Unicode 问题

Spring Boot 未使用 Spring Data 和 Hibernate 与 sql Server 一起运行

Spring Hibernate无法使用名称中的连字符连接到SQL Server

Microsoft SQL Server 2014 的 Hibernate 方言是啥?

hibernate.cfg.xml 配置SQL server,MySQL,Oracle

Spring+Hibernate+SQL Server之Web开发环境设置浅谈