Чем .NET StringComparer является эквивалентом SQL Latin__General_CI_AS - PullRequest
13 голосов
/ 22 февраля 2012

Я реализую слой кэширования между моей базой данных и моим кодом 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, если решение общего назначения не требуется.

(Примечание: уровень кэширования имеет общее назначение, поэтому я не могу делать определенные предположения окакова природа ключа и какое сопоставление будет подходящим. Все таблицы в моей базе данных имеют одинаковое сопоставление серверов по умолчанию. Мне просто нужно сопоставить сопоставление, как оно существует)

Ответы [ 4 ]

10 голосов
/ 29 мая 2014

Я недавно столкнулся с той же проблемой: мне нужен 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.
UPDATE:
Я также создал сущность с потенциальной реализацией "SQL-подобного компаратора" с использованием CollationInfo. Также следует уделить достаточно внимания , где искать «строковые ловушки» в вашей кодовой базе, поэтому, если сравнение строк, хэш-код, равенство должны быть изменены на «SQL-подобный сопоставлению», эти места равны 100% будет сломан, так что вам придется выяснить и осмотреть все места, которые могут быть сломаны.
ОБНОВЛЕНИЕ № 2:
Есть лучший и более чистый способ заставить GetHashCode () обрабатывать CompareOptions. Существует класс SortKey , который корректно работает с CompareOptions, и его можно получить с помощью

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

Вот ссылка на исходный код .NET и его реализацию.

8 голосов
/ 22 февраля 2012

Взгляните на класс 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 голосов
/ 22 февраля 2012

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

0 голосов
/ 21 июня 2019

Намного проще:

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

...