Чем.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();
        }
    }
}

Server.GetStringComparer SQL Server может быть полезным.

Другие вопросы по тегам