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

Comment puis-je 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. L'une des procédures stockées a une colonne supplémentaire qui n'est pas utilisé par les autres procédures stockées. Je veux modifier la méthode pour accommoder chaque scénario.

ma demande est rédigée en C#.

193
demandé sur Michael Kniskern 2008-12-17 03:01:19

22 réponses

L'utilisation de Exception s pour la logique de contrôle comme dans d'autres réponses est considérée comme une mauvaise pratique et a des coûts de performance.

boucle à travers les champs peut avoir une petite performance hit si vous l'utilisez beaucoup et vous pouvez envisager de cacher les résultats""

la façon la plus appropriée de le faire 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;
    }
}
299
répondu Chad Grant 2018-07-19 20:22:16

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

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

Un appel - pas d'exceptions. Il pourrait y avoir des exceptions à l'intérieur, mais je ne pense pas.

NOTE: dans les commentaires ci-dessous, nous avons trouvé ceci... le code correct est en fait ceci:

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
répondu Jasmine 2015-07-03 18:57:14

je pense que votre meilleur pari est d'appeler GetOrdinal ("columnnname") sur votre DataReader à l'avant, et d'attraper un IndexOutOfRangeException dans le 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;
    }
}

Modifier

Ok, ce post commence à recueillir quelques votes négatifs dernièrement, et je ne peux pas le supprimer parce que c'est la réponse acceptée, donc je vais aller pour le mettre à jour et (j'espère) essayer de justifier l'utilisation de la manipulation d'exception comme flux de contrôle.

l'autre façon d'y parvenir, comme posté par Chad Grant , est de faire une boucle à travers chaque champ dans le DataReader et de faire une comparaison cas-insensible pour le nom du champ que vous recherchez. Cela fonctionne vraiment bien, et franchement effectuera probablement mieux que ma méthode ci-dessus. Certainement je n'utiliserais jamais la méthode ci-dessus à l'intérieur d'une boucle où performace a été question.

je peux penser à une situation dans laquelle la méthode try/GetOrdinal/catch fonctionnera là où la boucle ne fonctionne pas. Il s'agit, cependant, d'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 permet de "alias" colonnes 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 un select pour l'un ou l'autre nom retournerait les données dans cette colonne. Avec moi jusqu'à présent?

imaginez maintenant qu'il y a un ADO.NET fournisseur pour cette base de données, et ils ont codé une implémentation IDataReader pour it qui prend en compte les alias de colonne.

maintenant, dr.GetName(i) (comme utilisé dans la réponse de Chad) ne peut retourner qu'une seule chaîne, il doit donc retourner Seulement un des" alias " sur une colonne. Toutefois, 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 situation hypothétique de" colonnes aliasées", la méthode try/GetOrdinal/catch serait le seul moyen d'être sûr que vous vérifiez chaque variation du nom d'une colonne dans le jeu de résultats.

fragile? Assurer. Mais vaut la peine d'une pensée. Honnêtement, je préférerais une méthode" officielle " sur IDataRecord.

30
répondu Matt Hamilton 2017-05-23 12:34:30

en une ligne, utilisez ceci après la récupération de votre DataReader:

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

Puis,

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

Modifier

revêtement unique 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));
22
répondu Larry 2018-05-17 06:48:13

voici un échantillon 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
répondu Chris Ji 2012-03-07 11:55:26

ça marche pour moi:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");
12
répondu Victor Labastida 2015-09-03 20:21:44

ce qui suit est simple et travaillé pour moi:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
11
répondu Paulo Lisboa 2015-06-24 22:32:54

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

en utilisant un r.GetSchemaTable().Columns.Contains(field) sur un DataRecord ne fonctionne, mais il retourne des colonnes BS (voir screenshot ci-dessous.)

pour voir si une colonne de données existe et contient des données dans un lecteur de données, 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: /q/check-for-column-name-in-a-sqldatareader-object-13768/"RoleTemplateId") 
            && !dataReader.IsDBNull("RoleTemplateId");
    }

