【问题标题】:What .NET StringComparer is equivalent SQL's Latin1_General_CI_AS什么 .NET StringComparer 相当于 SQL 的 Latin1_General_CI_AS
【发布时间】:2012-03-12 04:21:22
【问题描述】:

我正在我的数据库和我的 C# 代码之间实现一个缓存层。这个想法是根据查询的参数缓存某些数据库查询的结果。数据库使用默认排序规则 - SQL_Latin1_General_CP1_CI_ASLatin1_General_CI_AS,我相信基于一些简短的谷歌搜索,它们等同于相等,只是排序不同。

我需要一个 .NET StringComparer,它可以提供与数据库排序规则相同的行为,至少对于相等性测试和哈希码生成而言。目标是能够在 C# 代码中使用 .NET 字典中的 StringComparer 来确定特定的字符串键是否已经在缓存中。

一个非常简单的例子:

var comparer = StringComparer.??? // What goes here?

private static Dictionary<string, MyObject> cache =
    new Dictionary<string, MyObject>(comparer);

public static MyObject GetObject(string key) {
    if (cache.ContainsKey(key)) {
        return cache[key].Clone();
    } else {
        // invoke SQL "select * from mytable where mykey = @mykey"
        // with parameter @mykey set to key
        MyObject result = // object constructed from the sql result
        cache[key] = result;
        return result.Clone();
    }
}
public static void SaveObject(string key, MyObject obj) {
    // invoke SQL "update mytable set ... where mykey = @mykey" etc
    cache[key] = obj.Clone();
}

StringComparer 与数据库的排序规则匹配很重要的原因是误报和误报都会对代码产生不良影响。

如果 StringComparer 说当数据库认为两个键 A 和 B 不同时它们是相等的,那么数据库中可能有两行具有这两个键,但是如果询问,缓存将阻止返回第二个连续用于 A 和 B - 因为 B 的 get 将错误地命中缓存并返回为 A 检索的对象。

如果 StringComparer 在数据库认为 A 和 B 相等的情况下说 A 和 B 是不同的,那么问题就更加微妙了,但问题同样如此。两个键的 GetObject 调用都可以,并返回对应于同一数据库行的对象。但是随后使用键 A 调用 SaveObject 会使缓存不正确;对于具有旧数据的键 B,仍然会有一个缓存条目。随后的 GetObject(B) 将提供过时的信息。

因此,为了使我的代码正常工作,我需要 StringComparer 来匹配数据库行为以进行相等性测试和哈希码生成。到目前为止,我的谷歌搜索已经产生了很多关于 SQL 排序规则和 .NET 比较不完全等价的信息,但没有详细说明差异是什么,它们是否仅限于排序差异,或者是否有可能找到如果不需要通用解决方案,则等效于 特定 SQL 排序规则的 StringComparer。

(旁注 - 缓存层是通用的,所以我不能对密钥的性质和合适的排序规则做出特定的假设。我数据库中的所有表都共享相同的默认服务器排序规则。我只是需要匹配现有的排序规则)

