Vérifier le nom de colonne dans un objet SqlDataReader


212

Comment vérifier si une colonne existe dans un SqlDataReader objet? Dans ma couche d'accès aux données, j'ai créé une méthode qui crée le même objet pour plusieurs appels de procédures stockées. L'une des procédures stockées possède une colonne supplémentaire qui n'est pas utilisée par les autres procédures stockées. Je souhaite modifier la méthode pour l'adapter à chaque scénario.

Ma candidature est écrite en C #.

Réponses:


332
public static class DataRecordExtensions
{
    public static bool HasColumn(this IDataRecord dr, string columnName)
    {
        for (int i=0; i < dr.FieldCount; i++)
        {
            if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
                return true;
        }
        return false;
    }
}

L'utilisation de Exceptions pour la logique de contrôle comme dans certaines autres réponses est considérée comme une mauvaise pratique et a des coûts de performance. Il envoie également des faux positifs au profileur de # exceptions levées et Dieu aide toute personne configurant son débogueur à casser les exceptions levées.

GetSchemaTable () est également une autre suggestion dans de nombreuses réponses. Ce ne serait pas un moyen préféré de vérifier l'existence d'un champ car il n'est pas implémenté dans toutes les versions (il est abstrait et lève NotSupportedException dans certaines versions de dotnetcore). GetSchemaTable est également trop performant en termes de performances car c'est une fonction assez lourde si vous consultez la source .

La boucle à travers les champs peut avoir un petit impact sur les performances si vous l'utilisez beaucoup et vous pouvez envisager de mettre en cache les résultats.


Et si un alias est utilisé? La comparaison des noms échouera.
Murphybro2

Il est discutable que l'utilisation du flux d'exceptions soit une mauvaise pratique. Il a déjà été jugé mauvais car il est RELATIVEMENT cher pour les autres opérateurs, mais négligeable dans une application connectée. Skeet a mesuré 40-118 exceptions par ms en fonction de la profondeur de la pile depuis 2006. stackoverflow.com/a/891230/852208 . De plus, sans test, il est possible que ce code soit en fait plus lent avec sa casse moyenne vérifiant la moitié de toutes les colonnes (bien que toujours trivial dans une application connectée à db). Je modifierais cette réponse pour n'inclure que le paragraphe du milieu car les deux autres sont des opinions.
b_levitt

3
@b_levitt ce n'est pas discutable, c'est du code de merde et vous ne devriez pas compter sur des exceptions pour le flux de contrôle
Chad Grant

Comme les deux phrases que j'ai signalées, c'est encore une autre opinion qui n'est étayée par aucune justification au-delà des performances dans une application purement informatique. Je vous mets au défi de configurer votre débogueur pour qu'il casse toutes les exceptions et de désactiver uniquement mon code et vous verrez à quel point même le framework et les autres bibliothèques le font déjà.Le problème avec votre conseil est qu'il pousse les développeurs à renvoyer les codes les plus d'accord sont un modèle inférieur: stackoverflow.com/questions/99683/… . Une telle méthodologie échoue au test du «puits de succès».
b_levitt

Du point de vue du code, votre réponse est valide. Mais votre opinion essayant de la pondérer comme une réponse supérieure à la réponse avec le try / catch (qui gère également les alias) est hors de propos ici.
b_levitt

66

Il est préférable d'utiliser cette fonction booléenne:

r.GetSchemaTable().Columns.Contains(field)

Un appel - sans exception. Cela pourrait lever des exceptions en interne, mais je ne pense pas.

REMARQUE: dans les commentaires ci-dessous, nous avons compris cela ... le code correct est en fait le suivant:

public static bool HasColumn(DbDataReader Reader, string ColumnName) { 
    foreach (DataRow row in Reader.GetSchemaTable().Rows) { 
        if (row["ColumnName"].ToString() == ColumnName) 
            return true; 
    } //Still here? Column not found. 
    return false; 
}

5
@Jasmine: J'ai parlé trop tôt! Votre code recherche une colonne dans la table de schéma, pas votre jeu de résultats. Vous devez comparer "champ" (en supposant que "champ" est le nom de la colonne) à la valeur du champ "ColumnName" de chaque ligne. Cassez quand vous le trouvez, retournez faux si vous ne le faites pas.
Steve J

4
@Steve J: Quand le jeu de résultats N'aurait-il PAS une colonne dans le GetSchemaTable?
Bless Yahu

1
Pour toute autre personne confuse, cela ne fonctionne pas. Voir la réponse ci-dessous concernant la récupération de la ligne ColumnName de la table de schéma et son utilisation.
Jason Jackson

3
Oui, cela ne fonctionne pas. Qui l'a voté autant de fois ??? Cela m'aurait fait gagner beaucoup de temps de débogage plus tard si cette réponse n'était pas là!
c00000fd le

1
@ Jasmin, ils travaillent tous les deux? Pas vraiment. Veuillez retirer la première partie de votre réponse. Je l'aurais fait moi-même, mais pour ton dernier commentaire!
nawfal

33

