.NET StringComparerは、SQLのLATIN1_GENERAL_CI_ASに相当するものです
-
28-10-2019 - |
質問
データベースとC#コードの間にキャッシュレイヤーを実装しています。アイデアは、クエリのパラメーターに基づいて、特定のDBクエリの結果をキャッシュすることです。データベースは、デフォルトの照合を使用しています - どちらか SQL_Latin1_General_CP1_CI_AS
また Latin1_General_CI_AS
, 、私は、いくつかの短いグーグルに基づいて平等と同等であり、ソートとはまったく異なると思います。
データベースの照合が使用しているため、少なくとも等式テストとハッシュコード生成のために、同じ動作を与えることができる.NET StringCompererが必要です。目標は、C#コードの.NET辞書でStringCompererを使用して、特定の文字列キーがすでにキャッシュに含まれているかどうかを判断できることです。
本当に単純化された例:
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();
}
StringCompererがデータベースの照合と一致することが重要である理由は、誤検知と偽陰性の両方がコードに悪い影響を与えることです。
StringComparerが、データベースがそれが明確であると信じている場合、2つのキーAとBが等しいと言っている場合、データベースに2つの行がある可能性があります。 B連続して - BのGetがキャッシュを誤ってヒットし、Aのために取得されたオブジェクトを返すため
StringComparerが、データベースが等しいと考えている場合、AとBが異なると言っている場合、問題はより微妙です。両方のキーのgetObject呼び出しは問題なく、同じデータベース行に対応するオブジェクトを返すことができます。しかし、キーAでSaveObjectを呼び出すと、キャッシュが正しくありません。古いデータを持つキーBのキャッシュエントリがまだあります。その後のgetobject(b)は時代遅れの情報を提供します。
したがって、私のコードが正しく機能するには、等式テストとハッシュコード生成のデータベース動作に一致するためにStringCompererが必要です。これまでのところ、私のグーグルでは、SQL照合と.NET比較が正確に同等ではないという事実に関する多くの情報が得られましたが、違いの違いのみに限定されているか、見つけることができるかどうか、違いの詳細はありません。 aに相当する弦楽器の人 明確な SQL照合汎用ソリューションが必要ない場合。
(サイドノート - キャッシュレイヤーは汎用であるため、キーの性質が何であり、どの照合が適切であるかについて特別な仮定を行うことはできません。データベース内のすべてのテーブルは、同じデフォルトサーバーの照合を共有しています。一致する必要があります。それが存在する照合)
解決
を見てください 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>
.
他のヒント
私は最近同じ問題に直面しました:私は IEqualityComparer<string>
それはSQLのようなスタイルで動作します。私はもう試した CollationInfo
そしてその EqualityComparer
. 。 DBが常にある場合 _なので (アクセントセンシティブ)その後、ソリューションは機能しますが、照合を変更した場合に備えて 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がこれによって制限されている理由はわかりませんが、問題がどこから来るのかを理解する必要があります。 Diacrityを使用した文字列用の同じハッシュコードを取得するには、次のことを行うことができます。 実装を作成します の 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-Like Comparer」の潜在的な実装を伴う要点 使用 CollationInfo
。また、十分な注意を払う必要があります 「文字列落とし穴」を検索する場所 コードベースでは、文字列の比較、ハッシュコード、平等を「SQL照合のような」に変更する必要がある場合、それらの場所は100%が壊れます。 。
更新#2:
gethashcode()を扱う比較的オプションを作成するためのより良いよりクリーンな方法があります。クラスがあります SortKey それはCompareOptionsで正しく機能し、使用して取得できます
CompareInfo.getSortKey(YourString、YourCompareoptions).gethashCode()
これが次のとおりです リンク .NETソースコードと実装へ。
SQL Server's server.getStringComperer いくつかの使用があるかもしれません。
以下ははるかに簡単です:
System.Globalization.CultureInfo.GetCultureInfo(1033)
.CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)
与えられたオプションが与えられたハッシュコードを正しく計算します。 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();
}
}
}