Проверьте имя столбца в объекте SqlDataReader


Как проверить, существует ли столбец в объекте SqlDataReader? На уровне доступа к данным я создал метод, который создает один и тот же объект для нескольких вызовов хранимых процедур. Одна из хранимых процедур имеет дополнительный столбец, который не используется другими хранимыми процедурами. Я хочу изменить метод, чтобы приспособить его для каждого сценария.

Мое заявление написано на C#.

22 193

22 ответа:

Использование Exceptions для логики управления, как и в некоторых других ответах, считается плохой практикой и имеет затраты на производительность.

Циклическое перемещение по полям может иметь небольшое снижение производительности, если вы используете его много, и вы можете рассмотреть возможность кэширования результатов

Более подходящий способ сделать это:

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

гораздо лучше использовать эту булеву функцию:

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

Один вызов-никаких исключений. Он может создавать исключения внутри себя, но я так не думаю.

Примечание: в комментариях ниже мы выяснили это... правильный код на самом деле таков:
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; 
}

Я думаю, что лучше всего вызвать GetOrdinal("columnName") на вашем DataReader спереди и поймать IndexOutOfRangeException в случае, если столбец отсутствует.

Фактически, давайте сделаем метод расширения:

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

Edit

Хорошо, в последнее время этот пост начинает набирать несколько голосов, и я не могу удалить его, потому что это принятый ответ, поэтому я собираюсь обновить его и (я надеюсь) попытаться оправдать использование обработки исключений в качестве контроля поток.

Другой способ достичь этого, как, опубликованный чадом Грантом , заключается в циклическом переборе каждого поля в DataReader и выполнении сравнения без учета регистра для имени поля, которое вы ищете. Это будет работать очень хорошо, и, честно говоря, вероятно, будет работать лучше, чем мой метод выше. Конечно, я никогда не буду использовать данный метод в цикле, где производительность является проблемой.

Я могу придумать одну ситуацию, в которой метод try / GetOrdinal/catch будет работать там, где это, однако, полностью гипотетическая ситуация прямо сейчас, так что это очень хлипкое оправдание. В любом случае, потерпи и посмотри, что ты думаешь.

Представьте себе базу данных, которая позволяет вам "псевдонимировать" столбцы в таблице. Представьте, что я могу определить таблицу со столбцом под названием "EmployeeName", но также дать ему псевдоним" EmpName", и выполнение выбора для любого имени вернет данные в этом столбце. Со мной так далеко?

Теперь представьте себе, что существует ADO.NET поставщик для этой базы данных, и они закодировали реализацию IDataReader для нее, которая учитывает псевдонимы столбцов.

Теперь dr.GetName(i) (как используется в ответе чада) может возвращать только одну строку, поэтому он должен возвращать толькоодин из "псевдонимов" в столбце. Однако GetOrdinal("EmpName") может использовать внутреннюю реализацию полей этого поставщика для проверки псевдонима каждого столбца на имя, которое вы ищете.

В этой гипотетической ситуации с "псевдонимными столбцами" метод try / GetOrdinal / catch будет единственным способом убедиться, что вы проверяете каждое изменение имени столбца в результирующем наборе.

Хлипкий? Конечно. Но стоит подумать. Честно говоря, я бы предпочел "официальный" метод HasColumn на IDataRecord.

В одной строке используйте это после извлечения DataReader:

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

Затем,

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

Edit

Гораздо более эффективный однострочный, который не требует загрузки схемы:

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

Вот рабочий образец для идеи Жасмин:

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

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

}

Это работает для меня:

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

Следующее просто и работает для меня:

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

Если вы читали вопрос, Майкл спрашивал о DataReader, а не о людях DataRecord. Получите ваши объекты правильно.

Использование r.GetSchemaTable().Columns.Contains(field) в записи данных действительно работает, но возвращает столбцы BS (см. скриншот ниже.)

Чтобы узнать, существует ли столбец данных и содержит ли он данные в DataReader, используйте следующие расширения:

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

Использование:

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

Вызов r.GetSchemaTable().Columns в DataReader возвращает столбцы BS:

Вызов GetSchemeTable в средстве чтения данных

Я написал для пользователей 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

Я думаю, что это более мощный и использование:

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

Вот решение от жасмина в одну строку... (еще один, очень простой!):

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

Вот один лайнер linq версия принятого ответа:

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

Этот код исправляет проблемы, которые Левитикон имел с их кодом: (адаптировано из: [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;
}

Причина получения всех этих бесполезных имен столбцов, а не имя столбца из вашей таблицы... Это потому, что вы получаете имя столбца схемы (то есть имена столбцов для таблицы схемы)

Примечание: это, кажется, возвращает только имя первого колонка...

EDIT: исправлен код, который возвращает имя всех столбцов, но для этого нельзя использовать SqlDataReader

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
}

И я не мог заставить GetSchemaTable работать, пока не нашел этот путь.

В основном я делаю это:

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
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 не чувствителен к регистру, кстати.

Чтобы сохранить ваш код надежным и чистым, используйте одну функцию расширения, например:

    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

Вы также можете вызвать GetSchemaTable() в DataReader, если вам нужен список столбцов и вы не хотите получать исключение...

Эти ответы уже размещены здесь. Просто Линк-Инг немного:

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

Второй чище и гораздо быстрее. Даже если вы не будете запускать GetSchemaTable каждый раз в первом подходе, поиск будет очень медленным.

В вашей конкретной ситуации (все процедуры имеют одинаковые столбцы, кроме 1, который имеет дополнительный столбец 1), будет лучше и быстрее проверить reader. Свойство FieldCount позволяет различать их.

const int NormalColCount=.....
if(reader.FieldCount > NormalColCount)
{
// Do something special
}
Я знаю, что это старый пост, но я решил ответить, чтобы помочь другим в той же ситуации. вы также можете (по соображениям производительности) смешать это решение с итерационным решением.

Мой класс доступа к данным должен быть обратно совместим, поэтому я могу пытаться получить доступ к столбцу в выпуске, где он еще не существует в базе данных. У нас есть несколько довольно больших наборов данных, которые возвращаются, поэтому я не большой поклонник метода расширения, который должен повторять коллекцию столбцов DataReader для каждого свойства.

У меня есть служебный класс, который создает закрытый список столбцов, а затем имеет универсальный метод, который пытается разрешить значение на основе имени столбца и вывода тип параметра.

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

Тогда я могу просто назвать свой код так

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

Хотя не существует открытого метода, метод существует во внутреннем классе System.Data.ProviderBase.FieldNameLookup, на который опирается SqlDataReader.

Чтобы получить доступ к нему и получить собственную производительность, необходимо использовать ILGenerator для создания метода во время выполнения. Следующий код даст вам прямой доступ к int IndexOf(string fieldName) в классе System.Data.ProviderBase.FieldNameLookup, а также выполнит Бухгалтерский учет, который SqlDataReader.GetOrdinal()делает так, чтобы не было никакого побочного эффекта. Сгенерированный код отражает существующий SqlDataReader.GetOrdinal(), за исключением того, что он вызывает FieldNameLookup.IndexOf() вместо FieldNameLookup.GetOrdinal(). Метод GetOrdinal() вызывает функцию IndexOf() и создает исключение, если возвращается -1, поэтому мы обходим это поведение.

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

}

Как насчет

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

Это, вероятно, не было бы так эффективно в цикле