Je pense que votre meilleur pari est d'appeler GetOrdinal ("columnName") sur votre DataReader à l'avant, et d'attraper une IndexOutOfRangeException au cas où la colonne n'est pas présente.

En fait, faisons une méthode d'extension:

public static bool HasColumn(this IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}

Éditer

Ok, ce post commence à recueillir quelques votes négatifs récemment, et je ne peux pas le supprimer car c'est la réponse acceptée, donc je vais le mettre à jour et (j'espère) essayer de justifier l'utilisation de la gestion des exceptions comme contrôler le flux.

L'autre moyen d'y parvenir, tel que publié par Chad Grant , est de parcourir chaque champ dans le DataReader et de faire une comparaison insensible à la casse pour le nom de champ que vous recherchez. Cela fonctionnera très bien et, à vrai dire, fonctionnera probablement mieux que ma méthode ci-dessus. Certes, je n'utiliserais jamais la méthode ci-dessus dans une boucle où performace était un problème.

Je peux penser à une situation dans laquelle la méthode try / GetOrdinal / catch fonctionnera là où la boucle ne fonctionne pas. C'est, cependant, une situation complètement hypothétique en ce moment donc c'est une justification très fragile. Quoi qu'il en soit, supportez-moi et voyez ce que vous en pensez.

Imaginez une base de données qui vous permettait de "créer un alias" dans une table. Imaginez que je puisse définir une table avec une colonne appelée "EmployeeName" mais aussi lui donner un alias de "EmpName", et faire une sélection pour l'un ou l'autre nom retournerait les données dans cette colonne. Jusqu'à présent avec moi?

Imaginez maintenant qu'il existe un fournisseur ADO.NET pour cette base de données, et qu'ils ont codé une implémentation IDataReader pour celle-ci qui prend en compte les alias de colonne.

Maintenant, dr.GetName(i)(comme utilisé dans la réponse de Chad) ne peut renvoyer qu'une seule chaîne, il ne doit donc renvoyer qu'un seul des "alias" sur une colonne. cependant,GetOrdinal("EmpName") pourrait utiliser l'implémentation interne des champs de ce fournisseur pour vérifier l'alias de chaque colonne pour le nom que vous recherchez.

Dans cette hypothétique situation de "colonnes aliasées", la méthode try / GetOrdinal / catch serait le seul moyen d'être sûr de vérifier chaque variation du nom d'une colonne dans l'ensemble de résultats.

Fragile? Sûr. Mais ça vaut le coup. Honnêtement, je préfère de loin une méthode HasColumn "officielle" sur IDataRecord.


15
utiliser des exceptions pour la logique de contrôle? non non non
Chad Grant

28
Il y a une petite chose que tout le monde oublie lorsque j'ai posté cette question à l'origine ... J'ai posé la question le 08/12/08 et Matt a posté sa réponse le 17/12/08. Tout le monde a pu puiser une exception pour la logique de contrôle, mais n'a pas fourni de solution alternative solide avant le 5/1/09. C'est pourquoi il a été initialement marqué comme la réponse. J'utilise toujours cette solution aujourd'hui.
Michael Kniskern

19
Cela aura un impact sur les performances uniquement si la colonne n'était pas là. Les autres méthodes décrites auront un impact sur les performances et un impact sur les performances plus important à chaque fois. Bien qu'il soit généralement mauvais de ne pas utiliser la gestion des exceptions pour le flux de contrôle, cette solution ne doit pas être exclue sans d'abord se demander si elle fonctionne dans votre cas.
Nick Harrison

5
+1. Je suis d'accord avec "Ne pas utiliser d'exception pour la logique de contrôle" comme règle de conception large. Cela ne signifie pas "l'éviter à tout prix". La réponse est une solution de contournement très bien documentée, et comme @Nick le dit, le hit de performance (le cas échéant ..) se produit uniquement lorsque la colonne n'existe pas.
Larry

2
L'utilisation d'exceptions comme logique de contrôle rend également le débogage plus lourd selon mon expérience. Vous devez décocher «Thrown» sur «Common Language Runtime Exceptions» et puis lorsque vous obtenez une vraie exception, elle peut se casser quelque part dans un gestionnaire et non sur la ligne qui a le problème.
cedd

30

En une seule ligne, utilisez ceci après votre récupération DataReader:

var fieldNames = Enumerable.Range(0, dr.FieldCount).Select(i => dr.GetName(i)).ToArray();

Ensuite,

if (fieldNames.Contains("myField"))
{
    var myFieldValue = dr["myField"];
    ...

Éditer

One-liner beaucoup plus efficace qui ne nécessite pas de charger le schéma:

var exists = Enumerable.Range(0, dr.FieldCount).Any(i => string.Equals(dr.GetName(i), fieldName, StringComparison.OrdinalIgnoreCase));

Vous énumérez les noms de champ plusieurs fois / allouez un autre tableau à analyser avec un contient, ce serait beaucoup moins performant dans un code à fort trafic.
Chad Grant

@ChadGrant bien sûr, c'est pourquoi la doublure Linq one est beaucoup plus efficace car elle n'effectue qu'une seule itération.
Larry

18

Voici un exemple de travail pour l'idée de Jasmin:

var cols = r.GetSchemaTable().Rows.Cast<DataRow>().Select
    (row => row["ColumnName"] as string).ToList(); 

if (cols.Contains("the column name"))
{

}

1
Seulement si vous en faites un essai / capture
Donald.Record

Vous pouvez simplifier cette idée avec: reader.GetSchemaTable (). Columns.Contains ("myFiled")
Lev Z

l'utilisation de GetSchemaTable () est excessive (en termes d'allocation) pour simplement trouver un nom de colonne. Consultez la source github.com/microsoft/referencesource/blob/…
Chad Grant

