在SqlDataReader对象中检查列名

240

如何检查一个SqlDataReader对象中是否存在一列?在我的数据访问层中,我创建了一个方法,为多个存储过程调用构建同一个对象。其中一个存储过程有一个其他存储过程没有使用的额外列。我想修改该方法以适应每种情况。

我的应用程序是用C#编写的。

27个回答

373
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;
    }
}

使用异常来控制逻辑,就像其他答案中所述,被认为是不良实践,并且有性能成本。它还会向抛出的异常和设置调试器以断开异常的任何人发送错误警报。
GetSchemaTable()也是许多答案中的建议。这不是检查字段存在的首选方法,因为它在所有版本中都没有实现(在某些dotnetcore版本中抛出NotSupportedException)。 GetSchemaTable在性能上也过于重型,如果你查看源代码的话。
循环遍历字段可能会对性能产生轻微影响,如果您经常使用它,您可能需要考虑缓存结果。

1
使用异常流程是否是不良实践存在争议。曾经认为它相对于其他运算符而言代价过高,但在连接的应用程序中可以忽略不计。Skeet在2006年测量了每毫秒40-118个异常,具体取决于堆栈深度。https://stackoverflow.com/a/891230/852208。此外,如果没有测试,这段代码实际上可能会更慢,因为其平均情况下检查了一半的列(尽管在连接到数据库的应用程序中仍然微不足道)。我将编辑此答案,只包括中间段落,因为其他两个是观点。 - b_levitt
8
@b_levitt,这是不容置疑的,这是糟糕的代码,您不应该依赖异常来控制程序流程。 - Chad Grant
3
最终,我要说的就是坚持事实。留下你最受欢迎的代码片段,但不要加入观点-这不是S.O.的目的。大多数人会投赞成票是因为它是有效的代码,而不是表示“我同意”。但是当一个没有经验的人看到“那是不好的做法”的评论时,误解了几百个赞成票为“我同意”,现在我必须花费额外的时间来调试代码,因为有人编写了一个失败时返回false的DoSomething方法,但忘记了用该死的“if”进行检查。 - b_levitt
2
@b_levitt “大多数人点赞你的回答是因为它是可行的代码,而不是表示‘我同意’。” 你怎么知道的? - LarsTech
根据对GetOrdinal答案的赞成和反对评论推断而来。 - b_levitt
显示剩余9条评论

66

正确的代码是:

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; 
}

5
@Jasmine:我说得太早了!你的代码检查模式表中的一列,而不是你的结果集。你需要将“field”(假设“field”是列名称)与每行的“ColumnName”字段的值进行比较。找到时退出循环,如果找不到则返回false。 - Steve J
4
@Steve J: 在GetSchemaTable返回的结果集中,什么情况下不会有某一列呢? - Bless Yahu
1
对于其他有疑惑的人,这种方法是行不通的。请参考下面的答案,从模式表中检索ColumnName行并使用它。 - Jason Jackson
5
是的,这个方法不起作用。是谁给它投了这么多赞?如果这个答案不存在的话,以后我的调试时间就可以省下很多了! - c00000fd
1
@Jasmine 他们都有效吗?实际上并不是。请删除你回答的第一部分。我本来可以自己做的,但是因为你最后的评论! - nawfal
显示剩余9条评论

42

在你的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));

1
你正在多次枚举字段名称/分配另一个数组以进行包含扫描,在高流量代码中这将导致性能下降。 - Chad Grant
@ChadGrant 当然,这就是为什么 Linq 一行代码更有效率的原因,因为它只执行了一次迭代。 - Larry
这个很好用,而且简单易懂。也没有太大的性能损失。 - Michael
1
这两个选项(fieldNames.ContainsEnumerable.Any)都是线性时间。因此,如果您想检查n列,则必须遍历数组n^2次。将结果保存在哈希集中会更有效率,因为它具有常数时间查找。 - Oleksiy

