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

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

Мое приложение написано на C#.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
221
0
158 850
25
Перейти к ответу Данный вопрос помечен как решенный

Ответы 25

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

Собственно, сделаем метод расширения:

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

Редактировать

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

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

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

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

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

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

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

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

Я собирался предложить то же самое, GetOrdinal великолепен, потому что поиск нечувствителен к регистру, а в случае неудачи он выполняет поиск с учетом регистра.

Ta01 17.12.2008 03:16

использование исключений для логики управления? нет нет нет

Chad Grant 02.05.2009 03:42

Есть одна маленькая вещь, которую все упускают из виду, когда я первоначально разместил этот вопрос ... Я задал вопрос 8 декабря 2008 года, а Мэтт опубликовал свой ответ 17 декабря 2008 года. Все думали, что ловят исключение для логики управления, но не предлагали надежного альтернативного решения до 1 мая 2009 года. Поэтому изначально он был отмечен как ответ. Я все еще использую это решение сегодня.

Michael Kniskern 14.12.2010 18:32

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

Nick Harrison 05.05.2011 19:22

Вы также должны поймать другие исключения, которые означают "столбец не существует" (например, ArgumentException)

Ian Boyd 20.07.2012 18:19

+1. Я согласен с «Не использовать исключения для логики управления» как с общим правилом проектирования. Это не означает «избегать этого любой ценой». Ответ - это очень хорошо задокументированный обходной путь, и, как говорит @Nick, снижение производительности (если оно есть ..) происходит только тогда, когда столбец не существует.

Larry 04.11.2013 17:26

@IanBoyd в этом нет необходимости. В любом случае будут возникать и другие исключения.

Larry 05.08.2014 23:46

По моему опыту, использование исключений в качестве управляющей логики также делает отладку более громоздкой. Вы должны снять галочку с «Thrown» в «Common Language Runtime Exceptions», а затем, когда вы получите реальное исключение, оно может сломаться где-то в обработчике, а не в строке, в которой есть проблема.

cedd 23.12.2014 13:32

Я не вижу ничего плохого в этом коде. Беспокойство о стоимости исключения в подключенном приложении похоже на беспокойство об аэродинамике радиоантенны в автомобиле. stackoverflow.com/a/891230/852208

b_levitt 21.05.2019 19:46

Я начинаю понимать, почему этот ответ не получил голосов. При чтении docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… он содержит сводку «НЕ используйте исключения для нормального потока управления, если это возможно». Эта статья посвящена ВЫБРОСУ исключений. Код в этом примере нарушил бы это правило, только если бы он делал что-то вроде throw FalseException в случае, если столбец не был найден.

b_levitt 18.09.2019 00:57

после тестирования этот метод работает быстрее для более чем 11 столбцов. Смотрите мой ответ внизу.

b_levitt 19.09.2019 00:44

Как некоторые даже позволяют своему языку или пальцу двигаться, чтобы предложить использовать исключения для алгоритма проверки? О боже ... нет-нет-нет.

F8ER 06.10.2020 23:29

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

Есть некоторые споры о том, работает ли это: stackoverflow.com/questions/373230/…

bzlm 08.04.2010 10:40

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

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

@ Жасмин: Спасибо за ваше предложение. Кстати, это также будет работать для OracleDataReader! @Chad Grant: Я согласен, использование исключений для потока управления - НЕТ НЕТ! Если вы не беспокоитесь о написании корпоративного (то есть профессионального) кода.

Steve J 15.06.2009 19:06

@ Жасмин: Я заговорил слишком рано! Ваш код проверяет наличие столбца в таблице схемы, а не набора результатов. Вам необходимо сравнить «field» (при условии, что «field» - это имя столбца) со значением поля «ColumnName» каждой строки. Прервите, когда найдете, верните false, если нет.

Steve J 16.06.2009 01:46

