如何从SQL Server用户定义的函数中报告错误?

165

我正在编写一个在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,但对于使用该函数的任何开发人员来说,这将很难进行故障排除。我也可以造成除以零等类似错误-这将生成一个错误消息,但是会导致误导。是否有任何方法可以报告自己的错误消息?

9个回答

260

您可以使用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.

145
很好的答案,但是天哪,太糟糕了。>:( - JohnL4
5
对于一个返回行内表值的函数,如果其 RETURN 语句只是简单的 SELECT,那这样是不起作用的,因为没有返回任何东西,甚至不是 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。 - MikeTeeVee
13
非常好的解决方案,但对于那些正在使用 TVF 的人来说,这很难成为返回结果的一部分。 对于这些人: declare @error int; set @error = '在这里发生了错误.'; - Tim Lehner
1
如果您将此代码放入一个CASE语句中作为列的一部分,或者将其制作成多语句TVF,则可以在TVF中很好地工作,这是非常棒的解决方案! - Chris Schaller
32
我非常憎恨这件事,就像千万个灼热的太阳一样。没有其他选择吗?好吧,但是天哪... - Remi Despres-Smyth
显示剩余3条评论

22

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

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


10

继续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)    

9
一些人在询问如何在表值函数中引发错误,因为您不能使用“RETURN [invalid cast]”之类的语句。将无效转换赋值给变量同样有效。
CREATE FUNCTION fn()
RETURNS @T TABLE (Col CHAR)  
AS
BEGIN

DECLARE @i INT = CAST('booooom!' AS INT)  

RETURN

END

这会导致以下结果:

Msg 245, Level 16, State 1, Line 14 在将 varchar 值 'booooom!' 转换为数据类型 int 时转换失败。


这个技巧被用在了一个多语句 TVF 中(将有意的无法转换为字符串的变量设置为一个变量)。当在没有打开事务的连接中调用带有导致错误的参数的 TVF 时,它可以正常工作。但是,当使用那些崩溃参数调用 TVF 时,会抛出另一个与此无关的错误:“当前事务无法提交,也无法支持写入日志文件的操作”。希望这些建议对您有所帮助。 - Mauricio Ortega

8

我认为最简洁的方式是接受函数在传递无效参数时可能返回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

7

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这样的聚合函数,这在我的情况下效果不佳。

我尝试了一种替代解决方案,适用于需要返回类似select *而不是聚合的行内表值UDF的情况。下面是解决此特定情况的示例代码。正如有人指出的那样..."天哪,这是个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

1
对于任何阅读此内容的人,我没有考虑潜在的性能影响...如果 hack union + case 语句减慢了速度,我也不会感到惊讶... - davec

4

我无法在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

当错误条件提前知道并且可以将其硬编码到代码中时,您的解决方案效果更好(例如@a>50)。但是它无法处理动态错误条件(例如,如果以后需要在表中使用超过50个值怎么办?)- 我原来的解决方案没有硬编码的错误条件,但有那个丑陋的union - 可能混合使用最好。使用select not exists检查错误并立即返回(或使用NightShovel答案的强制错误转换方法)。如果情况看起来不错,则返回实际有效的表。避免通过两次查询来查询联合的性能损失。 - davec

-3
一种方法(一个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上做类似的事情,但值得一试。


5
有趣的想法,但在函数中是不允许使用INSERT的。 - EMP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接