【问题讨论】:

    标签: c# .net sql-server collation stringcomparer


    【解决方案1】:

    我最近遇到了同样的问题:我需要一个行为类似于 SQL 的IEqualityComparer&lt;string&gt;。我试过CollationInfo 和它的EqualityComparer。如果您的数据库始终是 _AS(区分重音),那么您的解决方案将起作用,但如果您更改了 AIWI 的排序规则或任何“不敏感”,否则散列将中断。
    为什么?如果您反编译 Microsoft.SqlServer.Management.SqlParser.dll 并查看内部,您会发现 CollationInfo 内部使用 CultureAwareComparer.GetHashCode (它是 mscorlib.dll 的内部类),最后它执行以下:

    public override int GetHashCode(string obj)
    {
      if (obj == null)
        throw new ArgumentNullException("obj");
      CompareOptions options = CompareOptions.None;
      if (this._ignoreCase)
        options |= CompareOptions.IgnoreCase;
      return this._compareInfo.GetHashCodeOfString(obj, options);
    }
    

    如您所见,它可以为“aa”和“AA”生成相同的哈希码,但不能为“äå”和“aa”(如果您忽略大多数文化中的变音符号 (AI),它们是相同的,所以他们应该有相同的哈希码)。我不知道为什么 .NET API 会受此限制,但您应该明白问题出在哪里。 要为带有变音符号的字符串获取相同的哈希码,您可以执行以下操作:create implementation of IEqualityComparer&lt;T&gt; 实现 GetHashCode,它将通过反射调用适当的 CompareInfo 对象的 GetHashCodeOfString,因为此方法是内部的并且可以'不能直接使用。但是直接用正确的CompareOptions 调用它会产生预期的结果: 看这个例子:

        static void Main(string[] args)
        {
            const string outputPath = "output.txt";
            const string latin1GeneralCiAiKsWs = "Latin1_General_100_CI_AI_KS_WS";
            using (FileStream fileStream = File.Open(outputPath, FileMode.Create, FileAccess.Write))
            {
                using (var streamWriter = new StreamWriter(fileStream, Encoding.UTF8))
                {
                    string[] strings = { "aa", "AA", "äå", "ÄÅ" };
                    CompareInfo compareInfo = CultureInfo.GetCultureInfo(1033).CompareInfo;
                    MethodInfo GetHashCodeOfString = compareInfo.GetType()
                        .GetMethod("GetHashCodeOfString",
                        BindingFlags.Instance | BindingFlags.NonPublic,
                        null,
                        new[] { typeof(string), typeof(CompareOptions), typeof(bool), typeof(long) },
                        null);
    
                    Func<string, int> correctHackGetHashCode = s => (int)GetHashCodeOfString.Invoke(compareInfo,
                        new object[] { s, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0L });
    
                    Func<string, int> incorrectCollationInfoGetHashCode =
                        s => CollationInfo.GetCollationInfo(latin1GeneralCiAiKsWs).EqualityComparer.GetHashCode(s);
    
                    PrintHashCodes(latin1GeneralCiAiKsWs, incorrectCollationInfoGetHashCode, streamWriter, strings);
                    PrintHashCodes("----", correctHackGetHashCode, streamWriter, strings);
                }
            }
            Process.Start(outputPath);
        }
        private static void PrintHashCodes(string collation, Func<string, int> getHashCode, TextWriter writer, params string[] strings)
        {
            writer.WriteLine(Environment.NewLine + "Used collation: {0}", collation + Environment.NewLine);
            foreach (string s in strings)
            {
                WriteStringHashcode(writer, s, getHashCode(s));
            }
        }
    

    输出是:

    Used collation: Latin1_General_100_CI_AI_KS_WS
    aa, hashcode: 2053722942
    AA, hashcode: 2053722942
    äå, hashcode: -266555795
    ÄÅ, hashcode: -266555795
    
    Used collation: ----
    aa, hashcode: 2053722942
    AA, hashcode: 2053722942
    äå, hashcode: 2053722942
    ÄÅ, hashcode: 2053722942
    

    我知道它看起来像 hack,但在检查了反编译的 .NET 代码后,我不确定是否有任何其他选项以防万一需要通用功能。 因此,请确保您不会陷入使用这个不完全正确的 API 的陷阱。
    更新:
    我还使用CollationInfo 创建了the gist with potential implementation of "SQL-like comparer"。 另外在你的代码库中应该有足够的注意力where to search for "string pitfalls",所以如果字符串比较、哈希码、相等应该改为“SQL collat​​ion-like”那些地方100%会被破坏,所以你必须找到出去检查所有可以坏的地方。
    更新 #2:
    有更好更简洁的方法可以让 GetHashCode() 处理 CompareOptions。有一个类 SortKey 可以与 CompareOptions 一起正常工作,并且可以使用

    检索它

    CompareInfo.GetSortKey(yourString, yourCompareOptions).GetHashCode()

    这里是link转.NET的源代码和实现。

    更新 #3:
    如果您使用的是 .NET Framework 4.7.1+,则应使用 this recent answer 建议的新 GlobalizationExtensions class

    【讨论】:

    • +1 这是一个经过认真研究的答案!我希望我能不止一次投票给你。
    • 请注意更新 #2 - 有一种开箱即用的方法可以让 GetHashCode() 正确处理 CompareOptions。这是更清洁的方式,不需要任何反射技巧。不幸的是,在我发完这篇文章之后的一段时间,我才找到它。
    • @yar_shukan 如果可以,请将您的评论直接移到此评论的最上方,因为大多数人可能不阅读 cmets ;-)。谢谢!
    • 这确实帮助我解决了一个长期存在的唯一键问题,尤其是更新 2。我现在有一个很好的自定义 StringComparer 可以用于我的 C# 字典:)
    【解决方案2】:

    看看CollationInfo 类。它位于一个名为 Microsoft.SqlServer.Management.SqlParser.dll 的程序集中,尽管我不完全确定从哪里得到它。有一个Collations(名称)的静态列表和一个静态方法GetCollationInfo(按名称)。

    每个CollationInfo 都有一个Comparer。它与StringComparer 不完全相同,但具有相似的功能。

    编辑: Microsoft.SqlServer.Management.SqlParser.dll 是共享管理对象 (SMO) 包的一部分。可以在此处为 SQL Server 2008 R2 下载此功能:

    http://www.microsoft.com/download/en/details.aspx?id=16978#SMO

    编辑: CollationInfo 确实有一个名为 EqualityComparer 的属性,它是一个 IEqualityComparer&lt;string&gt;

    【讨论】:

    • 不幸的是,IComparer 不包括获取哈希码的能力 - 我需要一个 IEqualityComparer,这是 StringComparer 提供的。
    • @Stuart - 查看我的编辑,ColactionInfo 确实有一个 IEqualityComparer。
    • 有没有办法通过(简单)配置或实现基本类型来实现 .NET SQL_Latin1_General_CP1_CI_AS 等效? SMO 对我的情况来说是一个重量级的依赖项。
    • 这里有人尝试使用 SMO 失败:stackoverflow.com/questions/11562042/…
    • 这个库有问题。 Sql 在比较字符串时不考虑尾随空格,但这个库会。
    【解决方案3】:

    SQL Server 的Server.GetStringComparer 可能有点用处。

    【讨论】:

    • 不幸的是,IComparer 不包括获取哈希码的能力 - 我需要 StringComparer 提供的 IEqualityComparer。
    【解决方案4】:

    下面要简单得多:

    System.Globalization.CultureInfo.GetCultureInfo(1033)
                  .CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)
    

    来自https://docs.microsoft.com/en-us/dotnet/api/system.globalization.globalizationextensions?view=netframework-4.8

    在给定选项的情况下,它会正确计算哈希码。 您仍然需要手动修剪尾随空格,因为它们被 ANSI sql 丢弃但不在 .net 中

    这是一个修剪空格的包装器。

    using System.Collections.Generic;
    using System.Globalization;
    
    namespace Wish.Core
    {
        public class SqlStringComparer : IEqualityComparer<string>
        {
            public static IEqualityComparer<string> Instance { get; }
    
            private static IEqualityComparer<string> _internalComparer =
                CultureInfo.GetCultureInfo(1033)
                           .CompareInfo
                           .GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth);
    
    
    
            private SqlStringComparer()
            {
            }
    
            public bool Equals(string x, string y)
            {
                //ANSI sql doesn't consider trailing spaces but .Net does
                return _internalComparer.Equals(x?.TrimEnd(), y?.TrimEnd());
            }
    
            public int GetHashCode(string obj)
            {
                return _internalComparer.GetHashCode(obj?.TrimEnd());
            }
    
            static SqlStringComparer()
            {
                Instance = new SqlStringComparer();
            }
        }
    }
    
    

    【讨论】:

    • 不错的方法!这个类是从 .NET Framework 4.7.1 开始引入的,但在我的原始帖子中绝对值得使用而不是解决方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-15
    • 1970-01-01
    • 2010-09-16
    相关资源
    最近更新 更多