Да, ты прав. Я закончил тем, что написал что-то подобное для своей программы, и именно поэтому я был здесь и смотрел на это в первую очередь. Это уродливая вещь зацикливания ... public static bool HasColumn (DbDataReader Reader, string ColumnName) {foreach (DataRow row in Reader.GetSchemaTable (). Rows) {if (row ["ColumnName"]. ToString () == ColumnName ) вернуть истину; } //Все еще здесь? Столбец не найден. вернуть ложь; }

Jasmine 23.06.2009 23:58

@Steve J: Когда в наборе результатов НЕ будет столбца в GetSchemaTable?

Bless Yahu 07.08.2009 20:13

У меня, к сожалению, не сработало. Но с модификацией я обнаружил, что это так, см. Мой ответ.

David Andersson 22.04.2010 11:10

Чтобы кого-нибудь еще запутали, ЭТО НЕ РАБОТАЕТ. См. Ответ ниже об извлечении строки ColumnName из таблицы схемы и ее использовании.

Jason Jackson 17.12.2012 21:30

Да, это НЕ РАБОТАЕТ. Кто столько раз за него голосовал ??? Это бы сэкономило мне много времени на отладку, если бы здесь не было этого ответа!

c00000fd 15.04.2013 14:11

Возможно, потому что ответ собственно в комментариях. Я отредактирую ответ, чтобы включить решение в основную часть.

Jasmine 16.04.2013 01:27

Почему бы не отредактировать свой ответ, чтобы удалить неработающую часть? Я все еще не понимаю, что на самом деле работает, а что нет в вашем ответе.

NickG 08.08.2013 20:01

Ник, они оба работают, но это не принятый ответ. На самом деле здесь пять правильных ответов, и все они немного разные.

Jasmine 08.08.2013 23:45

@ Жасмин, они оба работают? Не совсем. Удалите первую часть своего ответа. Я бы сделал сам, если бы не твой последний комментарий!

nawfal 12.12.2013 17:49

Это хороший ответ, но с Linq он становится еще лучше. Если интересно, см. Ответ Виктора Лабастиды.

Hagelt18 10.12.2015 20:36

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

Daniel Schilling 29.08.2018 21:19

использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. И это не реализовано в ядре dotnet. Проверьте источник github.com/microsoft/referencesource/blob/…

Chad Grant 04.10.2019 20:54
Ответ принят как подходящий
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;
    }
}

Использование Exception для логики управления, как и в некоторых других ответах, - это считается плохой практикой и имеет затраты на производительность. Он также отправляет ложные срабатывания профилировщику # выброшенных исключений, и бог поможет любому, кто настраивает свой отладчик на прерывание при появлении исключений.

GetSchemaTable () также является еще одним предложением во многих ответах. Это не будет предпочтительным способом проверки существования поля, поскольку он реализован не во всех версиях (он абстрактный и выдает NotSupportedException в некоторых версиях dotnetcore). GetSchemaTable также обладает избыточной производительностью, поскольку это довольно тяжелая функция, если вы используете проверить источник.

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

Что делать, если используется псевдоним? Сравнение имен не удастся.

Murphybro2 24.03.2017 13:54

Это спорно, что использование потока исключений является плохой практикой. Когда-то это считалось плохим, потому что это ОТНОСИТЕЛЬНО дорого для других операторов, но незначительно в подключенном приложении. Еще в 2006 году Skeet измерял 40–118 исключений в мс в зависимости от глубины стека. stackoverflow.com/a/891230/852208. Кроме того, без тестирования возможно, что этот код на самом деле медленнее, поскольку в среднем он проверяет половину всех столбцов (хотя все еще тривиально в приложении, подключенном к базе данных). Я бы отредактировал этот ответ, включив только средний абзац, поскольку два других являются мнениями.

b_levitt 21.05.2019 19:44

@b_levitt это не спорно, это дерьмо код, и вы не должны полагаться на исключения для управления потоком

Chad Grant 13.08.2019 04:26