35

我认为最好的方法是在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")可以使用此提供程序字段的内部实现来检查每个列的名称别名是否符合您要查找的名称。
在这种假设的“别名列”情况下,尝试/GetOrdinal/catch方法将是确保检查结果集中每个列名称变化的唯一方法。
脆弱?当然。但值得思考。老实说,我更喜欢在IDataRecord上有一个“官方”的HasColumn方法。

17
使用异常来控制逻辑?不不不。 - Chad Grant
30
当我最初发布这个问题时,有一件小事情被大家忽略了...我在12/8/08提出了问题,而Matt在12/17/08回答了我的问题。当时所有人都对捕获控制逻辑的异常提出批评,但直到5/1/09之前没有人提供一个实质性的替代方案。这就是为什么它最初被标记为答案的原因。我今天仍在使用这个解决方案。 - Michael Kniskern
22
仅当该列不存在时,这将对性能产生影响。其他描述的方法每次都会有性能损失,而且是更大的性能损失。虽然通常不建议避免使用异常处理来控制流程,但在考虑其是否适用于您的情况之前,不应排除此解决方案。 - Nick Harrison
7
作为广泛的设计原则,“不要使用异常作为控制逻辑”的做法我认可。它并不意味着“不惜一切代价避免使用异常”。正如@Nick所说,这种情况有一个被很好记录的解决方法,而且性能问题(如果有的话)只会在该列不存在时出现。 - Larry
2
我开始理解为什么这个答案会被踩。当阅读 https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exception-throwing 时,它概述了“如果可能的话,请勿将异常用于正常的控制流”。这篇文章是关于抛出异常的。如果在列未找到的情况下执行类似于 throw FalseException 的操作,那么这个示例中的代码才会违反该规则。 - b_levitt
显示剩余11条评论

18

这是一个Jasmin想法的可行示例:

var cols = r.GetSchemaTable().Rows.Cast<DataRow>().Select
    (row => row["ColumnName"] as string).ToList(); 

if (cols.Contains("the column name"))
{

}

1
只有在它周围包裹try/catch时才能这样做。 - Donald.Record
你可以用以下方式简化这个想法:reader.GetSchemaTable().Columns.Contains("myFiled") - Lev Z
仅仅为了查找列名,使用GetSchemaTable()是过度的(在分配方面)。请查看源代码 https://github.com/microsoft/referencesource/blob/24a17c0c3b23ddeac0926acc13b13c54b650d3be/System.Data/System/Data/SqlClient/SqlDataReader.cs#L422 - Chad Grant

14

这对我有用:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");

1
仅为查找列名而使用GetSchemaTable()是过度的(在分配方面)。而且它并没有在所有版本的dotnet core中实现。请查看源代码 https://github.com/microsoft/referencesource/blob/24a17c0c3b23ddeac0926acc13b13c54b650d3be/System.Data/System/Data/SqlClient/SqlDataReader.cs#L422 - Chad Grant

13

以下方法简单易行, 对我有用:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);

仅仅为了查找列名,使用GetSchemaTable()是过度的(在分配方面)。请查看源代码 https://github.com/microsoft/referencesource/blob/24a17c0c3b23ddeac0926acc13b13c54b650d3be/System.Data/System/Data/SqlClient/SqlDataReader.cs#L422 - Chad Grant

10
我为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

9

如果您仔细看问题,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 列:

在数据读取器中调用 GetSchemeTable


