Проверьте имя столбца в объекте SqlDataReader
Как проверить, существует ли столбец в объекте SqlDataReader
? На уровне доступа к данным я создал метод, который создает один и тот же объект для нескольких вызовов хранимых процедур. Одна из хранимых процедур имеет дополнительный столбец, который не используется другими хранимыми процедурами. Я хочу изменить метод, чтобы приспособить его для каждого сценария.
Мое заявление написано на C#.
22 ответа:
Использование
Exception
s для логики управления, как и в некоторых других ответах, считается плохой практикой и имеет затраты на производительность.Циклическое перемещение по полям может иметь небольшое снижение производительности, если вы используете его много, и вы можете рассмотреть возможность кэширования результатов
Более подходящий способ сделать это:
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:
Я написал для пользователей 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)); } }