.NET StringComparer与SQL的Latin1_General_CI_AS等效

时间:2012-02-21 20:27:00

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

我正在我的数据库和我的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不同,那么问题会更加微妙,但问题就不多了。对两个键的GetObject调用都没问题,并返回对应于同一数据库行的对象。但是然后用密钥A调用SaveObject会使缓存不正确;对于具有旧数据的密钥B,仍然存在缓存条目。随后的GetObject(B)将提供过时的信息。

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

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

4 个答案:

答案 0 :(得分:10)

我最近遇到了同样的问题:我需要一个行为类似SQL的IEqualityComparer<string>。我已尝试CollationInfo及其EqualityComparer。如果您的数据库始终 _AS (区分重音),那么您的解决方案将起作用,但如果您更改 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 直接调用它会产生所需的结果: 见这个例子:

CollationInfo

输出结果为:

{{1}}

我知道它看起来像黑客,但在检查反编译的.NET代码后,我不确定是否还有其他选项,以防需要通用功能。 因此,请确保使用这种不完全正确的API不会陷入陷阱 的更新:
我还使用{{1}}创建了create implementation。 在你的代码库中也应该有足够的注意力the gist with potential implementation of "SQL-like comparer",所以如果字符串比较,哈希码,等式应该改为“类似于SQL校对”那么这些地方是100%会被打破,所以你会有找出并检查所有可以破坏的地方。
更新#2:
有更好更清晰的方法使GetHashCode()处理CompareOptions。有一个类where to search for "string pitfalls"可以与CompareOptions一起正常工作,可以使用

进行检索
  

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

这是SortKey到.NET的源代码和实现。

答案 1 :(得分:6)

看一下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<string>

答案 2 :(得分:1)

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

答案 3 :(得分:0)

以下更简单:

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();
        }
    }
}