Как и два предложения, на которые я указал, это еще одно мнение, которое не подкрепляется никакими объяснениями, кроме производительности в чисто вычислительном приложении. Смею вас настроить ваш отладчик на прерывание всех исключений и отключение только моего кода, и вы увидите, сколько даже фреймворк и другие библиотеки уже делают это. Проблема с вашим советом в том, что он заставляет разработчиков возвращать коды, которые в большинстве согласен, это подчиненный шаблон: stackoverflow.com/questions/99683/…. Такая методика не проходит проверку «ямы успеха».

b_levitt 14.08.2019 21:23

С точки зрения кода ваш ответ верен. Но ваше мнение, пытаясь взвесить его как превосходный ответ на ответ с помощью try / catch (который также обрабатывает псевдонимы), здесь неуместен.

b_levitt 14.08.2019 21:26

В C# 6 были добавлены фильтры исключений. Более того, в основные системные библиотеки уже давно встроены различные типы исключений. Если бы прародители языка не планировали использовать исключения для контроля, это не было бы функциями.

Broom 14.08.2019 22:35

Ничто в моем примере не требует от разработчиков возврата кодов? Он возвращает логическое значение! Следует освежить в памяти некоторые основные рекомендации по кодированию: «НЕ используйте исключения для нормального потока управления, если это возможно». docs.microsoft.com/en-us/dotnet/standard/design-guidelines/…

Chad Grant 29.08.2019 01:04

Строуман. Я никогда не говорил, что ваш пример требует, чтобы разработчики возвращали коды. Ваш пример действительно является «ответом», но ваше мнение о логике исключений действительно уводит людей от исключений к низшим шаблонам.

b_levitt 18.09.2019 00:35

В конце концов, все, что я говорю, это придерживаться фактов. Оставьте фрагмент кода, получивший наибольшее количество голосов. Но отбросьте мнения - это не то, что С.О. для. Большинство людей поддерживают ваш ответ, потому что это рабочий код, они не говорят «Я согласен». Но затем приходит кто-то неопытный и читает «это плохая практика», неверно интерпретирует несколько сотен голосов как «я согласен», и теперь мне приходится тратить дополнительные часы на отладку кода, который выдает ошибку на 5 строк позже, потому что кто-то написал DoSomething метод, который возвращает false, когда он терпит неудачу, но забыл проверить это кровавым «если».

b_levitt 18.09.2019 00:53

Этот ответ быстрее, чем метод GetOrdinal / catch, когда общее количество возвращенных столбцов меньше 11. См. Мой ответ для полного объяснения и тестового кода.

b_levitt 19.09.2019 00:50

@b_levitt Most people are upvoting your answer because it's working code, they are not saying "I agree". Вы знаете это ... как?

LarsTech 21.09.2019 00:35

Экстраполировано из комментариев за и против ответа GetOrdinal.

b_levitt 21.09.2019 01:09

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

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

Как насчет

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

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

См. Левитикон ответ, чтобы увидеть, что содержит dr.GetSchemaTable().Columns - это не то, что вы ищете.

Daniel Schilling 29.08.2018 21:06
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, кстати, нечувствителен к регистру.

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

Chad Grant 04.10.2019 21:03

Если вы читаете вопрос, Майкл спрашивал о DataReader, а не о DataRecord. Сделайте ваши объекты правильными.

Использование r.GetSchemaTable().Columns.Contains(field) в DataRecord действительно работает, но возвращает столбцы 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:

Calling GetSchemeTable in a DataReader

см. комментарии под ответом Мэтта

nawfal 08.08.2013 08:49

Что вы имеете в виду под DataRecord работает, но возвращает столбцы BS? Вы имеете в виду, что он работает (и дает неверные результаты)?

nawfal 12.12.2013 18:01

