检查SqlDataReader对象中的列名称

Mic*_*ern 204 .net c# sqldatareader

如何检查SqlDataReader对象中是否存在列?在我的数据访问层中,我创建了一个方法,为多个存储过程调用构建相同的对象.其中一个存储过程具有另一个列,其他存储过程不使用该列.我想修改方法以适应每个场景.

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

Cha*_*ant 322

Exception在某些其他答案中使用s作为控制逻辑被认为是不好的做法并且具有性能成本.

如果你经常使用它,那么在字段中循环会有很小的性能损失,你可能想要考虑缓存结果

更合适的方法是:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @b_levitt,这是没有争议的,它是垃圾代码,你不应该依赖异常来控制流 (4认同)
  • 最后,我所说的只是坚持事实。留下您投票最高的代码片段。但请放弃意见——这不是 SO 的目的。大多数人都赞成你的答案,因为它是工作代码,他们并没有说“我同意”。但后来有一个没有经验的人出现并读到“这是不好的做法”,将几百个赞成票误解为“我同意”,现在我不得不花费额外的时间来调试代码,这导致错误 5 行太晚了,因为有人写了 DoSomething方法失败时返回 false,但忘记用该死的“if”进行检查。 (2认同)
  • @b_levitt`大多数人都赞成你的答案,因为它是工作代码,他们并没有说“我同意”。`你知道这一点......怎么知道? (2认同)

Jas*_*ine 64

使用这个布尔函数要好得多:

r.GetSchemaTable().Columns.Contains(field)
Run Code Online (Sandbox Code Playgroud)

一个电话 - 没有例外.它可能会在内部抛出异常,但我不这么认为.

注意:在下面的评论中,我们想出了......正确的代码实际上是这样的:

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; 
}
Run Code Online (Sandbox Code Playgroud)

  • @Jasmine:我说得太快了!您的代码检查架构表中的列,而不是结果集.您需要将"field"(假设"field"是列名)与每行的"ColumnName"字段的值进行比较.当你找到它时休息,如果你没有,则返回false. (5认同)
  • @Steve J:结果集什么时候在GetSchemaTable中没有列? (4认同)
  • 是的,这不起作用.谁投了这么多次??? 如果这个答案不在这里,它会为我节省大量的调试时间! (3认同)

Mat*_*ton 32

我认为你最好的选择是在你的DataReader上预先调用GetOrdinal("columnName"),并在列不存在时捕获IndexOutOfRangeException.

实际上,让我们做一个扩展方法:

public static bool HasColumn(this IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑

好吧,这篇文章最近开始收集一些下注,我不能删除它,因为它是公认的答案,所以我要更新它,并且(我希望)试图证明使用异常处理是合理的.控制流.

Chad Grant发布的另一种实现方法是循环访问DataReader中的每个字段,并对您要查找的字段名称进行不区分大小写的比较.这将非常有效,并且真实地可能比我上面的方法表现得更好.当然,我绝不会在性能问题的循环中使用上面的方法.

我可以想到一种情况,其中try/GetOrdinal/catch方法将在循环不起作用的情况下工作.然而,这是一个完全假设的情况,所以这是一个非常脆弱的理由.无论如何,忍受我,看看你的想法.

想象一个允许您在表中"别名"列的数据库.想象一下,我可以使用名为"EmployeeName"的列定义一个表,但也给它一个别名"EmpName",并且对任一名称执行select会返回该列中的数据.和我一起到目前为止?

现在想象一下,该数据库有一个ADO.NET提供程序,并且它们为它编写了一个IDataReader实现,它将列别名考虑在内.

现在,dr.GetName(i)(在乍得的答案使用)只能返回一个字符串,所以它必须只返回一个列上的"别名"的.但是,GetOrdinal("EmpName")可以使用此提供程序字段的内部实现来检查每个列的别名,以查找您要查找的名称.

在这个假设的"别名列"情况下,try/GetOrdinal/catch方法将是确保您在结果集中检查列名称的每个变体的唯一方法.

劣质的?当然.但值得一想.老实说,我更倾向于IDataRecord上的"官方"HasColumn方法.

  • 当我最初发布这个问题时,每个人都会忽略一件小事......我在2008年8月8日问了这个问题,Matt在2008年12月17日发布了他的回答.每个人都对捕获控制逻辑的异常感到不好,但直到2009年5月1日才提供可靠的替代解决方案.这就是为什么它最初被标记为答案.我今天仍在使用此解决方案. (27认同)
  • 只有当列不存在时,才会有性能损失.所描述的其他方法每次都会受到性能影响,并且会有更大的性能影响.虽然避免对控制流使用异常处理通常是不好的做法,但是如果不首先考虑它是否适用于您的情况,则不应排除此解决方案. (19认同)
  • 使用控制逻辑的异常?不不不 (14认同)
  • +1.我对"不要将控制逻辑的异常使用"作为一个广泛的设计规则感到满意.它并不意味着"不惜一切代价避免它".答案是一个记录很好的解决方法,正如@Nick所说,性能命中(如果有的话)只在列不存在时才会出现. (5认同)
  • 使用异常作为控制逻辑,根据我的经验,调试也变得更加麻烦。您必须取消选中“公共语言运行时异常”上的“引发”,然后当您收到真正的异常时,它可能在某个地方的处理程序中中断,而不是在出现问题的行中。 (2认同)
  • 我开始理解为什么这个答案被否决了。当阅读https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exception- throwing时,它有这样的总结:“如果可能的话,不要将异常用于正常的控制流”这篇文章是关于抛出异常。仅当此示例中的代码在未找到列的情况下执行抛出 FalseException 之类的操作时,才会违反该规则。 (2认同)

Lar*_*rry 27

在一行中,在DataReader检索后使用它:

var fieldNames = Enumerable.Range(0, dr.FieldCount).Select(i => dr.GetName(i)).ToArray();
Run Code Online (Sandbox Code Playgroud)

然后,

if (fieldNames.Contains("myField"))
{
    var myFieldValue = dr["myField"];
    ...
Run Code Online (Sandbox Code Playgroud)

编辑

更高效的单行程,不需要加载模式:

var exists = Enumerable.Range(0, dr.FieldCount).Any(i => string.Equals(dr.GetName(i), fieldName, StringComparison.OrdinalIgnoreCase));
Run Code Online (Sandbox Code Playgroud)


小智 18

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

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

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

}
Run Code Online (Sandbox Code Playgroud)


小智 12

这对我有用:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");
Run Code Online (Sandbox Code Playgroud)


小智 10

以下内容很简单,对我有用:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
Run Code Online (Sandbox Code Playgroud)


Lev*_*kon 8

如果您阅读了这个问题,Michael会询问DataReader,而不是DataRecord的人.让你的对象正确.

使用r.GetSchemaTable().Columns.Contains(field)on DataRecord确实有效,但它返回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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

    public static bool CanCreate(SqlDataReader dataReader)
    {
        return dataReader.ContainsColumn("RoleTemplateId") 
            && !dataReader.IsDBNull("RoleTemplateId");
    }
Run Code Online (Sandbox Code Playgroud)

调用r.GetSchemaTable().ColumnsDataReader返回BS列:

在DataReader中调用GetSchemeTable

  • “正确摆放你的物品。” -但是`IDataReader`实现了`IDataRecord`。它们是同一对象的不同接口-就像`ICollection &lt;T&gt;`和`IEnumerable &lt;T&gt;`是List &lt;T&gt;`的不同接口一样。IDataReader允许前进到下一条记录,而IDataRecord允许从当前记录读取。此答案中使用的方法全部来自“ IDataRecord”接口。请参阅/sf/answers/95042041/,以获取有关为什么最好将参数声明为“ IDataRecord”的说明。 (2认同)

小智 7

我为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
Run Code Online (Sandbox Code Playgroud)

我认为这更强大,用法是:

If HasColumnAndValue(reader, "ID_USER") Then
    Me.UserID = reader.GetDecimal(reader.GetOrdinal("ID_USER")).ToString()
End If
Run Code Online (Sandbox Code Playgroud)


b_l*_*itt 5

域名注册地址:

有很多关于性能和不良做法的说法的答案,所以我在这里澄清一下。

返回的列数越多,异常路由越快,列数越少,循环路由越快,交叉点大约为 11 列。滚动到底部以查看图表和测试代码。

完整答案:

一些顶级答案的代码有效,但基于对逻辑中异常处理及其相关性能的接受程度,此处存在关于“更好”答案的潜在争论。

为了清除这一点,我认为没有太多关于捕获异常的指导。Microsoft 确实有一些关于抛出异常的指导。他们确实在那里声明:

如果可能,不要在正常的控制流中使用异常。

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

框架设计者应该设计 API 以便用户可以编写不会抛出异常的代码

这意味着,如果您正在编写可能被其他人使用的 API,请让他们能够在没有 try/catch 的情况下导航异常。例如,为抛出异常的 Parse 方法提供 TryParse。 尽管这并没有说明您不应该捕获异常。

此外,正如另一位用户指出的那样,捕获一直允许按类型过滤,并且最近允许通过when 子句进一步过滤。如果我们不应该使用它们,这似乎是对语言功能的浪费。

可以说抛出异常是代价的,这个代价可能会影响重循环中的性能。但是,也可以说在“连接的应用程序”中,异常的成本可以忽略不计。十多年前调查了实际成本:C# 中的异常有多昂贵?

换句话说,连接和查询数据库的成本可能比抛出异常的成本相形见绌。

除此之外,我想确定哪种方法真正更快。正如预期的那样,没有具体的答案。

随着列数的增加,任何在列上循环的代码都会变慢。也可以说,任何依赖于异常的代码都会根据查询失败的速度变慢。

根据Chad GrantMatt Hamilton的答案,我运行了两种方法,最多 20 列,错误率高达 50%(OP 表示他在不同的存储过程之间使用这两个测试,所以我假设只有两个)。

以下是使用LINQPad绘制的结果:

结果 - 系列 1 是循环,2 是异常

此处的锯齿形是每个列计数内的故障率(未找到列)。

在较窄的结果集上,循环是一个不错的选择。但是,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; }


}
Run Code Online (Sandbox Code Playgroud)

  • 你显然对例外有某种奇怪的痴迷。更好的方法只是在静态查找中缓存列位置以提高性能并使用整数查找 (2认同)