12

cela fonctionne pour moi:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");

l'utilisation de GetSchemaTable () est excessive (en termes d'allocation) pour simplement trouver un nom de colonne. Et il n'est pas implémenté dans toutes les versions de dotnet core. Consultez la source github.com/microsoft/referencesource/blob/…
Chad Grant


8

Si vous avez lu la question, Michael a posé des questions sur DataReader, pas sur les gens de DataRecord. Obtenez vos objets à droite.

Utilisant un r.GetSchemaTable().Columns.Contains(field) sur un DataRecord fonctionne, mais il renvoie des colonnes BS (voir capture d'écran ci-dessous.)

Pour voir si une colonne de données existe ET contient des données dans un DataReader, utilisez les extensions suivantes:

public static class DataReaderExtensions
{
    /// <summary>
    /// Checks if a column's value is DBNull
    /// </summary>
    /// <param name="dataReader">The data reader</param>
    /// <param name="columnName">The column name</param>
    /// <returns>A bool indicating if the column's value is DBNull</returns>
    public static bool IsDBNull(this IDataReader dataReader, string columnName)
    {
        return dataReader[columnName] == DBNull.Value;
    }

    /// <summary>
    /// Checks if a column exists in a data reader
    /// </summary>
    /// <param name="dataReader">The data reader</param>
    /// <param name="columnName">The column name</param>
    /// <returns>A bool indicating the column exists</returns>
    public static bool ContainsColumn(this IDataReader dataReader, string columnName)
    {
        /// See: http://stackoverflow.com/questions/373230/check-for-column-name-in-a-sqldatareader-object/7248381#7248381
        try
        {
            return dataReader.GetOrdinal(columnName) >= 0;
        }
        catch (IndexOutOfRangeException)
        {
            return false;
        }
    }
}

Usage:

    public static bool CanCreate(SqlDataReader dataReader)
    {
        return dataReader.ContainsColumn("RoleTemplateId") 
            && !dataReader.IsDBNull("RoleTemplateId");
    }

L'appel r.GetSchemaTable().Columnssur un DataReader renvoie des colonnes BS:

Appel de GetSchemeTable dans un DataReader


voir les commentaires sous la réponse de Matts
nawfal

Que voulez-vous dire par DataRecord fonctionne , mais il renvoie des colonnes BS ? Vous voulez dire qu'il fonctionne (et donne des résultats erronés)?
nawfal

2
"Obtenez vos objets à droite." - mais IDataReadermet en œuvre IDataRecord. Ce sont des interfaces différentes du même objet - tout comme ICollection<T>et IEnumerable<T>sont des interfaces différentes de List<T>. IDataReaderpermet de passer à l'enregistrement suivant, tout en IDataRecordpermettant la lecture de l'enregistrement en cours. Les méthodes utilisées dans cette réponse proviennent toutes de l' IDataRecordinterface. Voir stackoverflow.com/a/1357743/221708 pour une explication de la raison pour laquelle déclarer le paramètre comme IDataRecordpréférable.
Daniel Schilling

Upvote pour montrer pourquoi r.GetSchemaTable().Columnsest une réponse absolument fausse à cette question.
Daniel Schilling

GetName () est hérité de l'interface IDataRecord dans IDataReader. Cibler l'interface de base est le bon code.
Chad Grant

7

J'ai écrit pour les utilisateurs de Visual Basic:

Protected Function HasColumnAndValue(ByRef reader As IDataReader, ByVal columnName As String) As Boolean
    For i As Integer = 0 To reader.FieldCount - 1
        If reader.GetName(i).Equals(columnName) Then
            Return Not IsDBNull(reader(columnName))
        End If
    Next

    Return False
End Function

Je pense que c'est plus puissant et l'utilisation est:

If HasColumnAndValue(reader, "ID_USER") Then
    Me.UserID = reader.GetDecimal(reader.GetOrdinal("ID_USER")).ToString()
End If

4

Voici une version linq à une ligne de la réponse acceptée:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")

comparaison sensible à la casse ... pourquoi?
Chad Grant

4

Voici la solution de Jasmine en une seule ligne ... (une de plus, si simple!):

reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;

l'utilisation de GetSchemaTable () est excessive (en termes d'allocation) pour simplement trouver un nom de colonne. Consultez la source github.com/microsoft/referencesource/blob/…
Chad Grant

@ChadGrant Possible. Je suppose que l'on doit choisir judicieusement en fonction du contexte et de la fréquence, il est nécessaire d'utiliser cela ...
spaark

