问题 什么.NET StringComparer等同于SQL的Latin1_General_CI_AS


我正在我的数据库和我的C#代码之间实现一个缓存层。我们的想法是根据参数将某些数据库查询的结果缓存到查询中。数据库正在使用默认排序规则 - 或者 SQL_Latin1_General_CP1_CI_AS 要么 Latin1_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不同,那么问题会更加微妙,但问题就不多了。对两个键的GetObject调用都没问题,并返回对应于同一数据库行的对象。但是然后用密钥A调用SaveObject会使缓存不正确;对于具有旧数据的密钥B,仍然存在缓存条目。随后的GetObject(B)将提供过时的信息。

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

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


887
2018-02-21 20:26


起源



答案:


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

CollationInfo 有一个 Comparer。它与a不完全相同 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<string>


6
2018-02-21 21:04



不幸的是IComparer不包括获取哈希码的能力 - 我需要一个IEqualityComparer,这是StringComparer提供的。 - Stuart
@Stuart - 看我的编辑,CollactionInfo确实有一个IEqualityComparer。 - dana
有没有办法通过(简单地)配置或实现基本类型来实现.NET SQL_Latin1_General_CP1_CI_AS等效?对我的情况来说,SMO是一种重量级的依赖。 - Jason Kleban
以下是某人使用SMO失败的尝试: stackoverflow.com/questions/11562042/... - Jason Kleban


答案:


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

CollationInfo 有一个 Comparer。它与a不完全相同 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<string>


6
2018-02-21 21:04



不幸的是IComparer不包括获取哈希码的能力 - 我需要一个IEqualityComparer,这是StringComparer提供的。 - Stuart
@Stuart - 看我的编辑,CollactionInfo确实有一个IEqualityComparer。 - dana
有没有办法通过(简单地)配置或实现基本类型来实现.NET SQL_Latin1_General_CP1_CI_AS等效?对我的情况来说,SMO是一种重量级的依赖。 - Jason Kleban
以下是某人使用SMO失败的尝试: stackoverflow.com/questions/11562042/... - Jason Kleban


我最近遇到了同样的问题:我需要一个 IEqualityComparer<string> 表现得像SQL一样。我试过了 CollationInfo 和它的 EqualityComparer。如果您的数据库始终如此 _如 (重音敏感)然后你的解决方案将工作,但万一你改变了排序规则 AI 要么 WI 或者其他任何“麻木不仁”,否则哈希会破裂。
为什么?如果你反编译 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受此限制,但您应该了解问题的来源。 要为具有变音符号的字符串获取相同的哈希码,您可以执行以下操作: 创建实现 的 IEqualityComparer<T> 实施 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

我知道它看起来像黑客,但在检查反编译的.NET代码后,我不确定是否有任何其他选项,以防需要通用功能。 所以请确保使用这个不完全正确的API不会陷入陷阱。
更新:
我也创造了 可能实现“类似SQL的比较器”的要点 运用 CollationInfo。 还应该给予足够的重视 在哪里搜索“字符串陷阱” 在你的代码库中,所以如果字符串比较,哈希码,等式应该改为“类似于SQL校对”那些地方是100%将会被打破,所以你必须找出并检查所有可以破坏的地方。
更新#2:
有更好更清洁的方法使GetHashCode()处理CompareOptions。有班级 SORTKEY 与CompareOptions一起正常工作,可以使用它进行检索

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

这里是 链接 到.NET源代码和实现。


9
2018-05-29 16:34



+1这是一个认真研究的答案!我希望我能不止一次地投票给你。 - Steve Pettifer
很好地解释。 - user2250250
请注意UPDATE#2 - 有一些开箱即用的方法可以让GetHashCode()正确处理CompareOptions。它更清洁,不需要任何反射黑客。不幸的是,在我发布此帖后,我才能找到它。 - yar_shukan
@yar_shukan如果可以的话,请将你的评论直接移到你的答案的最上面,因为大多数人可能不会阅读评论;-)。谢谢! - Solomon Rutzky
这真的帮助我解决了一个长期存在的独特关键问题,特别是对于更新2.我现在有一个很好的自定义StringComparer,我可以用于我的C#Dictionary :) - Mattias Åslund


SQL Server的 Server.GetStringComparer 可能有一些用处。


1
2018-02-21 21:08



不幸的是IComparer不包括获取哈希码的能力 - 我需要一个IEqualityComparer,这是StringComparer提供的。 - Stuart