Question

J'implémente une couche de mise en cache entre ma base de données et mon code C #. L'idée est de mettre en cache les résultats de certaines requêtes DB en fonction des paramètres de la requête. La base de données utilise la collation par défaut - soit SQL_Latin1_General_CP1_CI_AS ou Latin1_General_CI_AS, qui, je crois, sur la base d'un bref googler, est équivalent à l'égalité, juste différent pour le tri.

J'ai besoin d'un .NET STRINGCOMPARER qui peut me donner le même comportement, au moins pour les tests d'égalité et la génération de codes de hash, comme le classement de la base de données utilise. L'objectif est de pouvoir utiliser le stringcomparer dans un dictionnaire .NET en code C # pour déterminer si une clé de chaîne particulière est déjà dans le cache ou non.

Un exemple vraiment simplifié:

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 raison pour laquelle il est important que le stringcomparer correspond au collation de la base de données est que les faux positifs et les faux négatifs auraient de mauvais effets pour le code.

Si le stringcomparer dit que les deux clés A et B sont égales lorsque la base de données estime qu'elles sont distinctes, alors il pourrait y avoir deux lignes dans la base de données avec ces deux clés, mais que le cache empêchera le deuxième jamais renvoyé si vous êtes demandé A et B succession - Parce que le GET pour B frappera incorrectement le cache et renverra l'objet qui a été récupéré pour A.

Le problème est plus subtil si le stringcomparer dit que A et B sont différents lorsque la base de données estime qu'ils sont égaux, mais non moins problématiques. Les appels GetObject pour les deux clés seraient bien et les objets renvoyés correspondant à la même ligne de base de données. Mais ensuite appeler SaveObject avec la clé A laisserait le cache incorrect; Il y aurait toujours une entrée de cache pour la clé B qui a les anciennes données. Un GetObject (b) ultérieur donnerait des informations obsolètes.

Donc, pour que mon code fonctionne correctement, j'ai besoin que le string obcomparateur corresponde au comportement de la base de données pour les tests d'égalité et la génération de code hashcode. Jusqu'à présent, mon Google a donné beaucoup d'informations sur le fait que les collations SQL et les comparaisons .NET ne sont pas exactement équivalentes, mais aucun détail sur les différences, qu'ils soient limités à des différences de tri, ou s'il est possible de trouver un stringcomparer qui équivaut à un spécifique Collation SQL Si une solution à usage général n'est pas nécessaire.

(Note latérale - La couche de mise en cache est à des fins générales, je ne peux donc pas faire des hypothèses particulières sur la nature de la clé et la collation appropriée. Toutes les tables de ma base de données partagent le même collation de serveur par défaut. J'ai juste besoin de correspondre le collation tel qu'il existe)

Était-ce utile?

La solution

Jetez un œil au CollationInfo classer. Il est situé dans une assemblée appelée Microsoft.SqlServer.Management.SqlParser.dll Bien que je ne sache pas totalement où obtenir cela. Il y a une liste statique de Collations (noms) et une méthode statique GetCollationInfo (de nom).

Chaque CollationInfo a un Comparer. Ce n'est pas exactement la même chose qu'un StringComparer mais a des fonctionnalités similaires.

ÉDITER: Microsoft.sqlServer.management.sqlparser.dll fait partie du package d'objets de gestion partagée (SMO). Cette fonctionnalité peut être téléchargée pour SQL Server 2008 R2 ici:

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

ÉDITER: CollationInfo a une propriété nommée EqualityComparer qui est un IEqualityComparer<string>.

Autres conseils

J'ai récemment été confronté au même problème: j'ai besoin d'un IEqualityComparer<string> Cela se comporte dans un style SQL. J'ai essayé CollationInfo et son EqualityComparer. Si votre DB est toujours _COMME (Sensitif IA ou WI ou quelle que soit "insensible" sinon le hachage se casse.
Pourquoi? Si tu décompiles Microsoft.sqlserver.management.sqlparser.dll Et regardez à l'intérieur, vous découvrirez que CollationInfo Utilisations en interne CultureAwareComparer.GetHashCode (C'est une classe interne de mscorlib.dll) et enfin il fait ce qui suit:

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

Comme vous pouvez le voir, il peut produire le même code de hash pour "aa" et "aa", mais pas pour "äå" et "aa" (qui sont les mêmes, si vous ignorez les diacritiques (AI) dans la majorité des cultures, donc elles devraient donc ont le même code de hash). Je ne sais pas pourquoi l'API .NET est limité par cela, mais vous devez comprendre d'où peut venir le problème. Pour obtenir le même code de hash pour les chaînes avec Diacritics, vous pouvez effectuer ce qui suit: créer une implémentation de IEqualityComparer<T> implémentation du GetHashCode qui appellera approprié CompareInfoLes objets sont GetHashCodeOfString via la réflexion car cette méthode est interne et ne peut pas être utilisée directement. Mais l'appeler directement avec correct CompareOptions produira le résultat souhaité: voir cet exemple:

    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 sortie est:

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

Je sais que cela ressemble au piratage, mais après avoir inspecté le code .NET décompilé, je ne sais pas s'il y a une autre option au cas où la fonctionnalité générique serait nécessaire. Assurez-vous donc que vous ne tomberez pas dans le piège en utilisant cette API non complètement correcte.
METTRE À JOUR:
J'ai aussi créé L'essentiel avec une mise en œuvre potentielle de "comparateur de type SQL" utilisant CollationInfo. Il devrait également y avoir suffisamment d'attention où rechercher "Pièges de chaîne" Dans votre base de code, donc si la comparaison des chaînes, HashCode, l'égalité doit être changée en "SQL Collation-like", ces endroits sont brisés à 100%, vous devrez donc découvrir et inspecter tous les endroits qui peuvent être brisés .
Mise à jour n ° 2:
Il existe un moyen meilleur et plus propre de faire des compareoptions GethashCode (). Il y a la classe Clé de pointe Cela fonctionne correctement avec CompareOptions et il peut être récupéré en utilisant

Compareinfo.getsortkey (yourString, yourCompareOptions) .GethashCode ()

Voici la lien à .NET Code source et implémentation.

SQL Server Server.getStringcomparer peut être utile.

Ce qui suit est beaucoup plus simple:

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

Ça vient de https://docs.microsoft.com/en-us/dotnet/api/system.globalisation.globalisationExtensions?view=netframework-4.8

Il calcule correctement le code de hash si donné les options données. Vous devrez toujours couper les espaces de fuite manuellement, car ils sont rejetés par ANSI SQL mais pas dans .NET

Voici un emballage qui coupe les espaces.

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top