3
Hashtable ht = new Hashtable();
    Hashtable CreateColumnHash(SqlDataReader dr)
    {
        ht = new Hashtable();
        for (int i = 0; i < dr.FieldCount; i++)
        {
            ht.Add(dr.GetName(i), dr.GetName(i));
        }
        return ht;
    }

    bool ValidateColumn(string ColumnName)
    {
        return ht.Contains(ColumnName);
    }

3

TLDR:

Beaucoup de réponses avec des affirmations sur les performances et les mauvaises pratiques, donc je clarifie cela ici.

La route d'exception est plus rapide pour un nombre plus élevé de colonnes retournées, la route de boucle est plus rapide pour un nombre inférieur de colonnes et le point de croisement est d'environ 11 colonnes. Faites défiler vers le bas pour voir un graphique et un code de test.

Réponse complète:

Le code de certaines des meilleures réponses fonctionne, mais il y a un débat sous-jacent ici pour la "meilleure" réponse basée sur l'acceptation de la gestion des exceptions dans la logique et ses performances associées.

Pour clarifier cela, je ne pense pas qu'il y ait beaucoup d'indications concernant les exceptions de capture. Microsoft a quelques conseils concernant les exceptions de LANCEMENT. Là, ils déclarent:

N'UTILISEZ PAS d'exceptions pour le flux normal de contrôle, si possible.

La première note est la clémence de «si possible». Plus important encore, la description donne ce contexte:

framework designers should design APIs so users can write code that does not throw exceptions

Cela signifie que si vous écrivez une API qui pourrait être utilisée par quelqu'un d'autre, donnez-lui la possibilité de naviguer dans une exception sans essayer / attraper. Par exemple, fournissez un TryParse avec votre méthode d'analyse d'exception. Cela ne dit nulle part que vous ne devriez pas attraper une exception.

De plus, comme le souligne un autre utilisateur, les captures ont toujours autorisé le filtrage par type et ont récemment permis un filtrage supplémentaire via la clause when . Cela semble être un gaspillage de fonctionnalités linguistiques si nous ne sommes pas censés les utiliser.

On peut dire qu'il y a QUELQUE coût pour une exception levée, et que le coût PEUT avoir un impact sur les performances dans une boucle lourde. Cependant, on peut également dire que le coût d'une exception va être négligeable dans une "application connectée". Le coût réel a été étudié il y a plus d'une décennie: https://stackoverflow.com/a/891230/852208 En d'autres termes, le coût d'une connexion et d'une requête d'une base de données est susceptible de éclipser celui d'une exception levée.

Tout cela mis à part, je voulais déterminer quelle méthode est vraiment la plus rapide. Comme prévu, il n'y a pas de réponse concrète.

Tout code qui boucle sur les colonnes devient plus lent à mesure que le nombre de colonnes existe. On peut également dire que tout code qui repose sur des exceptions ralentira en fonction de la vitesse à laquelle la requête ne sera pas trouvée.

