【问题标题】:SQL ExecuteScalar doesn't throw exception when insert used with output clause当插入与输出子句一起使用时,SQL ExecuteScalar 不会引发异常
【发布时间】:2015-06-18 11:53:27
【问题描述】:

我在 SqlCommand 对象的“ExecuteScalar()”方法中遇到了一个有趣的问题。

我有一个存储过程,它在表中创建一条新记录。它包含一个“插入”语句,其中有一个“输出”子句(因为创建的记录利用了一个标识列和一些默认值)

这很好用,“ExecuteScalar”返回标识列值(第 1 行,第 1 列)

但是,在测试中,我故意调用了我的存储过程两次,第二次预计会在数据表中遇到唯一约束并失败 (在SP).

到目前为止一切顺利,除了第二次调用返回一个空行集(我可以验证我是否从 SQLMgr 运行它)并且 ExecuteScalar 返回一个空值 - 没有不要抛出异常。

这不是预期的,给我留下了一个有趣的问题。如何使用 ExecuteScalar - 为了在成功的情况下返回 id 列值 - 但如果发生错误,它会失败吗?

我知道,如果我从“插入”中删除“输出”子句,则会按预期抛出异常。

如果这是预期的行为,我应该如何调用我的 SP,以便返回 id 值,并在我的约束被击中时引发异常?

很明显,我可以在存储过程中做各种诡计多端的操作(比如使用 scope_identity 插入然后选择),并且 - 正如我在测试中所做的那样 - 我可以检查 null返回并进行后续的 ExecuteNonQuery 调用(确实会引发异常)。但是,考虑到“输出”子句的用处,这一切似乎都是捏造的。

我做了一些“谷歌搜索”,并找到了对 ExecuteScalar“吃”异常的引用,但这只是一个传递引用。

想法?

-- 编辑 1--

我的意思是,在进一步调查编写下面的“简单案例”后,(显然)如果我有一个存储过程,它有一个“try catch”和一个违反唯一约束的插入,当通过 ExecuteScalar 调用,我没有收到异常。

  • 如果我删除“try/catch/throw” - 我会得到异常。

  • 如果我删除“输出” - 我会得到异常

-- 编辑 2--

这里有一些示例(这是非常减少的......是的,我使用“try/catch”来跳过其他功能:

表格如下:

CREATE TABLE [dbo].[test]
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [value] [varchar](10) NOT NULL,
    [description] [varchar](100) NOT NULL,
    CONSTRAINT [PK_test_1] PRIMARY KEY CLUSTERED ([id] ASC) ON [PRIMARY]
) ON [PRIMARY]
END
GO
CREATE UNIQUE NONCLUSTERED INDEX [UNQ_test_description] ON [dbo].[test]([description] ASC) ON [PRIMARY]
GO

SP 看起来像这样:

create procedure dbo.fred
(
    @Code           varchar(10),
    @Description    varchar(100)
)
as
begin
    begin try

        insert into dbo.test
            (value, [description])
        output
            inserted.ID
        values
            (@Code, @Description)

    end try
    begin catch
        throw
    end catch
end

【问题讨论】:

  • 尝试添加一个最小示例来编译您所看到的问题。你越容易为每个人检查这个,你就会越快得到帮助:)
  • 您是说在一个 ExecuteScalar 方法中执行相同的过程两次?请注意,ExecuteScalar 会忽略除第一个结果集的第一列之外的所有内容。这意味着这将吃掉返回标量值的语句之后的后续异常。

标签: sql-server stored-procedures exception-handling


【解决方案1】:

根据 Dan 的建议,我编写了一个强类型扩展方法,虽然不能直接替代 ExecuteScalar(),但已经足够满足我的需要了。

这个扩展的两个主要优点是生成的标量值是强类型的,并且,如果在执行命令时发生异常 - 导致一些数据或没有数据 - 所有结果都会按顺序遍历来检测异常。一个缺点是,如果没有返回结果,标量结果现在可能不会为空(因为泛型类型可能不可为空(即 Int32))

