我正在数据库和C#代码之间实现一个缓存层。其思想是基于查询的参数来缓存某些DB查询的结果。数据库使用默认的排序规则——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比较并不完全等效的信息,但没有详细说明差异是什么,它们是否仅限于排序方面的差异,或者如果不需要通用解决方案,是否可以找到一个与特定SQL排序规则等效的StringComparer。
(附带说明-缓存层是通用的,所以我不能对密钥的性质和合适的排序规则做出特别的假设。我的数据库中的所有表都共享相同的默认服务器排序规则。我只需要匹配现有的排序规则)
我最近也遇到了同样的问题:我需要一个类似SQL风格的IEqualityComparer<string>
。我试过CollationInfo
和它的EqualityComparer
。如果您的DB始终是_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"one_answers"aa"生成相同的哈希代码,但不能为"äå"one_answers"aa"生成相同(如果你忽略大多数文化中的变音符号(AI),它们应该有相同的哈希码)。我不知道为什么.NET API受此限制,但您应该了解问题的根源。要为带有变音符号的字符串获得相同的哈希代码,您可以执行以下操作:创建实现GetHashCode
的IEqualityComparer<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可能有一些用处。