"Сделайте ваши объекты правильными". - но IDataReader реализует IDataRecord. Это разные интерфейсы одного и того же объекта - точно так же, как ICollection<T> и IEnumerable<T> - это разные интерфейсы List<T>. IDataReader позволяет перейти к следующей записи, а IDataRecord позволяет читать из текущей записи. Все методы, которые используются в этом ответе, поступают из интерфейса IDataRecord. См. stackoverflow.com/a/1357743/221708 для объяснения того, почему объявление параметра как IDataRecord предпочтительнее.

Daniel Schilling 29.08.2018 20:55

Проголосуйте за то, чтобы показать, почему r.GetSchemaTable().Columns - абсолютно неправильный ответ на этот вопрос.

Daniel Schilling 29.08.2018 20:59

GetName () наследуется от интерфейса IDataRecord в IDataReader. Ориентация на базовый интерфейс - это правильный код.

Chad Grant 04.10.2019 20:56

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

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

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

}

Только если обернуть вокруг него попытку / улов

Donald.Record 25.06.2015 18:59

Вы можете упростить эту идею с помощью: reader.GetSchemaTable (). Columns.Contains ("myFiled")

Lev Z 24.11.2016 19:24

использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…

Chad Grant 04.10.2019 20:52

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

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

ПРИМЕЧАНИЕ: похоже, это возвращает только имя первого столбца ...

Обновлено: исправленный код, который возвращает имя всех столбцов, но вы не можете использовать 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
}

Или в одну строчку return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList(); :)

nawfal 12.12.2013 18:03

использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…

Chad Grant 04.10.2019 20:52
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);
    }

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

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

Потом,

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

Редактировать

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

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

Вы перечисляете имена полей несколько раз / выделяете другой массив для сканирования с помощью contains, это будет намного менее эффективно в коде с высоким трафиком.

Chad Grant 04.10.2019 20:44

@ChadGrant, конечно, поэтому Linq one liner намного эффективнее, поскольку он выполняет только одну итерацию.

Larry 06.10.2019 01:31

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

    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

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

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

сравнение с учетом регистра ... почему?

Chad Grant 25.10.2019 01:26

Вот решение от Jasmine в одну строку ... (еще одно, хотя простое!):

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

использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…

Chad Grant 04.10.2019 20:52

@ChadGrant Возможно. Думаю, нужно выбирать с умом в зависимости от контекста и частоты, с которой необходимо это использовать ...

spaark 18.10.2019 11:50

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

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

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

Назовите решение, о котором вы говорите. Какие два раствора следует смешать?

Pablo Jomer 18.08.2014 11:47

Мой класс доступа к данным должен быть обратно совместим, поэтому я могу попытаться получить доступ к столбцу в выпуске, где он еще не существует в базе данных. У нас есть возвращаемые довольно большие наборы данных, поэтому я не большой поклонник метода расширения, который должен повторять коллекцию столбцов 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;
    }
}

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

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

использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…

Chad Grant 04.10.2019 20:49

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

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

использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. И это реализовано не во всех версиях ядра dotnet. Проверьте источник github.com/microsoft/referencesource/blob/…

Chad Grant 04.10.2019 20:58

Хотя общедоступного метода нет, метод все же существует во внутреннем классе 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));
   }

}

Внутренний код делает почти то же самое, что и мой ответ, без необходимости в этом странном отражении / делегате. Это кеширование поиска для каждого экземпляра объекта, что не принесет пользы, поскольку в реальном мире вы хотите кэшировать порядковые номера при первом запуске запроса и использовать этот кеш в течение всего времени существования приложения, а не создавать новый кеш для каждого запроса.

Chad Grant 04.10.2019 21:34

эта работа для меня

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

TL; DR:

Множество ответов с утверждениями о производительности и плохой практике, поэтому я уточняю это здесь.

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

Полный ответ:

Код для некоторых из основных ответов работает, но здесь ведутся споры о «лучшем» ответе, основанном на принятии обработки исключений в логике и связанной с ней производительности.