En prenant les réponses de Chad Grant et de Matt Hamilton, j'ai exécuté les deux méthodes avec jusqu'à 20 colonnes et jusqu'à un taux d'erreur de 50% (l'OP a indiqué qu'il utilisait ces deux tests entre des procs différents, donc j'ai supposé aussi peu que deux) .

Voici les résultats, tracés avec LinqPad: Résultats - La série 1 est une boucle, 2 est une exception

Les zigzags ici sont des taux d'erreur (colonne non trouvée) dans chaque nombre de colonnes.

Sur des ensembles de résultats plus étroits, le bouclage est un bon choix. Cependant, la méthode GetOrdinal / Exception n'est pas aussi sensible au nombre de colonnes et commence à surpasser la méthode de bouclage autour de 11 colonnes.

Cela dit, je n'ai pas vraiment de préférence en termes de performances, car 11 colonnes semblent raisonnables, car le nombre moyen de colonnes renvoyées sur une application entière. Dans les deux cas, nous parlons ici de fractions de milliseconde.

Cependant, du point de vue de la simplicité du code et de la prise en charge des alias, j'irais probablement avec la route GetOrdinal.

Voici le test sous forme linqpad. N'hésitez pas à republier avec votre propre méthode:

void Main()
{
    var loopResults = new List<Results>();
    var exceptionResults = new List<Results>();
    var totalRuns = 10000;
    for (var colCount = 1; colCount < 20; colCount++)
    {
        using (var conn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=master;Integrated Security=True;"))
        {
            conn.Open();

            //create a dummy table where we can control the total columns
            var columns = String.Join(",",
                (new int[colCount]).Select((item, i) => $"'{i}' as col{i}")
            );
            var sql = $"select {columns} into #dummyTable";
            var cmd = new SqlCommand(sql,conn);
            cmd.ExecuteNonQuery();

            var cmd2 = new SqlCommand("select * from #dummyTable", conn);

            var reader = cmd2.ExecuteReader();
            reader.Read();

            Func<Func<IDataRecord, String, Boolean>, List<Results>> test = funcToTest =>
            {
                var results = new List<Results>();
                Random r = new Random();
                for (var faultRate = 0.1; faultRate <= 0.5; faultRate += 0.1)
                {
                    Stopwatch stopwatch = new Stopwatch();
                    stopwatch.Start();
                    var faultCount=0;
                    for (var testRun = 0; testRun < totalRuns; testRun++)
                    {
                        if (r.NextDouble() <= faultRate)
                        {
                            faultCount++;
                            if(funcToTest(reader, "colDNE"))
                                throw new ApplicationException("Should have thrown false");
                        }
                        else
                        {
                            for (var col = 0; col < colCount; col++)
                            {
                                if(!funcToTest(reader, $"col{col}"))
                                    throw new ApplicationException("Should have thrown true");
                            }
                        }
                    }
                    stopwatch.Stop();
                    results.Add(new UserQuery.Results{
                        ColumnCount = colCount, 
                        TargetNotFoundRate = faultRate,
                        NotFoundRate = faultCount * 1.0f / totalRuns, 
                        TotalTime=stopwatch.Elapsed
                    });
                }
                return results;
            };
            loopResults.AddRange(test(HasColumnLoop));

            exceptionResults.AddRange(test(HasColumnException));

        }

    }
    "Loop".Dump();
    loopResults.Dump();

    "Exception".Dump();
    exceptionResults.Dump();

    var combinedResults = loopResults.Join(exceptionResults,l => l.ResultKey, e=> e.ResultKey, (l, e) => new{ResultKey = l.ResultKey, LoopResult=l.TotalTime, ExceptionResult=e.TotalTime});
    combinedResults.Dump();
    combinedResults
        .Chart(r => r.ResultKey, r => r.LoopResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .AddYSeries(r => r.ExceptionResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .Dump();
}
public static bool HasColumnLoop(IDataRecord dr, string columnName)
{
    for (int i = 0; i < dr.FieldCount; i++)
    {
        if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
            return true;
    }
    return false;
}

public static bool HasColumnException(IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}

public class Results
{
    public double NotFoundRate { get; set; }
    public double TargetNotFoundRate { get; set; }
    public int ColumnCount { get; set; }
    public double ResultKey {get => ColumnCount + TargetNotFoundRate;}
    public TimeSpan TotalTime { get; set; }


}

1
Vous avez clairement une sorte d'obsession bizarre avec les exceptions. Une meilleure approche consisterait simplement à mettre en cache l'emplacement de la colonne dans une recherche statique de performances et à utiliser la recherche entière
Chad Grant

un autre problème avec l'utilisation des exceptions comme flux de contrôle est qu'elles apparaissent dans le profileur comme # d'exceptions levées lorsque dans votre code suggéré elles sont intentionnelles ... pas des exceptions. Sans oublier de paramétrer votre débogueur pour qu'il se casse les exceptions levées. Signalement essentiellement des erreurs qui ne sont pas des erreurs. Tu ne devrais pas faire ça.
Chad Grant

1
Il existe également des compteurs pour finalement / seconde et des filtres / seconde. Est-ce que c'est mauvais aussi? Je dirais que c'est une mise en garde possible - la première vraie que vous avez fournie. Les comptoirs ne sont que des informations. Ils ne signifient rien sauf s'ils correspondent à un problème de performances - et dans ce cas, j'ai déjà montré le point où les exceptions ont de MEILLEURES performances. J'ai également indiqué que le cadre et les bibliothèques lèvent déjà beaucoup d'exceptions. J'ai une instance de visual studio qui lance 60 ex / s en ce moment. Les exceptions ne sont pas des erreurs, sauf si elles ne sont pas détectées.
b_levitt

Grande analyse. J'ai utilisé ses résultats dans ma nouvelle réponse.
yazanpro

1

Ce code corrige les problèmes que Levitikon avait avec leur code: (adapté de: [1]: http://msdn.microsoft.com/en-us/library/system.data.datatablereader.getschematable.aspx )

public List<string> GetColumnNames(SqlDataReader r)
{
    List<string> ColumnNames = new List<string>();
    DataTable schemaTable = r.GetSchemaTable();
    DataRow row = schemaTable.Rows[0];
    foreach (DataColumn col in schemaTable.Columns)
    {
        if (col.ColumnName == "ColumnName") 
        { 
            ColumnNames.Add(row[col.Ordinal].ToString()); 
            break; 
        }
    }
    return ColumnNames;
}

La raison pour laquelle vous obtenez tous ces noms de colonne inutiles et non le nom de la colonne de votre table ... est parce que vous obtenez le nom de la colonne de schéma (c'est-à-dire les noms de colonne de la table de schéma)

REMARQUE: cela semble renvoyer uniquement le nom de la première colonne ...

EDIT: code corrigé qui renvoie le nom de toutes les colonnes, mais vous ne pouvez pas utiliser un SqlDataReader pour le faire

public List<string> ExecuteColumnNamesReader(string command, List<SqlParameter> Params)
{
    List<string> ColumnNames = new List<string>();
    SqlDataAdapter da = new SqlDataAdapter();
    string connection = ""; // your sql connection string
    SqlCommand sqlComm = new SqlCommand(command, connection);
    foreach (SqlParameter p in Params) { sqlComm.Parameters.Add(p); }
    da.SelectCommand = sqlComm;
    DataTable dt = new DataTable();
    da.Fill(dt);
    DataRow row = dt.Rows[0];
    for (int ordinal = 0; ordinal < dt.Columns.Count; ordinal++)
    {
        string column_name = dt.Columns[ordinal].ColumnName;
        ColumnNames.Add(column_name);
    }
    return ColumnNames; // you can then call .Contains("name") on the returned collection
}

Ou en une seule ligne return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList();:)
nawfal

l'utilisation de GetSchemaTable () est excessive (en termes d'allocation) pour simplement trouver un nom de colonne. Consultez la source github.com/microsoft/referencesource/blob/…
Chad Grant

1

Pour garder votre code robuste et propre, utilisez une seule fonction d'extension, comme ceci:

    Public Module Extensions

        <Extension()>
        Public Function HasColumn(r As SqlDataReader, columnName As String) As Boolean

            Return If(String.IsNullOrEmpty(columnName) OrElse r.FieldCount = 0, False, Enumerable.Range(0, r.FieldCount).Select(Function(i) r.GetName(i)).Contains(columnName, StringComparer.OrdinalIgnoreCase))

        End Function

    End Module

0

Je n'ai pas non plus pu GetSchemaTabletravailler, jusqu'à ce que je trouve cette voie .

Fondamentalement, je fais ceci:

Dim myView As DataView = dr.GetSchemaTable().DefaultView
myView.RowFilter = "ColumnName = 'ColumnToBeChecked'"

If myView.Count > 0 AndAlso dr.GetOrdinal("ColumnToBeChecked") <> -1 Then
  obj.ColumnToBeChecked = ColumnFromDb(dr, "ColumnToBeChecked")
End If

0
public static bool DataViewColumnExists(DataView dv, string columnName)
{
    return DataTableColumnExists(dv.Table, columnName);
}

public static bool DataTableColumnExists(DataTable dt, string columnName)
{
    string DebugTrace = "Utils::DataTableColumnExists(" + dt.ToString() + ")";
    try
    {
        return dt.Columns.Contains(columnName);
    }
    catch (Exception ex)
    {
        throw new MyExceptionHandler(ex, DebugTrace);
    }
}

Columns.Contains est insensible à la casse.


Contains () ne lève pas d'exceptions, ce code est inutile. Vous ne captureriez que des exceptions de pointeur nul.
Chad Grant

0

Dans votre situation particulière (toutes les procédures ont les mêmes colonnes sauf 1 qui a 1 colonne supplémentaire), il sera meilleur et plus rapide de vérifier le lecteur. Propriété FieldCount pour les distinguer.

const int NormalColCount=.....
if(reader.FieldCount > NormalColCount)
{
// Do something special
}

Je sais que c'est un ancien poste mais j'ai décidé de répondre pour aider les autres dans la même situation. vous pouvez également (pour des raisons de performances) mélanger cette solution avec la solution itérative de la solution.


Veuillez nommer la solution à laquelle vous faites référence. Quelles sont les deux solutions à mélanger?
Pablo Jomer

0

Ma classe d'accès aux données doit être rétrocompatible, donc j'essaie peut-être d'accéder à une colonne dans une version où elle n'existe pas encore dans la base de données. Nous avons renvoyé des ensembles de données assez volumineux, donc je ne suis pas un grand fan d'une méthode d'extension qui doit itérer la collection de colonnes DataReader pour chaque propriété.

J'ai une classe utilitaire qui crée une liste privée de colonnes, puis possède une méthode générique qui tente de résoudre une valeur basée sur un nom de colonne et un type de paramètre de sortie.

private List<string> _lstString;

public void GetValueByParameter<T>(IDataReader dr, string parameterName, out T returnValue)
{
    returnValue = default(T);

    if (!_lstString.Contains(parameterName))
    {
        Logger.Instance.LogVerbose(this, "missing parameter: " + parameterName);
        return;
    }

    try
    {
        if (dr[parameterName] != null && [parameterName] != DBNull.Value)
            returnValue = (T)dr[parameterName];
    }
    catch (Exception ex)
    {
        Logger.Instance.LogException(this, ex);
    }
}

/// <summary>
/// Reset the global list of columns to reflect the fields in the IDataReader
/// </summary>
/// <param name="dr">The IDataReader being acted upon</param>
/// <param name="NextResult">Advances IDataReader to next result</param>
public void ResetSchemaTable(IDataReader dr, bool nextResult)
{
    if (nextResult)
        dr.NextResult();

    _lstString = new List<string>();

    using (DataTable dataTableSchema = dr.GetSchemaTable())
    {
        if (dataTableSchema != null)
        {
            foreach (DataRow row in dataTableSchema.Rows)
            {
                _lstString.Add(row[dataTableSchema.Columns["ColumnName"]].ToString());
            }
        }
    }
}

Ensuite, je peux simplement appeler mon code comme ça

using (var dr = ExecuteReader(databaseCommand))
{
    int? outInt;
    string outString;

    Utility.ResetSchemaTable(dr, false);        
    while (dr.Read())
    {
        Utility.GetValueByParameter(dr, "SomeColumn", out outInt);
        if (outInt.HasValue) myIntField = outInt.Value;
    }

    Utility.ResetSchemaTable(dr, true);
    while (dr.Read())
    {
        Utility.GetValueByParameter(dr, "AnotherColumn", out outString);
        if (!string.IsNullOrEmpty(outString)) myIntField = outString;
    }
}

0

La clé de tout le problème est ici :

if (-1 == index) {
    throw ADP.IndexOutOfRange(fieldName);
}

Si les trois lignes référencées (actuellement les lignes 72, 73 et 74) sont supprimées, vous pouvez facilement vérifier -1afin de déterminer si la colonne n'existe pas.

La seule façon de contourner ce problème tout en garantissant des performances natives est d'utiliser une Reflectionimplémentation basée, comme suit:

Utilisations:

using System;
using System.Data;
using System.Reflection;
using System.Data.SqlClient;
using System.Linq;
using System.Web.Compilation; // I'm not sure what the .NET Core equivalent to BuildManager.cs

La méthode d'extension basée sur la réflexion:

/// Gets the column ordinal, given the name of the column.
/// </summary>
/// <param name="reader"></param>
/// <param name="name">The name of the column.</param>
/// <returns> The zero-based column ordinal. -1 if the column does not exist.</returns>
public static int GetOrdinalSoft(this SqlDataReader reader, string name)
{
    try
    {
        // Note that "Statistics" will not be accounted for in this implemenation
        // If you have SqlConnection.StatisticsEnabled set to true (the default is false), you probably don't want to use this method
        // All of the following logic is inspired by the actual implementation of the framework:
        // https://referencesource.microsoft.com/#System.Data/fx/src/data/System/Data/SqlClient/SqlDataReader.cs,d66096b6f57cac74
        if (name == null)
            throw new ArgumentNullException("fieldName");

        Type sqlDataReaderType = typeof(SqlDataReader);
        object fieldNameLookup = sqlDataReaderType.GetField("_fieldNameLookup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(reader);
        Type fieldNameLookupType;
        if (fieldNameLookup == null)
        {
            MethodInfo checkMetaDataIsReady = sqlDataReaderType.GetRuntimeMethods().First(x => x.Name == "CheckMetaDataIsReady" && x.GetParameters().Length == 0);
            checkMetaDataIsReady.Invoke(reader, null);
            fieldNameLookupType = BuildManager.GetType("System.Data.ProviderBase.FieldNameLookup", true, false);
            ConstructorInfo ctor = fieldNameLookupType.GetConstructor(new[] { typeof(SqlDataReader), typeof(int) });
            fieldNameLookup = ctor.Invoke(new object[] { reader, sqlDataReaderType.GetField("_defaultLCID", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(reader) });
        }
        else
            fieldNameLookupType = fieldNameLookup.GetType();

        MethodInfo indexOf = fieldNameLookupType.GetMethod("IndexOf", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string) }, null);

        return (int)indexOf.Invoke(fieldNameLookup, new object[] { name });
    }
    catch
    {
        // .NET Implemenation might have changed, revert back to the classic solution.
        if (reader.FieldCount > 11) // Performance observation by b_levitt
        {
            try
            {
                return reader.GetOrdinal(name);
            }
            catch
            {
                return -1;
            }
        }
        else
        {
            var exists = Enumerable.Range(0, reader.FieldCount).Any(i => string.Equals(reader.GetName(i), name, StringComparison.OrdinalIgnoreCase));
            if (exists)
                return reader.GetOrdinal(name);
            else
                return -1;
        }
    }
}


-1

Que diriez-vous

if (dr.GetSchemaTable().Columns.Contains("accounttype"))
   do something
else
   do something

Ce ne serait probablement pas aussi efficace dans une boucle


Voir la réponse de Levitikon pour voir le genre de chose qui dr.GetSchemaTable().Columnscontient - ce n'est pas ce que vous recherchez.
Daniel Schilling

-1

Bien qu'il n'y ait pas de méthode exposée publiquement, une méthode existe dans la classe interne System.Data.ProviderBase.FieldNameLookupquiSqlDataReader s'appuie.

Pour y accéder et obtenir des performances natives, vous devez utiliser ILGenerator pour créer une méthode au moment de l'exécution. Le code suivant vous donnera un accès direct à int IndexOf(string fieldName)la System.Data.ProviderBase.FieldNameLookupclasse ainsi que la tenue de la comptabilité qui SqlDataReader.GetOrdinal()fait qu'il n'y a aucun effet secondaire. Le code généré reflète l'existant, SqlDataReader.GetOrdinal()sauf qu'il appelle à la FieldNameLookup.IndexOf()place de FieldNameLookup.GetOrdinal(). La GetOrdinal()méthode appelle la IndexOf()fonction et lève une exception si -1est renvoyée, nous contournons donc ce comportement.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using System.Reflection.Emit;

public static class SqlDataReaderExtensions {

   private delegate int IndexOfDelegate(SqlDataReader reader, string name);
   private static IndexOfDelegate IndexOf;

   public static int GetColumnIndex(this SqlDataReader reader, string name) {
      return name == null ? -1 : IndexOf(reader, name);
   }

   public static bool ContainsColumn(this SqlDataReader reader, string name) {
      return name != null && IndexOf(reader, name) >= 0;
   }

   static SqlDataReaderExtensions() {
      Type typeSqlDataReader = typeof(SqlDataReader);
      Type typeSqlStatistics = typeSqlDataReader.Assembly.GetType("System.Data.SqlClient.SqlStatistics", true);
      Type typeFieldNameLookup = typeSqlDataReader.Assembly.GetType("System.Data.ProviderBase.FieldNameLookup", true);

      BindingFlags staticflags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Static;
      BindingFlags instflags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance;

      DynamicMethod dynmethod = new DynamicMethod("SqlDataReader_IndexOf", typeof(int), new Type[2]{ typeSqlDataReader, typeof(string) }, true);
      ILGenerator gen = dynmethod.GetILGenerator();
      gen.DeclareLocal(typeSqlStatistics);
      gen.DeclareLocal(typeof(int));

      // SqlStatistics statistics = (SqlStatistics) null;
      gen.Emit(OpCodes.Ldnull);
      gen.Emit(OpCodes.Stloc_0);
      // try {
      gen.BeginExceptionBlock();
      //    statistics = SqlStatistics.StartTimer(this.Statistics);
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Call, typeSqlDataReader.GetProperty("Statistics", instflags | BindingFlags.GetProperty, null, typeSqlStatistics, Type.EmptyTypes, null).GetMethod);
      gen.Emit(OpCodes.Call, typeSqlStatistics.GetMethod("StartTimer", staticflags | BindingFlags.InvokeMethod, null, new Type[] { typeSqlStatistics }, null));
      gen.Emit(OpCodes.Stloc_0); //statistics
      //    if(this._fieldNameLookup == null) {
      Label branchTarget = gen.DefineLabel();
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldfld, typeSqlDataReader.GetField("_fieldNameLookup", instflags | BindingFlags.GetField));
      gen.Emit(OpCodes.Brtrue_S, branchTarget);
      //       this.CheckMetaDataIsReady();
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Call, typeSqlDataReader.GetMethod("CheckMetaDataIsReady", instflags | BindingFlags.InvokeMethod, null, Type.EmptyTypes, null));
      //       this._fieldNameLookup = new FieldNameLookup((IDataRecord)this, this._defaultLCID);
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldfld, typeSqlDataReader.GetField("_defaultLCID", instflags | BindingFlags.GetField));
      gen.Emit(OpCodes.Newobj, typeFieldNameLookup.GetConstructor(instflags, null, new Type[] { typeof(IDataReader), typeof(int) }, null));
      gen.Emit(OpCodes.Stfld, typeSqlDataReader.GetField("_fieldNameLookup", instflags | BindingFlags.SetField));
      //    }
      gen.MarkLabel(branchTarget);
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldfld, typeSqlDataReader.GetField("_fieldNameLookup", instflags | BindingFlags.GetField));
      gen.Emit(OpCodes.Ldarg_1); //name
      gen.Emit(OpCodes.Call, typeFieldNameLookup.GetMethod("IndexOf", instflags | BindingFlags.InvokeMethod, null, new Type[] { typeof(string) }, null));
      gen.Emit(OpCodes.Stloc_1); //int output
      Label leaveProtectedRegion = gen.DefineLabel();
      gen.Emit(OpCodes.Leave_S, leaveProtectedRegion);
      // } finally {
      gen.BeginFaultBlock();
      //    SqlStatistics.StopTimer(statistics);
      gen.Emit(OpCodes.Ldloc_0); //statistics
      gen.Emit(OpCodes.Call, typeSqlStatistics.GetMethod("StopTimer", staticflags | BindingFlags.InvokeMethod, null, new Type[] { typeSqlStatistics }, null));
      // }
      gen.EndExceptionBlock();
      gen.MarkLabel(leaveProtectedRegion);
      gen.Emit(OpCodes.Ldloc_1);
      gen.Emit(OpCodes.Ret);

      IndexOf = (IndexOfDelegate)dynmethod.CreateDelegate(typeof(IndexOfDelegate));
   }

}

1
Le code interne fait presque exactement la même chose que ma réponse, sans avoir besoin de cette étrange réflexion / délégué. Il met en cache la recherche par instance d'objet, ce qui ne serait pas bénéfique car dans le monde réel, vous voulez mettre en cache les ordinaux la première fois que la requête est exécutée et utiliser ce cache pendant la durée de vie de l'application, pas créer un nouveau cache sur chaque requête.
Chad Grant

-1

ce travail pour moi

public static class DataRecordExtensions
{
        public static bool HasColumn(IDataReader dataReader, string columnName)
        {
            dataReader.GetSchemaTable().DefaultView.RowFilter = $"ColumnName= '{columnName}'";
            return (dataReader.GetSchemaTable().DefaultView.Count > 0);
        }
}

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.