1
请查看Matt的答案下面的评论。 - nawfal
1
“DataRecord does work, but it returns BS columns”这句话的意思是什么?你是说它能运行(但返回错误结果)吗? - nawfal
2
把你的对象搞对。但是 IDataReader 实现了 IDataRecord。它们是同一个对象的不同接口,就像 ICollection<T>IEnumerable<T>List<T> 的不同接口一样。IDataReader 允许前进到下一条记录,而 IDataRecord 允许从当前记录中读取。在这个答案中使用的方法都来自 IDataRecord 接口。请参见 https://dev59.com/_nM_5IYBdhLWcg3wfTK7#1357743,了解为什么将参数声明为 IDataRecord 更可取。 - Daniel Schilling
点赞,因为它清楚地说明了r.GetSchemaTable().Columns是这个问题的完全错误的答案。 - Daniel Schilling
GetName()是从IDataRecord接口继承到IDataReader中的。针对基础接口进行编码是正确的代码。 - Chad Grant
不应该使用异常来控制流程。这是糟糕的设计。 - Dave Black

6

简述:

有很多回答声称自己有更好的性能和更好的实践方法,所以我在这里澄清一下。

对于返回列数较多的情况,异常处理方法更快,而对于返回列数较少的情况,循环方法更快,在大约11列时它们会交叉。请滚动到底部查看图表和测试代码。

完整回答:

一些顶级答案的代码确实有效,但是关于“更好”的答案是否接受异常处理的逻辑及其相关性能存在争议。

为了解决这个问题,我认为没有太多关于捕获异常的指导。微软确实提供了一些指导关于抛出异常。他们在那里说:

如果可能的话,不要使用异常进行正常的控制流程。

第一个注释是“如果可能的话”的宽容度。更重要的是,这个描述给出了这个上下文:

框架设计者应该设计API,使用户可以编写不会抛出异常的代码。这意味着,如果您正在编写一个API,可能会被其他人使用,请为他们提供一种无需try/catch即可导航异常的能力。例如,使用抛出异常的Parse方法提供TryParse。但是,这并不意味着您不应该捕获异常。 此外,正如另一位用户指出的那样,catch始终允许按类型进行过滤,并且最近通过when子句允许进一步过滤。如果我们不打算使用它们,这似乎是一种浪费语言功能。
可以说,抛出异常有一些成本,而这个成本可能会影响重循环中的性能。然而,也可以说,在“连接的应用程序”中,异常的成本将是微不足道的。实际成本在十多年前就已经调查过了:C#中的异常有多昂贵? 换句话说,连接和查询数据库的成本可能会使抛出异常的成本相形见绌。
除此之外,我想确定哪种方法真正更快。如预期的那样,没有确切的答案。
任何循环遍历列的代码都会随着列数的增加而变慢。也可以说,任何依赖于异常的代码都会因查询无法找到的速率而变慢。
结合Chad GrantMatt Hamilton的答案,我使用两种方法运行了最多20列和50%的错误率(OP表示他在不同存储过程之间进行了这两个测试,所以我假设至少有两个)。
以下是结果,使用LINQPad绘制:

Results - Series 1 is Loop, 2 is Exception

这里的“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; }


}

2
你显然对异常有一些奇怪的迷恋。更好的方法是为了性能,只需在静态查找中缓存列位置并使用整数查找。 - Chad Grant
在使用异常作为控制流程时,另一个问题是它们会显示在分析器中作为抛出的异常数量,而在您建议的代码中它们是有意的……不是异常。更不用说将调试器设置为在抛出异常时中断。本质上会报告不是错误的错误。你不应该这样做。 - Chad Grant
1
还有 finallys/sec 和 filters/sec 的计数器。这些也不好吗?我称之为一个可能的警告 - 你提供的第一个真正的警告。计数器只是信息。除非它们对应于性能问题,否则它们没有任何意义 - 在这种情况下,我已经展示了异常具有更好的性能的点。我还指出,框架和库已经抛出了很多异常。我现在有一个视觉工作室抛出60个ex/s的实例。除非未捕获,否则异常不是错误。 - b_levitt
很棒的分析。我在我的新答案中使用了它的结果。 - yazanpro
(示例代码格式不一致。) - Peter Mortensen
@PeterMortensen - 详细说明一下,我可以修复。 - b_levitt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接