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

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


当前回答

如果你读了这个问题,Michael问的是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()。DataReader上的Columns返回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

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


}

以下是Jasmin想法的工作示例:

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

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

}

下面的方法很简单,对我来说很有效:

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

我认为你最好的办法是在你的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方法。