Appel r.GetSchemaTable().Columns sur un DataReader retourne BS colonnes:

Calling GetSchemeTable in a DataReader

9
répondu Levitikon 2011-08-30 21:55:31

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'usage est :

If HasColumnAndValue(reader, "ID_USER") Then
    Me.UserID = reader.GetDecimal(reader.GetOrdinal("ID_USER")).ToString()
End If
7
répondu HoLyVieR 2011-11-07 21:45:17

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

reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;
5
répondu spaark 2014-04-24 11:33:23

Voici une version linq one liner de la réponse acceptée:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")
4
répondu Clement 2014-03-13 00:58:50
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
répondu Deepak 2014-12-23 21:53:07

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 colonne inutiles et pas le nom de la colonne de votre table... Est parce que vous recevez le nom de la colonne de schéma (i.e. les noms de colonne pour la table de schéma)

NOTE: ceci ne semble renvoyer que le nom de la première colonne...

EDIT: correction de code qui renvoie le nom de toutes les colonnes, mais vous ne pouvez pas utiliser un SqlDataReader de 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
répondu NeoH4x0r 2013-11-08 17:47:38

Je N'ai pas non plus obtenu GetSchemaTable de travailler, jusqu'à ce que je trouve 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
répondu David Andersson 2010-04-22 07:07:53
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 pas sensible à la casse btw.

1
répondu RBAFF79 2011-11-08 11:38:02

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
1
répondu Michael B 2013-12-17 12:52:26

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

0
répondu Dave Markle 2008-12-17 00:27:43

ces réponses sont déjà postées ici. Il suffit de 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 l'exécutez GetSchemaTable à chaque fois dans la première approche, la recherche va être très lent.

0
répondu nawfal 2013-12-12 14:28:25

dans votre situation particulière (toutes les procédures ont les mêmes colonnes sauf une qui a une colonne supplémentaire), il sera préférable 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 vieux post, mais j'ai décidé de répondre pour aider les autres dans la même situation. vous pouvez également (pour des raisons de performance) mélanger cette solution avec la solution itérative solution.

0
répondu pkrzemo 2014-08-18 07:51:34

ma classe d'accès aux données doit être rétrocompatible, donc je pourrais essayer d'accéder à une colonne dans une version où elle n'existe pas encore dans la base de données. Nous avons quelques ensembles de données assez grands étant retournés de sorte que je ne suis pas un grand fan d'une méthode d'extension qui doit itérer la collecte de colonne DataReader pour chaque propriété.

j'ai une classe utilitaire qui crée une liste privée de colonnes et ensuite on a une méthode générique qui tente de résoudre une valeur basée sur une nom de la colonne et 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());
            }
        }
    }
}

alors je peux 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
répondu Tresto 2015-05-14 19:40:44

bien qu'il n'existe aucune méthode exposée publiquement, une méthode existe dans la classe interne System.Data.ProviderBase.FieldNameLookup sur laquelle SqlDataReader se fonde.

pour y accéder et obtenir des performances natives, vous devez utiliser ILGenerator pour créer une méthode à l'exécution. Le code suivant vous donnera un accès direct à int IndexOf(string fieldName) dans la classe System.Data.ProviderBase.FieldNameLookup ainsi qu'effectuer la tenue de livres que SqlDataReader.GetOrdinal() fait de sorte qu'il n'y a aucun effet secondaire. Les miroirs de code générés le SqlDataReader.GetOrdinal() sauf qu'il appelle FieldNameLookup.IndexOf() au lieu de FieldNameLookup.GetOrdinal() . La méthode GetOrdinal() appelle la fonction IndexOf() et lance une exception si -1 est retourné, donc nous contournons 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
répondu Derek Ziemba 2018-05-02 01:33:54

Que Diriez-vous de

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

Il ne serait probablement pas aussi efficace dans une boucle

-1
répondu Skadoosh 2011-01-12 16:18:05