【问题标题】:How to report an error from a SQL Server user-defined function如何从 SQL Server 用户定义函数报告错误
【发布时间】:2010-12-01 21:14:41
【问题描述】:

我正在 SQL Server 2008 中编写用户定义函数。我知道函数不能以通常的方式引发错误 - 如果您尝试包含 RAISERROR 语句 SQL 返回:

Msg 443, Level 16, State 14, Procedure ..., Line ...
Invalid use of a side-effecting operator 'RAISERROR' within a function.

但事实是,该函数需要一些输入,这可能是无效的,如果是,则该函数无法返回有意义的值。那我该怎么办?

当然,我可以返回 NULL,但任何使用该函数的开发人员都很难解决此问题。我也可能导致除以零或类似的东西 - 这会产生错误消息,但会产生误导。有什么方法可以让我以某种方式报告自己的错误消息?

【问题讨论】:

    标签: sql sql-server tsql sql-server-2008 user-defined-functions


    【解决方案1】:

    您可以使用 CAST 抛出有意义的错误:

    create function dbo.throwError()
    returns nvarchar(max)
    as
    begin
        return cast('Error happened here.' as int);
    end
    

    然后Sql Server会显示一些帮助信息:

    Msg 245, Level 16, State 1, Line 1
    Conversion failed when converting the varchar value 'Error happened here.' to data type int.
    

    【讨论】:

    • 很好的答案,但是 JEEZ wotta hack。 >:(
    • 对于 RETURN 是简单选择的内联表值函数,仅此一项不起作用,因为没有返回任何内容 - 甚至不是 null,在我的情况下,我想抛出一个错误当什么都没找到。出于明显的性能原因,我不想将内联函数分解为多语句。相反,我使用了您的解决方案加上 ISNULL 和 MAX。 RETURN 语句现在看起来像这样:SELECT ISNULL(MAX(E.EntityID), CAST('The Lookup (' + @LookupVariable + ') does not exist.' as Int))[EntityID] FROM Entity as E WHERE E. Lookup = @LookupVariable
    • 很好的解决方案,但对于那些使用 TVF 的人来说,这不能轻易成为回报的一部分。对于那些:declare @error int; set @error = 'Error happened here.';
    • 如果你把它放在一个列的 CASE 语句中,在 TVF 中也很有效,或者你可以使它成为多语句 TVF,非常酷的解决方案!
    • 我讨厌这个拥有一千个燃烧太阳的力量。没有其他选择?美好的。但是绉纱......
    【解决方案2】:

    通常的技巧是强制除以 0。这将引发错误并中断正在评估函数的当前语句。如果开发人员或支持人员知道此行为,则调查和解决问题相当容易,因为除以 0 错误被理解为不同的、不相关问题的症状。

    从任何角度来看,这看起来很糟糕,不幸的是,目前 SQL 函数的设计没有更好的选择。在函数中绝对应该允许使用 RAISERROR。

    【讨论】:

      【解决方案3】:

      从 Vladimir Korolev 的回答开始,有条件地抛出错误的成语是

      CREATE FUNCTION [dbo].[Throw]
      (
          @error NVARCHAR(MAX)
      )
      RETURNS BIT
      AS
      BEGIN
          RETURN CAST(@error AS INT)
      END
      GO
      
      DECLARE @error NVARCHAR(MAX)
      DECLARE @bit BIT
      
      IF `error condition` SET @error = 'My Error'
      ELSE SET @error = '0'
      
      SET @bit = [dbo].[Throw](@error)    
      

      【讨论】:

        【解决方案4】:

        我认为最干净的方法是接受如果传递了无效参数,函数可以返回 NULL。只要有明确的记录,就可以了?

        -- =============================================
        -- Author: AM
        -- Create date: 03/02/2010
        -- Description: Returns the appropriate exchange rate
        -- based on the input parameters.
        -- If the rate cannot be found, returns NULL
        -- (RAISEERROR can't be used in UDFs)
        -- =============================================
        ALTER FUNCTION [dbo].[GetExchangeRate] 
        (
            @CurrencyFrom char(3),
            @CurrencyTo char(3),
            @OnDate date
        )
        RETURNS decimal(18,4)
        AS
        BEGIN
        
          DECLARE @ClosingRate as decimal(18,4)
        
            SELECT TOP 1
                @ClosingRate=ClosingRate
            FROM
                [FactCurrencyRate]
            WHERE
                FromCurrencyCode=@CurrencyFrom AND
                ToCurrencyCode=@CurrencyTo AND
                DateID=dbo.DateToIntegerKey(@OnDate)
        
            RETURN @ClosingRate 
        
        END
        GO
        

        【讨论】:

          【解决方案5】:

          RAISEERROR@@ERROR 不允许在 UDF 中使用。你能把UDF变成一个存储过程吗?

          来自 Erland Sommarskog 的文章Error Handling in SQL Server – a Background

          用户定义的函数通常是 作为 SET、SELECT、 INSERT、UPDATE 或 DELETE 语句。 我发现如果出现错误 出现在多条语句中 表值函数或标量 函数,执行 函数立即中止,并且 函数的语句也是如此 部分。继续执行 下一行,除非错误中止 批次。在任何一种情况下,@@error 都是 0. 因此,无法检测到函数中发生了错误 来自 T-SQL。

          问题没有出现 内联表函数,因为 内联表值函数是 基本上是一个宏,查询 处理器粘贴到查询中。

          您还可以执行标量函数 与 EXEC 语句。在这种情况下, 如果发生错误则继续执行 (除非它是批处理中止错误)。 @@error 已设置,您可以检查 函数内@@error 的值。 沟通可能有问题 但是给调用者的错误。

          【讨论】:

            【解决方案6】:

            最佳答案通常是最好的,但不适用于内联表值函数。

            MikeTeeVee 在他对最佳答案的评论中为此提供了解决方案,但它需要使用像 MAX 这样的聚合函数,这对我的情况并不适用。

            如果您需要一个值 udf 的内联表,它返回类似 select * 之类的东西,而不是聚合,我搞砸了另一种解决方案。解决这种特殊情况的示例代码如下。正如有人已经指出的那样...... “JEEZ wotta hack” :) 我欢迎任何更好的解决方案!

            create table foo (
                ID nvarchar(255),
                Data nvarchar(255)
            )
            go
            
            insert into foo (ID, Data) values ('Green Eggs', 'Ham')
            go
            
            create function dbo.GetFoo(@aID nvarchar(255)) returns table as return (
                select *, 0 as CausesError from foo where ID = @aID
            
                --error checking code is embedded within this union
                --when the ID exists, this second selection is empty due to where clause at end
                --when ID doesn't exist, invalid cast with case statement conditionally causes an error
                --case statement is very hack-y, but this was the only way I could get the code to compile
                --for an inline TVF
                --simpler approaches were caught at compile time by SQL Server
                union
            
                select top 1 *, case
                                    when ((select top 1 ID from foo where ID = @aID) = @aID) then 0
                                    else 'Error in GetFoo() - ID "' + IsNull(@aID, 'null') + '" does not exist'
                                end
                from foo where (not exists (select ID from foo where ID = @aID))
            )
            go
            
            --this does not cause an error
            select * from dbo.GetFoo('Green Eggs')
            go
            
            --this does cause an error
            select * from dbo.GetFoo('Yellow Eggs')
            go
            
            drop function dbo.GetFoo
            go
            
            drop table foo
            go
            

            【讨论】:

            • 对于任何阅读的人,我没有考虑潜在的性能影响...如果 hack union + case 语句减慢速度,我不会感到惊讶...
            【解决方案7】:

            一些人询问有关在表值函数中引发错误的问题,因为您不能使用“RETURN [invalid cast]”之类的东西。将无效强制转换分配给变量也可以。

            CREATE FUNCTION fn()
            RETURNS @T TABLE (Col CHAR)  
            AS
            BEGIN
            
            DECLARE @i INT = CAST('booooom!' AS INT)  
            
            RETURN
            
            END
            

            这会导致:

            消息 245,第 16 级,状态 1,第 14 行 转换 varchar 值 'booooom!' 时转换失败数据类型为 int。

            【讨论】:

              【解决方案8】:

              我无法就 davec 关于表值函数的回答发表评论,但在我看来这是更简单的解决方案:

              CREATE FUNCTION dbo.ufn_test (@a TINYINT)
              RETURNS @returns TABLE(Column1 VARCHAR(10), Value1 TINYINT)
              BEGIN
                  IF @a>50 -- if @a > 50 - raise an error
                  BEGIN
                    INSERT INTO @returns (Column1, Value1)
                    VALUES('error','@a is bigger than 50!') -- reminder Value1 should be TINYINT
                  END
              
                  INSERT INTO @returns (Column1, Value1)
                  VALUES('Something',@a)
                  RETURN;
              END
              
              SELECT Column1, Value1 FROM dbo.ufn_test(1) -- this is okay
              SELECT Column1, Value1 FROM dbo.ufn_test(51) -- this will raise an error
              

              【讨论】:

                【解决方案9】:

                一种方法(hack)是拥有一个执行无效操作的函数/存储过程。比如下面的伪SQL

                create procedure throw_error ( in err_msg varchar(255))
                begin
                insert into tbl_throw_error (id, msg) values (null, err_msg);
                insert into tbl_throw_error (id, msg) values (null, err_msg);
                end;
                

                在表 tbl_throw_error 上,列 err_msg 上存在唯一约束。这样做的一个副作用(至少在 MySQL 上)是 err_msg 的值被用作异常的描述,当它返回到应用程序级异常对象时。

                我不知道你是否可以用 SQL Server 做类似的事情,但值得一试。

                【讨论】:

                • 有趣的想法,但在函数中也不允许 INSERT。
                猜你喜欢
                • 2011-07-02
                • 1970-01-01
                • 1970-01-01
                • 2018-06-30
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多