什么.NET StringComparer等效于SQL的Latin1_General_CI_AS



我正在数据库和C#代码之间实现一个缓存层。其思想是基于查询的参数来缓存某些DB查询的结果。数据库使用默认的排序规则——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相等时,它们是不同的,那么问题就更微妙了,但问题也同样严重。对这两个键的GetObject调用都可以,并返回对应于同一数据库行的对象。但是使用键A调用SaveObject会导致缓存不正确;仍将存在具有旧数据的密钥B的高速缓存条目。随后的GetObject(B)将给出过时的信息。

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

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

我最近也遇到了同样的问题:我需要一个类似SQL风格的IEqualityComparer<string>。我试过CollationInfo和它的EqualityComparer。如果您的DB始终是_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"one_answers"aa"生成相同的哈希代码,但不能为"äå"one_answers"aa"生成相同(如果你忽略大多数文化中的变音符号(AI),它们应该有相同的哈希码)。我不知道为什么.NET API受此限制,但您应该了解问题的根源。要为带有变音符号的字符串获得相同的哈希代码,您可以执行以下操作:创建实现GetHashCodeIEqualityComparer<T>的实现,该实现将通过反射调用相应的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
更新:
我还创建了使用CollationInfo潜在实现"类SQL比较器"的要点。此外,还应该足够注意在代码库中搜索"字符串陷阱"的位置,因此,如果字符串比较、hashcode、equality应该改为"SQL排序规则",那么这些地方100%都会被破坏,所以你必须找出并检查所有可能被破坏的地方
更新#2:
有一种更好、更干净的方法可以使GetHashCode()处理CompareOptions。有一个类SortKey可以与CompareOptions一起正常工作,并且可以使用进行检索

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

这是指向.NET源代码和实现的链接。

更新#3:
如果您使用的是.NET Framework 4.7.1+,则应该使用最近的答案中提出的新GlobalizationExtensions类。

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

每个CCD_ 19具有一个CCD_。它与StringComparer并不完全相同,但具有类似的功能。

EDIT:Microsoft.SqlServer.Management.SqlParser.dll是共享管理对象(SMO)包的一部分。此功能可在以下位置下载用于SQL Server 2008 R2:

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

EDIT:CollationInfo确实有一个名为EqualityComparer的属性,该属性是IEqualityComparer<string>

以下要简单得多:

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

它来自https://learn.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();
        }
    }
}

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

最新更新