Pregunta

Estoy implementando una capa de almacenamiento en caché entre mi base de datos y mi código C#. La idea es almacenar en caché los resultados de ciertas consultas de DB en función de los parámetros a la consulta. La base de datos está utilizando la recopilación predeterminada, ya sea SQL_Latin1_General_CP1_CI_AS o Latin1_General_CI_AS, que creo que, en base a un breve Google, son equivalentes a la igualdad, simplemente diferentes para la clasificación.

Necesito un .NET StringComparer que pueda darme el mismo comportamiento, al menos para las pruebas de igualdad y la generación de hashcode, como está utilizando la recopilación de la base de datos. El objetivo es poder usar el StringComparer en un diccionario .NET en el código C# para determinar si una clave de cadena en particular ya está en la memoria caché o no.

Un ejemplo realmente simplificado:

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

La razón por la que es importante que el StringComparer coincida con la recopilación de la base de datos es que tanto los falsos positivos como los falsos negativos tendrían malos efectos para el código.

Si el StringComparer dice que dos claves A y B son iguales cuando la base de datos cree que son distintas, entonces podría haber dos filas en la base de datos con esas dos claves, pero el caché evitará que el segundo se devuelva si se le pide A y B En sucesión: porque el Get for B golpeará incorrectamente el caché y devolverá el objeto que se recuperó para A.

El problema es más sutil si el StringComparer dice que A y B son diferentes cuando la base de datos cree que son iguales, pero no menos problemáticas. Los llamados de GetObject para ambas claves estarían bien, y devolver los objetos correspondientes a la misma fila de la base de datos. Pero luego llamar a SaveObject con la tecla A dejaría incorrecto el caché; Todavía habría una entrada de caché para la clave B que tiene los datos antiguos. Un GetObject (b) posterior daría información anticuada.

Entonces, para que mi código funcione correctamente, necesito que StringComparer coincida con el comportamiento de la base de datos para las pruebas de igualdad y la generación de hashcode. Mi Google hasta ahora ha arrojado mucha información sobre el hecho de que las comparaciones de SQL Collations y .NET no son exactamente equivalentes, pero no hay detalles sobre cuáles son las diferencias, si están limitadas solo a diferencias en la clasificación o si es posible encontrar un StringComparer que es equivalente a un específico COLACIÓN SQL Si no se necesita una solución de propósito general.

(Nota al margen: la capa de almacenamiento en caché es un propósito general, por lo que no puedo hacer suposiciones particulares sobre cuál es la naturaleza de la clave y qué colatación sería apropiada. Todas las tablas en mi base de datos comparten la misma recopilación de servidor predeterminado. Solo necesito coincidir con la recopilación tal como existe)

¿Fue útil?

Solución

Echa un vistazo al CollationInfo clase. Se encuentra en un ensamblaje llamado Microsoft.SqlServer.Management.SqlParser.dll Aunque no estoy totalmente seguro de dónde obtener esto. Hay una lista estática de Collations (nombres) y un método estático GetCollationInfo (por nombre).

Cada CollationInfo tiene un Comparer. No es exactamente lo mismo que un StringComparer pero tiene una funcionalidad similar.

EDITAR: Microsoft.sqlserver.management.sqlparser.dll es parte del paquete de objetos de administración compartida (SMO). Esta característica se puede descargar para SQL Server 2008 R2 aquí:

http://www.microsoft.com/download/en/details.aspx?id=16978#smo

EDITAR: CollationInfo tiene una propiedad nombrada EqualityComparer que es un IEqualityComparer<string>.

Otros consejos

Recientemente me he enfrentado al mismo problema: necesito un IEqualityComparer<string> Eso se comporta en estilo SQL. He intentado CollationInfo y es EqualityComparer. Si tu DB es siempre _COMO (acento sensible) entonces su solución funcionará, pero en caso de que cambie la recopilación que es AI o Wisconsin O lo que sea "insensible" de lo contrario que se rompa el hash.
¿Por qué? Si te descomprimes Microsoft.sqlserver.management.sqlparser.dll y mira por dentro, descubrirás que CollationInfo usa internamente CultureAwareComparer.GetHashCode (Es la clase interna de MSCorlib.dll) y finalmente hace lo siguiente:

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

Como puede ver, puede producir el mismo hashcode para "aa" y "aa", pero no para "äå" y "aa" (que son iguales, si ignora a los diacríticos (AI) en la mayoría de las culturas, por lo que deberían tener el mismo hashcode). No sé por qué la API .NET está limitada por esto, pero debes entender de dónde puede provenir el problema. Para obtener el mismo hashcode para cadenas con diacríticos, puede hacer lo siguiente: crear implementación de IEqualityComparer<T> implementando el GetHashCode que llamará apropiado CompareInfo'S Object's GetHashCodeOfString a través de la reflexión porque este método es interno y no se puede usar directamente. Pero llamarlo directamente con correcto CompareOptions producirá el resultado deseado: ver este ejemplo:

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

La salida es:

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

Sé que se parece al truco, pero después de inspeccionar el código .NET descompilado, no estoy seguro de si hay alguna otra opción en caso de que se necesite la funcionalidad genérica. Así que asegúrese de no caer en trampa usando esta API no completamente correcta.
ACTUALIZAR:
También he creado La esencia con la implementación potencial de "comparador similar a SQL" usando CollationInfo. También se debe prestar suficiente atención dónde buscar "trampas de cadena" En su base de código, por lo que si la comparación de cadenas, hashcode, la igualdad debe cambiarse a "SQL, como la colación", esos lugares están al 100% se romperán, por lo que tendrá que averiguar e inspeccionar todos los lugares que se pueden romper con .
Actualización #2:
Hay una forma mejor y más limpia de hacer comparaciones de trato gethashcode (). Hay la clase Kinkey que funciona correctamente con comparaciones y se puede recuperar usando

CompareInfo.getSortKey (YourString, YourComPareOptions) .GethashCode ()

Aquí está el Enlace a .NET Código fuente e implementación.

Servidor SQL Servidor.getStringComparer puede ser de alguna utilidad.

Lo siguiente es mucho más simple:

System.Globalization.CultureInfo.GetCultureInfo(1033)
              .CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)

Viene de https://docs.microsoft.com/en-us/dotnet/api/system.globalization.globalizationextensions?view=netframework-4.8

Calcula el ChoCode correctamente dadas las opciones dadas. Todavía tendrá que recortar espacios finales manualmente, ya que están descartados por ANSI SQL pero no en .NET

Aquí hay un envoltorio que recorta espacios.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top