Чтобы убрать это, я не верю, что существует много рекомендаций относительно ЛОВАШНИХ исключений. У Microsoft есть некоторое руководство относительно исключений THROWING. Там они утверждают:

DO NOT use exceptions for the normal flow of control, if possible.

Первое замечание - снисходительность «по возможности». Что еще более важно, описание дает этот контекст:

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

Это означает, что если вы пишете API, который может быть использован кем-то другим, дайте им возможность перемещаться по исключению без использования try / catch. Например, предоставьте TryParse с помощью метода Parse, генерирующего исключение. Однако нигде не сказано, что вы не должны ловить исключение.

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

Можно сказать, что есть НЕКОТОРЫЕ затраты на выброшенное исключение, и эти затраты МОГУТ повлиять на производительность в тяжелом цикле. Однако можно также сказать, что стоимость исключения будет незначительной в «подключенном приложении». Фактическая стоимость была исследована более десяти лет назад: https://stackoverflow.com/a/891230/852208 Другими словами, стоимость соединения и запроса к базе данных, вероятно, будет меньше, чем стоимость брошенного исключения.

Помимо всего этого, я хотел определить, какой метод действительно быстрее. Как и ожидалось, конкретного ответа нет.

Любой код, который перебирает столбцы, становится медленнее по мере того, как количество столбцов существует. Также можно сказать, что любой код, который полагается на исключения, будет замедляться в зависимости от скорости, с которой не удается найти запрос.

Взяв ответы как Чада Гранта, так и Мэтта Гамильтона, я запустил оба метода с количеством столбцов до 20 и частотой ошибок до 50% (OP указал, что он использовал эти два теста между разными процессами, поэтому я предположил, что их всего два) .

Вот результаты, построенные с помощью LinqPad: Results - Series 1 is Loop, 2 is Exception

Зигзаги здесь - частота отказов (столбец не найден) в пределах каждого столбца.

Для более узких наборов результатов хорошим выбором будет зацикливание. Однако метод GetOrdinal / Exception не так чувствителен к количеству столбцов и начинает превосходить метод цикла примерно в 11 столбцах.

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

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

Вот тест в форме linqpad. Не стесняйтесь репостить своим собственным методом:

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


}

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

Chad Grant 20.09.2019 21:11

Другая проблема с использованием исключений в качестве потока управления заключается в том, что они отображаются в профилировщике как # исключений, созданных, когда в предложенном вами коде они являются преднамеренными ... а не исключениями. Не говоря уже о том, чтобы настроить ваш отладчик на прерывание при появлении исключений. По сути, сообщение об ошибках, которые не являются ошибками. Вы не должны этого делать.

Chad Grant 04.10.2019 20:33

Также есть счетчики для окончаний / сек и фильтров / сек. Это тоже плохо? Я бы назвал это возможным предостережением - первым из реальных, которые вы предоставили. Счетчики - это просто информация. Они ничего не значат, если не соответствуют проблеме с производительностью - и в этом случае я уже показал точку, в которой исключения имеют ЛУЧШУЮ производительность. Я также указал, что фреймворк и библиотеки уже создают множество исключений. У меня сейчас есть пример визуальной студии, выдающей 60 ex / s. Исключения не являются ошибками, если они не перехвачены.

b_levitt 07.10.2019 16:54

Отличный анализ. Я использовал его результаты в своем новом ответе.

yazanpro 13.11.2019 22:46
if (Enumerable.Range(0,reader.FieldCount).Select(reader.GetName).Contains("columName"))
{
     employee.EmployeeId= Utility.ConvertReaderToLong(reader["EmployeeId"]);
}

вы можете получить более подробную информацию здесь: Можете ли вы получить имена столбцов из SqlDataReader?

Ключ ко всей проблеме - здесь:

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

Если указанные три строки (в настоящее время строки 72, 73 и 74) удалены, вы можете легко проверить наличие -1, чтобы определить, не существует ли столбец.

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

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

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

Метод расширения на основе отражения:

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

Другие вопросы по теме