我如何检查看看一个列是否存在于一个SqlDataReader对象?在我的数据访问层,我创建了一个为多个存储过程调用构建相同对象的方法。其中一个存储过程具有其他存储过程不使用的附加列。我想修改方法以适应各种情况。
我的应用程序是用c#编写的。
我如何检查看看一个列是否存在于一个SqlDataReader对象?在我的数据访问层,我创建了一个为多个存储过程调用构建相同对象的方法。其中一个存储过程具有其他存储过程不使用的附加列。我想修改方法以适应各种情况。
我的应用程序是用c#编写的。
当前回答
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);
}
其他回答
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在性能方面也是过度消耗的,因为如果您查看源代码,它是一个相当繁重的函数。
如果您经常使用循环遍历字段,则可能会对性能造成较小的影响,您可能需要考虑缓存结果。
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; }
}
正确的代码是:
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;
}
下面的方法很简单,对我来说很有效:
bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
这段代码纠正了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;
}
获取所有那些无用的列名,而不是从表中获取列名的原因是…… 是因为您正在获取模式列的名称(即schema表的列名)
注意:这似乎只返回第一列的名称…
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
}