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





Я думаю, что лучше всего вызвать 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.
использование исключений для логики управления? нет нет нет
Есть одна маленькая вещь, которую все упускают из виду, когда я первоначально разместил этот вопрос ... Я задал вопрос 8 декабря 2008 года, а Мэтт опубликовал свой ответ 17 декабря 2008 года. Все думали, что ловят исключение для логики управления, но не предлагали надежного альтернативного решения до 1 мая 2009 года. Поэтому изначально он был отмечен как ответ. Я все еще использую это решение сегодня.
Это приведет к снижению производительности только в том случае, если столбец не был там. Другие описанные методы будут иметь падение производительности и большее снижение производительности каждый раз. Хотя, как правило, избегать использования обработки исключений для потока управления, это решение не следует исключать, предварительно не подумав, работает ли оно в вашем случае.
Вы также должны поймать другие исключения, которые означают "столбец не существует" (например, ArgumentException)
+1. Я согласен с «Не использовать исключения для логики управления» как с общим правилом проектирования. Это не означает «избегать этого любой ценой». Ответ - это очень хорошо задокументированный обходной путь, и, как говорит @Nick, снижение производительности (если оно есть ..) происходит только тогда, когда столбец не существует.
@IanBoyd в этом нет необходимости. В любом случае будут возникать и другие исключения.
По моему опыту, использование исключений в качестве управляющей логики также делает отладку более громоздкой. Вы должны снять галочку с «Thrown» в «Common Language Runtime Exceptions», а затем, когда вы получите реальное исключение, оно может сломаться где-то в обработчике, а не в строке, в которой есть проблема.
Я не вижу ничего плохого в этом коде. Беспокойство о стоимости исключения в подключенном приложении похоже на беспокойство об аэродинамике радиоантенны в автомобиле. stackoverflow.com/a/891230/852208
Я начинаю понимать, почему этот ответ не получил голосов. При чтении docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… он содержит сводку «НЕ используйте исключения для нормального потока управления, если это возможно». Эта статья посвящена ВЫБРОСУ исключений. Код в этом примере нарушил бы это правило, только если бы он делал что-то вроде throw FalseException в случае, если столбец не был найден.
после тестирования этот метод работает быстрее для более чем 11 столбцов. Смотрите мой ответ внизу.
Как некоторые даже позволяют своему языку или пальцу двигаться, чтобы предложить использовать исключения для алгоритма проверки? О боже ... нет-нет-нет.
Вы также можете вызвать GetSchemaTable () в своем DataReader, если вам нужен список столбцов, и вы не хотите получать исключение ...
Есть некоторые споры о том, работает ли это: stackoverflow.com/questions/373230/…
Намного лучше использовать эту логическую функцию:
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: Я согласен, использование исключений для потока управления - НЕТ НЕТ! Если вы не беспокоитесь о написании корпоративного (то есть профессионального) кода.
@ Жасмин: Я заговорил слишком рано! Ваш код проверяет наличие столбца в таблице схемы, а не набора результатов. Вам необходимо сравнить «field» (при условии, что «field» - это имя столбца) со значением поля «ColumnName» каждой строки. Прервите, когда найдете, верните false, если нет.
Да, ты прав. Я закончил тем, что написал что-то подобное для своей программы, и именно поэтому я был здесь и смотрел на это в первую очередь. Это уродливая вещь зацикливания ... public static bool HasColumn (DbDataReader Reader, string ColumnName) {foreach (DataRow row in Reader.GetSchemaTable (). Rows) {if (row ["ColumnName"]. ToString () == ColumnName ) вернуть истину; } //Все еще здесь? Столбец не найден. вернуть ложь; }
@Steve J: Когда в наборе результатов НЕ будет столбца в GetSchemaTable?
У меня, к сожалению, не сработало. Но с модификацией я обнаружил, что это так, см. Мой ответ.
Чтобы кого-нибудь еще запутали, ЭТО НЕ РАБОТАЕТ. См. Ответ ниже об извлечении строки ColumnName из таблицы схемы и ее использовании.
Да, это НЕ РАБОТАЕТ. Кто столько раз за него голосовал ??? Это бы сэкономило мне много времени на отладку, если бы здесь не было этого ответа!
Возможно, потому что ответ собственно в комментариях. Я отредактирую ответ, чтобы включить решение в основную часть.
Почему бы не отредактировать свой ответ, чтобы удалить неработающую часть? Я все еще не понимаю, что на самом деле работает, а что нет в вашем ответе.
Ник, они оба работают, но это не принятый ответ. На самом деле здесь пять правильных ответов, и все они немного разные.
@ Жасмин, они оба работают? Не совсем. Удалите первую часть своего ответа. Я бы сделал сам, если бы не твой последний комментарий!
Это хороший ответ, но с Linq он становится еще лучше. Если интересно, см. Ответ Виктора Лабастиды.
См. Документация GetSchemaTable, чтобы узнать, почему исходная часть этого ответа не работает. Каждый столбец, который вы ищете, представлен в результате GetSchemaTable в виде строки, а не столбца.
использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. И это не реализовано в ядре dotnet. Проверьте источник github.com/microsoft/referencesource/blob/…
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 также обладает избыточной производительностью, поскольку это довольно тяжелая функция, если вы используете проверить источник.
Цикл по полям может иметь небольшое снижение производительности, если вы часто его используете, и вы можете рассмотреть возможность кеширования результатов.
Что делать, если используется псевдоним? Сравнение имен не удастся.
Это спорно, что использование потока исключений является плохой практикой. Когда-то это считалось плохим, потому что это ОТНОСИТЕЛЬНО дорого для других операторов, но незначительно в подключенном приложении. Еще в 2006 году Skeet измерял 40–118 исключений в мс в зависимости от глубины стека. stackoverflow.com/a/891230/852208. Кроме того, без тестирования возможно, что этот код на самом деле медленнее, поскольку в среднем он проверяет половину всех столбцов (хотя все еще тривиально в приложении, подключенном к базе данных). Я бы отредактировал этот ответ, включив только средний абзац, поскольку два других являются мнениями.
@b_levitt это не спорно, это дерьмо код, и вы не должны полагаться на исключения для управления потоком
Как и два предложения, на которые я указал, это еще одно мнение, которое не подкрепляется никакими объяснениями, кроме производительности в чисто вычислительном приложении. Смею вас настроить ваш отладчик на прерывание всех исключений и отключение только моего кода, и вы увидите, сколько даже фреймворк и другие библиотеки уже делают это. Проблема с вашим советом в том, что он заставляет разработчиков возвращать коды, которые в большинстве согласен, это подчиненный шаблон: stackoverflow.com/questions/99683/…. Такая методика не проходит проверку «ямы успеха».
С точки зрения кода ваш ответ верен. Но ваше мнение, пытаясь взвесить его как превосходный ответ на ответ с помощью try / catch (который также обрабатывает псевдонимы), здесь неуместен.
В C# 6 были добавлены фильтры исключений. Более того, в основные системные библиотеки уже давно встроены различные типы исключений. Если бы прародители языка не планировали использовать исключения для контроля, это не было бы функциями.
Ничто в моем примере не требует от разработчиков возврата кодов? Он возвращает логическое значение! Следует освежить в памяти некоторые основные рекомендации по кодированию: «НЕ используйте исключения для нормального потока управления, если это возможно». docs.microsoft.com/en-us/dotnet/standard/design-guidelines/…
Строуман. Я никогда не говорил, что ваш пример требует, чтобы разработчики возвращали коды. Ваш пример действительно является «ответом», но ваше мнение о логике исключений действительно уводит людей от исключений к низшим шаблонам.
В конце концов, все, что я говорю, это придерживаться фактов. Оставьте фрагмент кода, получивший наибольшее количество голосов. Но отбросьте мнения - это не то, что С.О. для. Большинство людей поддерживают ваш ответ, потому что это рабочий код, они не говорят «Я согласен». Но затем приходит кто-то неопытный и читает «это плохая практика», неверно интерпретирует несколько сотен голосов как «я согласен», и теперь мне приходится тратить дополнительные часы на отладку кода, который выдает ошибку на 5 строк позже, потому что кто-то написал DoSomething метод, который возвращает false, когда он терпит неудачу, но забыл проверить это кровавым «если».
Этот ответ быстрее, чем метод GetOrdinal / catch, когда общее количество возвращенных столбцов меньше 11. См. Мой ответ для полного объяснения и тестового кода.
@b_levitt Most people are upvoting your answer because it's working code, they are not saying "I agree". Вы знаете это ... как?
Экстраполировано из комментариев за и против ответа GetOrdinal.
Я написал для пользователей 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 - это не то, что вы ищете.
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 () не генерирует исключений, этот код не имеет смысла. Вы будете ловить только исключения с нулевым указателем.
Если вы читаете вопрос, Майкл спрашивал о 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:

см. комментарии под ответом Мэтта
Что вы имеете в виду под DataRecord работает, но возвращает столбцы BS? Вы имеете в виду, что он работает (и дает неверные результаты)?
"Сделайте ваши объекты правильными". - но IDataReader реализует IDataRecord. Это разные интерфейсы одного и того же объекта - точно так же, как ICollection<T> и IEnumerable<T> - это разные интерфейсы List<T>. IDataReader позволяет перейти к следующей записи, а IDataRecord позволяет читать из текущей записи. Все методы, которые используются в этом ответе, поступают из интерфейса IDataRecord. См. stackoverflow.com/a/1357743/221708 для объяснения того, почему объявление параметра как IDataRecord предпочтительнее.
Проголосуйте за то, чтобы показать, почему r.GetSchemaTable().Columns - абсолютно неправильный ответ на этот вопрос.
GetName () наследуется от интерфейса IDataRecord в IDataReader. Ориентация на базовый интерфейс - это правильный код.
Вот рабочий образец идеи Жасмин:
var cols = r.GetSchemaTable().Rows.Cast<DataRow>().Select
(row => row["ColumnName"] as string).ToList();
if (cols.Contains("the column name"))
{
}
Только если обернуть вокруг него попытку / улов
Вы можете упростить эту идею с помощью: reader.GetSchemaTable (). Columns.Contains ("myFiled")
использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…
Этот код исправляет проблемы, которые были у 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(); :)
использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…
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, это будет намного менее эффективно в коде с высоким трафиком.
@ChadGrant, конечно, поэтому Linq one liner намного эффективнее, поскольку он выполняет только одну итерацию.
Чтобы ваш код оставался надежным и чистым, используйте одну функцию расширения, например:
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")
сравнение с учетом регистра ... почему?
Вот решение от Jasmine в одну строку ... (еще одно, хотя простое!):
reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;
использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…
@ChadGrant Возможно. Думаю, нужно выбирать с умом в зависимости от контекста и частоты, с которой необходимо это использовать ...
В вашей конкретной ситуации (все процедуры имеют одинаковые столбцы, кроме 1, в котором есть еще 1 столбец), будет лучше и быстрее проверить читателя. 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;
}
}
Следующее простое и сработало для меня:
bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. Проверьте источник github.com/microsoft/referencesource/blob/…
это работает для меня:
bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");
использование GetSchemaTable () является чрезмерным (с точки зрения распределения) для простого поиска имени столбца. И это реализовано не во всех версиях ядра dotnet. Проверьте источник github.com/microsoft/referencesource/blob/…
Хотя общедоступного метода нет, метод все же существует во внутреннем классе 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));
}
}
Внутренний код делает почти то же самое, что и мой ответ, без необходимости в этом странном отражении / делегате. Это кеширование поиска для каждого экземпляра объекта, что не принесет пользы, поскольку в реальном мире вы хотите кэшировать порядковые номера при первом запуске запроса и использовать этот кеш в течение всего времени существования приложения, а не создавать новый кеш для каждого запроса.
эта работа для меня
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:

