我如何检查看看一个列是否存在于一个SqlDataReader对象?在我的数据访问层,我创建了一个为多个存储过程调用构建相同对象的方法。其中一个存储过程具有其他存储过程不使用的附加列。我想修改方法以适应各种情况。
我的应用程序是用c#编写的。
我如何检查看看一个列是否存在于一个SqlDataReader对象?在我的数据访问层,我创建了一个为多个存储过程调用构建相同对象的方法。其中一个存储过程具有其他存储过程不使用的附加列。我想修改方法以适应各种情况。
我的应用程序是用c#编写的。
当前回答
TLDR:
有很多关于性能和不良实践的说法,所以我在这里澄清一下。
对于返回的列数较多的情况,异常路由更快,对于返回的列数较低的情况,循环路由更快,交叉点在11列左右。滚动到底部以查看图形和测试代码。
完整的回答:
一些顶级答案的代码可以工作,但是这里存在一个潜在的争论,即基于在逻辑中接受异常处理及其相关性能的“更好的”答案。
为了澄清这一点,我不认为有太多关于捕获异常的指导。微软确实有一些关于抛出异常的指导。他们写道:
如果可能,不要对正常的控制流使用异常。
第一个注意事项是“如果可能”的宽大。更重要的是,描述给出了以下上下文:
框架设计者应该设计api,这样用户就可以编写不抛出异常的代码
这意味着,如果你正在编写一个可能被其他人使用的API,让他们能够在不使用try/catch的情况下导航异常。例如,使用抛出异常的Parse方法提供TryParse。但是这里并没有说不应该捕获异常。
而且,正如另一个用户指出的那样,catch一直允许按类型进行过滤,最近还允许通过when子句进行进一步过滤。这似乎是对语言特性的浪费,如果我们不应该使用它们的话。
可以说,抛出异常是有代价的,而这种代价可能会影响重循环中的性能。然而,也可以说异常的代价在“连接应用程序”中是可以忽略不计的。实际成本在十多年前就已经调查过了:c#中的异常有多昂贵?
换句话说,数据库连接和查询的成本可能会使抛出异常的成本相形见绌。
除此之外,我还想确定哪种方法确实更快。不出所料,没有具体的答案。
任何遍历列的代码都会随着列数的增加而变慢。也可以说,任何依赖于异常的代码都会变慢,这取决于查找查询失败的速率。
使用Chad Grant和Matt Hamilton的答案,我运行了两种方法,最多有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; }
}
其他回答
我认为你最好的办法是在你的DataReader上预先调用GetOrdinal("columnName"),并在列不存在的情况下捕获indexoutofranceexception。
实际上,让我们创建一个扩展方法:
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)(在Chad的回答中使用)只能返回单个字符串,因此它必须只返回列上的一个“别名”。但是,GetOrdinal(“EmpName”)可以使用该提供程序字段的内部实现来检查每个列的别名,以查找您要查找的名称。
在这种假设的“别名列”情况下,try/GetOrdinal/catch方法将是确保检查结果集中列名的每个变体的唯一方法。
脆弱的吗?当然。但值得一想。老实说,我更希望在IDataRecord上有一个“官方的”HasColumn方法。
对于这个简单的问题,我建议使用try{} catch{}。但是,我不建议在catch中处理异常。
try
{
if (string.IsNullOrEmpty(reader["Name"].ToString()))
{
name = reader["Name"].ToString();
}
}
catch
{
//Do nothing
}
你也可以调用GetSchemaTable()在你的DataReader上,如果你想要列的列表,你不想得到一个异常…
在一行中,在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));
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;
}
}
在控制逻辑中使用异常被认为是不好的做法,并且会带来性能损失。它还会将抛出的# exceptions错误消息发送给分析器,并帮助任何人设置调试器在抛出异常时中断。
GetSchemaTable()也是许多答案中的另一个建议。这不是检查字段是否存在的首选方法,因为它不是在所有版本中都实现的(它是抽象的,并且在dotnetcore的某些版本中抛出NotSupportedException)。GetSchemaTable在性能方面也是过度消耗的,因为如果您查看源代码,它是一个相当繁重的函数。
如果您经常使用循环遍历字段,则可能会对性能造成较小的影响,您可能需要考虑缓存结果。