就我而言,这很好,因为我要么得到结果,要么得到异常。我不会得到(通过设计) 是单个值,位于大量结果集的第一行,然后是异常。如果我们认为 ExecuteScalar 应该只对第一行的第一列感兴趣,我的扩展将 (我怀疑) 强制遍历所有结果 (这可能是large) 直到读取所有数据和/或遇到异常。如果您没有预料到,性能影响可能会引起关注。就我而言,我插入一行并返回该行,从中取出第一列。

不完美,但我要解决的问题是,如果命令引发异常,我想知道 - 无论它是否已经决定要返回的结果。

我列出了扩展名以备不时之需:

public static class Extensions
{
    /// <summary>
    /// Executes the query, and returns the first column of the first row in the result set returned by the query.
    /// </summary>
    /// <remarks>
    /// This is here because SqlCommand.ExecuteScalar() can, under some circumstances, fail to propagate an exception raised by the command.
    /// </remarks>
    static public T ExecuteScalar<T>(this SqlCommand command)
    {
        T result = default(T);

        using (IDataReader reader = command.ExecuteReader())
        {
            if (reader.Read())
            {
                var value = reader.GetValue(0);

                try
                {
                    result = (T)Convert.ChangeType(value, typeof(T));
                }
                catch (Exception ex)
                {
                    throw new FormatException(String.Format("Unable to convert scalar value of \"{0}\" to type {1}.", value, typeof(T)),ex);
                }
            }

            while (reader.NextResult()) ;

            return result;
        }
    }

【讨论】:

    【解决方案2】:

    ExecuteScalar 返回第一行的第一列并丢弃可能包含异常的剩余结果。对于OUTPUT 子句,INSERT 上的错误,以及 SQL TRY/CATCH,首先返回空单列结果,然后是丢弃的异常。

    如果您想要更多控制权,可以直接使用 ExecuteReader 方法。无论如何,ExecuteScalar 及其 ExecuteNonQuery 表亲基本上只是 ExecuteReader 的包装器。

    var r = command.ExecuteReader();
    if (r.Read())
        result = r.GetInt32(0);
    else
        r.NextResult();
    r.Close();
    

    【讨论】:

    • 有趣的是它会丢弃异常,因为插入失败,异常比空结果更相关。鉴于上述情况,我很难了解如何编写目标 SP,ExecuteScalar 在您进行插入并想知道第一行/第一列值的情况下是一个不错的选择(因为是通常情况下,该值是一个标识值)。
    • @BlackLight,我个人不是 ExecuteScalar 的忠实粉丝。除了您在此处遇到的OUTPUT 子句问题之外,返回值不是强类型的。该异常的根本问题是使用流式 TDS 协议返回结果。结果后面出现异常,但 ExecuteScalar 丢弃了除第一行之外的所有内容。您也可能在多语句过程中遇到此问题,其中 SELECT 语句位于其他 DML 之前。
    • 嗨,丹,不,我不是忠实粉丝——现在。我编写了一个通用扩展来解决“强类型”问题,并引发异常,但没有仔细考虑,因此它总能找到任何异常。我想我必须获得第一行/列的值,然后遍历所有以下结果以检查是否有扩展名。没什么大不了的,但有点烦人,因为当我不想想知道异常时,我看不到这种情况。当我有机会完成它时,我会发布它作为回复(著名的 IT 说没有 57)。
    • 哇。多年来一直这样做,从不知道这一点。并不是说我使用 ExecuteScalar 那么多。谢谢。
    • 对于未来的读者,我发现这一点是因为我在我的代码中放入了一个临时/假语句。 SELECT 1/0 as PurposelyInducedError PurposelyInducedError 实际上是 ExecuteScalar 看到的并忽略了实际的“遇到的除以零错误”。错误。我以为我快疯了。下一条评论中的完整示例。
    猜你喜欢
    • 2023-03-28
    • 2018-11-24
    • 2018-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多