Зигзаги здесь - частота отказов (столбец не найден) в пределах каждого столбца.
Для более узких наборов результатов хорошим выбором будет зацикливание. Однако метод 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; }
}
У вас явно есть какая-то странная одержимость исключениями. Лучшим подходом было бы просто кэшировать расположение столбца в статическом поиске для повышения производительности и использовать целочисленный поиск.
Другая проблема с использованием исключений в качестве потока управления заключается в том, что они отображаются в профилировщике как # исключений, созданных, когда в предложенном вами коде они являются преднамеренными ... а не исключениями. Не говоря уже о том, чтобы настроить ваш отладчик на прерывание при появлении исключений. По сути, сообщение об ошибках, которые не являются ошибками. Вы не должны этого делать.
Также есть счетчики для окончаний / сек и фильтров / сек. Это тоже плохо? Я бы назвал это возможным предостережением - первым из реальных, которые вы предоставили. Счетчики - это просто информация. Они ничего не значат, если не соответствуют проблеме с производительностью - и в этом случае я уже показал точку, в которой исключения имеют ЛУЧШУЮ производительность. Я также указал, что фреймворк и библиотеки уже создают множество исключений. У меня сейчас есть пример визуальной студии, выдающей 60 ex / s. Исключения не являются ошибками, если они не перехвачены.
Отличный анализ. Я использовал его результаты в своем новом ответе.
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;
}
}
}
Я собирался предложить то же самое, GetOrdinal великолепен, потому что поиск нечувствителен к регистру, а в случае неудачи он выполняет поиск с учетом регистра.