Чем.NET StringComparer является эквивалентом SQL Latin1_General_CI_AS
Я реализую слой кэширования между моей базой данных и моим кодом C#. Идея состоит в том, чтобы кэшировать результаты определенных запросов БД на основе параметров запроса. База данных использует параметры сортировки по умолчанию - либо SQL_Latin1_General_CP1_CI_AS
или же Latin1_General_CI_AS
Я полагаю, что, основываясь на некотором кратком поиске в Google, это эквивалентно равенству, но отличается для сортировки.
Мне нужен.NET StringComparer, который может дать мне то же поведение, по крайней мере, для тестирования на равенство и генерации хеш-кода, что используется для сортировки базы данных. Цель состоит в том, чтобы иметь возможность использовать StringComparer в словаре.NET в коде C#, чтобы определить, находится ли конкретный строковый ключ в кэше или нет.
Действительно упрощенный пример:
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 последовательно - потому что get for B будет неправильно попадать в кеш и вернет объект, который был найден для A.
Проблема становится более тонкой, если StringComparer говорит, что A и B отличаются, когда база данных считает, что они равны, но не менее проблематичны. Вызовы GetObject для обоих ключей будут в порядке, и они будут возвращать объекты, соответствующие одной и той же строке базы данных. Но тогда вызов SaveObject с ключом A оставил бы кеш неправильным; для ключа B все еще будет запись в кэше, содержащая старые данные. Последующий GetObject(B) даст устаревшую информацию.
Поэтому, чтобы мой код работал правильно, мне нужно, чтобы StringComparer соответствовал поведению базы данных для тестирования на равенство и генерации хэш-кода. До сих пор мой поиск в Google дал много информации о том, что сопоставления SQL и сравнения.NET не совсем эквивалентны, но нет подробностей о том, в чем заключаются различия, ограничены ли они только различиями в сортировке, или можно ли найти StringComparer, который эквивалентен определенному сопоставлению SQL, если решение общего назначения не требуется.
(Примечание: уровень кэширования является общим назначением, поэтому я не могу делать конкретных предположений о том, каков характер ключа и какое сопоставление будет уместным. Все таблицы в моей базе данных имеют одинаковое сопоставление серверов по умолчанию. Мне просто нужно сопоставить сопоставление как оно есть)
3 ответа
Посмотрите на 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
РЕДАКТИРОВАТЬ: CollationInfo
действительно есть свойство с именем EqualityComparer
который является IEqualityComparer<string>
,
Я недавно столкнулся с той же проблемой: мне нужно IEqualityComparer<string>
который ведет себя в стиле SQL. я пробовал 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" и "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 и его реализацию.
Гораздо проще следующее:
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();
}
}
}