You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

读取存储过程结果行前获取总行数输出参数遇空引用异常求助

解决存储过程输出参数在DataReader关闭前读取的空引用异常问题

你遇到的问题核心在于SQL Server输出参数的返回时机——输出参数和返回值只有在整个批处理执行完成、所有结果集都被客户端读取完毕后,才会被服务器发送给客户端。当DataReader还处于打开状态时,@TotalRows参数的最终值还没被返回,此时读取它会得到null,自然触发"未将对象引用设置到对象实例"的异常。

下面给你两种可行的解决方案,你可以根据导出场景的实际需求选择:


方案1:读取完DataReader后再获取输出参数

这种方案不需要修改现有存储过程,只需要调整C#代码的执行顺序:先完整读取所有导出数据,再读取输出参数。

C#示例代码

using (SqlConnection conn = new SqlConnection("你的数据库连接字符串"))
{
    conn.Open();
    using (SqlCommand cmd = new SqlCommand("[dbo].[test]", conn))
    {
        cmd.CommandType = CommandType.StoredProcedure;
        
        // 配置输出参数
        var totalRowsParam = cmd.Parameters.Add("@TotalRows", SqlDbType.Int);
        totalRowsParam.Direction = ParameterDirection.Output;

        int exportedCount = 0;
        using (SqlDataReader reader = cmd.ExecuteReader())
        {
            // 循环读取所有导出数据
            while (reader.Read())
            {
                // 处理每行数据(比如写入文件、缓存等)
                exportedCount++;
                // 临时显示已导出数量:"已导出{exportedCount}行"
            }
        }

        // DataReader关闭后,才能正确获取输出参数的值
        int totalRows = (int)totalRowsParam.Value;
        Console.WriteLine($"导出完成:已导出{exportedCount}/{totalRows}行");
    }
}

为什么这个方案有效?

当你关闭DataReader后,客户端会通知服务器所有结果集已处理完毕,此时服务器才会把输出参数的最终值发送过来,你就能正常读取到@TotalRows的正确数值了。


方案2:用双结果集提前获取总行数(更适合进度显示)

如果你需要在导出开始前就显示完整进度(比如"已导出100/100000行"),可以修改存储过程,先返回总行数作为第一个结果集,再返回导出数据作为第二个结果集。这样你在处理数据前就能拿到总行数,实时更新进度。

修改后的存储过程

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[test]
AS
BEGIN
    SET NOCOUNT ON
    BEGIN TRANSACTION
    -- 执行插入操作(确保不重复导出的逻辑)
    -- 出错则回滚事务

    -- 第一步:先计算总行数,作为第一个结果集返回
    SELECT COUNT(*) AS TotalRows 
    FROM (
        -- 完全复用你原来的导出查询逻辑
        SELECT ... FROM ... WHERE ...
        UNION
        SELECT ... FROM ... WHERE ... GROUP BY ...
    ) AS TempResult

    -- 第二步:返回导出的数据
    SELECT ... FROM ... WHERE ...
    UNION
    SELECT ... FROM ... WHERE ... GROUP BY ...

    -- 执行更新操作
    -- 出错则回滚事务
    COMMIT TRANSACTION
END

对应的C#代码

using (SqlConnection conn = new SqlConnection("你的数据库连接字符串"))
{
    conn.Open();
    using (SqlCommand cmd = new SqlCommand("[dbo].[test]", conn))
    {
        cmd.CommandType = CommandType.StoredProcedure;

        int totalRows = 0;
        int exportedCount = 0;

        using (SqlDataReader reader = cmd.ExecuteReader())
        {
            // 读取第一个结果集:总行数
            if (reader.Read())
            {
                totalRows = reader.GetInt32(reader.GetOrdinal("TotalRows"));
                Console.WriteLine($"开始导出,共{totalRows}行");
            }

            // 切换到第二个结果集:导出数据
            reader.NextResult();

            // 实时处理数据并更新进度
            while (reader.Read())
            {
                // 处理每行数据
                exportedCount++;
                Console.WriteLine($"已导出{exportedCount}/{totalRows}行");
            }
        }

        Console.WriteLine($"导出完成:已导出{exportedCount}/{totalRows}行");
    }
}

注意事项

  • 总行数查询和导出数据都在同一个事务里,不会出现中间数据被修改导致总行数与实际导出数不一致的问题。
  • 如果导出查询非常复杂,COUNT(*)可能会带来一定性能开销,但合理的索引可以把这个影响降到最低。

额外提醒

不管用哪种方案,都要确保存储过程里的事务逻辑完善:任何步骤出错时都要正确回滚事务,避免数据不一致。

内容的提问来源于stack exchange,提问作者mulllhausen

火山引擎 最新活动