如何检查一个SqlDataReader
对象中是否存在一列?在我的数据访问层中,我创建了一个方法,为多个存储过程调用构建同一个对象。其中一个存储过程有一个其他存储过程没有使用的额外列。我想修改该方法以适应每种情况。
我的应用程序是用C#编写的。
如何检查一个SqlDataReader
对象中是否存在一列?在我的数据访问层中,我创建了一个方法,为多个存储过程调用构建同一个对象。其中一个存储过程有一个其他存储过程没有使用的额外列。我想修改该方法以适应每种情况。
我的应用程序是用C#编写的。
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;
}
}
异常
来控制逻辑,就像其他答案中所述,被认为是不良实践,并且有性能成本。它还会向抛出的异常和设置调试器以断开异常的任何人发送错误警报。正确的代码是:
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;
}
在你的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));
fieldNames.Contains
和Enumerable.Any
)都是线性时间。因此,如果您想检查n列,则必须遍历数组n^2次。将结果保存在哈希集中会更有效率,因为它具有常数时间查找。 - Oleksiy我认为最好的方法是在Datareader上提前调用GetOrdinal("columnName"),并在列不存在的情况下捕获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)
(如Chad的答案中所使用)只能返回一个字符串,因此它必须仅返回列上的一个“别名”之一。但是,GetOrdinal("EmpName")
可以使用此提供程序字段的内部实现来检查每个列的名称别名是否符合您要查找的名称。这是一个Jasmin想法的可行示例:
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);
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
如果您仔细看问题,Michael是问DataReader的问题,而不是DataRecord。请正确使用对象。
在DataRecord上使用r.GetSchemaTable().Columns.Contains(field)
确实可以工作,但它会返回无用的列(如下图所示)。
要查看数据列是否存在并且在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: https://dev59.com/znRC5IYBdhLWcg3wOOP1#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
返回 BS 列:
IDataReader
实现了 IDataRecord
。它们是同一个对象的不同接口,就像 ICollection<T>
和 IEnumerable<T>
是 List<T>
的不同接口一样。IDataReader
允许前进到下一条记录,而 IDataRecord
允许从当前记录中读取。在这个答案中使用的方法都来自 IDataRecord
接口。请参见 https://dev59.com/_nM_5IYBdhLWcg3wfTK7#1357743,了解为什么将参数声明为 IDataRecord
更可取。 - Daniel Schillingr.GetSchemaTable().Columns
是这个问题的完全错误的答案。 - Daniel Schilling简述:
有很多回答声称自己有更好的性能和更好的实践方法,所以我在这里澄清一下。
对于返回列数较多的情况,异常处理方法更快,而对于返回列数较少的情况,循环方法更快,在大约11列时它们会交叉。请滚动到底部查看图表和测试代码。
完整回答:
一些顶级答案的代码确实有效,但是关于“更好”的答案是否接受异常处理的逻辑及其相关性能存在争议。
为了解决这个问题,我认为没有太多关于捕获异常的指导。微软确实提供了一些指导关于抛出异常。他们在那里说:
如果可能的话,不要使用异常进行正常的控制流程。
第一个注释是“如果可能的话”的宽容度。更重要的是,这个描述给出了这个上下文:
框架设计者应该设计API,使用户可以编写不会抛出异常的代码。这意味着,如果您正在编写一个API,可能会被其他人使用,请为他们提供一种无需try/catch即可导航异常的能力。例如,使用抛出异常的Parse方法提供TryParse。但是,这并不意味着您不应该捕获异常。 此外,正如另一位用户指出的那样,catch始终允许按类型进行过滤,并且最近通过when子句允许进一步过滤。如果我们不打算使用它们,这似乎是一种浪费语言功能。这里的“zigzags”是指每列计数内的故障率(未找到列)
对于较窄的结果集,使用循环是一个不错的选择。然而,“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; }
}