web-dev-qa-db-fra.com

Vérifier le nom de la colonne dans un objet SqlDataReader

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

Mon application est écrite en C #.

201
Michael Kniskern

L'utilisation de Exceptions pour la logique de contrôle, comme dans d'autres réponses, est considérée comme une mauvaise pratique et entraîne des coûts de performance.

La boucle dans les champs peut avoir un impact négatif sur les performances si vous l'utilisez beaucoup et envisagez de mettre en cache les résultats.

La manière la plus appropriée de faire ceci est:

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;
    }
}
319
Chad Grant

C'est beaucoup mieux d'utiliser cette fonction booléenne:

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

Un appel - sans exception. Il pourrait jeter des exceptions en interne, mais je ne le pense pas.

NOTE: Dans les commentaires ci-dessous, nous avons compris ceci ... 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; 
}
64
Jasmine

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

En fait, créons une méthode d'extension:

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

Modifier

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

L’autre moyen d’y parvenir, comme posté par Chad Grant , consiste à parcourir en boucle chaque champ du DataReader et à effectuer une comparaison insensible à la casse du nom du champ que vous recherchez. Cela fonctionnera vraiment bien et, en toute honnêteté, il fonctionnera probablement mieux que ma méthode ci-dessus. Bien sûr, 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 où la boucle ne fonctionne pas. Il s’agit toutefois d’une situation tout à fait hypothétique à l’heure actuelle, c’est donc une justification très fragile. Peu importe, supporte-moi et vois ce que tu penses.

Imaginez une base de données qui vous permet d’aliaser des colonnes dans une table. Imaginez que je puisse définir une table avec une colonne appelée "EmployeeName", mais aussi lui donner un alias "EmpName", et sélectionner un nom ou l'autre renverrait les données de cette colonne. Avec moi jusqu'à présent?

Imaginons maintenant qu’il existe un fournisseur ADO.NET pour cette base de données et qu’il a codé pour son implémentation une implémentation IDataReader qui prend en compte les alias de colonnes.

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

Dans cette situation hypothétique de "colonnes avec alias", la méthode try/GetOrdinal/catch est le seul moyen de s'assurer que vous vérifiez chaque variation du nom d'une colonne dans le jeu de résultats.

Fragile? Sûr. Mais mérite une pensée. Honnêtement, je préférerais de loin une méthode HasColumn "officielle" sur IDataRecord.

31
Matt Hamilton

Sur une ligne, utilisez-le 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 bien 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));
27
Larry

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"))
{

}
19
Chris Ji

cela fonctionne pour moi:

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

Ce qui suit est simple et a fonctionné pour moi:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
11
Paulo Lisboa

Si vous lisez la question, Michael vous a posé des questions sur DataReader, et non sur les gens de DataRecord. Obtenez vos objets correctement.

L'utilisation d'une r.GetSchemaTable().Columns.Contains(field) sur un DataRecord fonctionne, mais renvoie des colonnes BS (voir la 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 de r.GetSchemaTable().Columns sur un DataReader renvoie les colonnes BS:

Calling GetSchemeTable in a DataReader

9
Levitikon

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

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

Voici une version linéaire de la réponse acceptée:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")
4
Clement
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
Deepak

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 obtenir tous ces noms de colonnes 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 Schema)

NOTE: cela semble ne renvoyer que 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
}
2
NeoH4x0r

Pour que votre code soit solide et propre, utilisez une fonction d'extension unique, comme celle-ci:

    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
1
Michael B

Je n’ai pas non plus obtenu GetSchemaTable au travail, jusqu’à ce que j’ai trouvé de cette façon .

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
1
David Andersson
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 n'est pas sensible à la casse.

1
RBAFF79

Vous pouvez également appeler GetSchemaTable () sur votre DataReader si vous voulez la liste des colonnes et que vous ne voulez pas avoir à obtenir une exception ...

0
Dave Markle

Ma classe d'accès aux données doit être compatible avec les versions antérieures. Par conséquent, j'essaie peut-être d'accéder à une colonne d'une version qui n'existe pas encore dans la base de données. Nous avons renvoyé des ensembles de données assez volumineux, je ne suis donc 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 une méthode générique qui tente de résoudre une valeur en fonction d'un nom de colonne et d'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 si

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
Tresto

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);
        }
}
0
joshua saucedo

Dans votre situation particulière (toutes les procédures ont les mêmes colonnes sauf 1 qui contient 1 colonne supplémentaire), il sera préférable de vérifier le lecteur plus rapidement. 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 à la solution itérative.

0
pkrzemo

Bien qu'il n'y ait pas de méthode exposée publiquement, une méthode existe dans la classe interne System.Data.ProviderBase.FieldNameLookup sur laquelle SqlDataReader 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) dans la classe System.Data.ProviderBase.FieldNameLookup et vous permettra de conserver le livre en conservant cette SqlDataReader.GetOrdinal() afin d'éviter tout effet secondaire. Le code généré reflète le SqlDataReader.GetOrdinal() existant, sauf qu'il appelle FieldNameLookup.IndexOf() au lieu de FieldNameLookup.GetOrdinal(). La méthode GetOrdinal() appelle la fonction IndexOf() et lève une exception si -1 est renvoyé. 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));
   }

}
0
Derek Ziemba

Ces réponses sont déjà postées ici. Just Linq-ing un peu:

bool b = reader.GetSchemaTable().Rows
                                .Cast<DataRow>()
                                .Select(x => (string)x["ColumnName"])
                                .Contains(colName, StringComparer.OrdinalIgnoreCase);
//or

bool b = Enumerable.Range(0, reader.FieldCount)
                   .Select(reader.GetName)
                   .Contains(colName, StringComparer.OrdinalIgnoreCase);

Le second est plus propre et beaucoup plus rapide. Même si vous ne lancez pas GetSchemaTable à chaque fois dans la première approche, la recherche sera très lente.

0
nawfal