Как сообщить об ошибке из пользовательской функции SQL Server


146

Я пишу пользовательскую функцию в 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, но для любого разработчика было бы сложно устранить эту проблему. Я также мог бы вызвать деление на ноль или что-то в этом роде - это сгенерировало бы сообщение об ошибке, но вводящее в заблуждение Могу ли я каким-либо образом сообщать о своем сообщении об ошибке?

Ответы:


223

Вы можете использовать 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.

112
Отличный ответ, но JEEZ вот взломать. > :(
— JohnL4

5
Для функции inline-table-valueed-function, где RETURN является простым выбором, это само по себе не работает, потому что ничего не возвращается - даже ноль, и в моем случае я хотел выдать ошибку, когда ничего не было найдено. Я не хотел разбивать встроенную функцию на мультистатистическую по очевидным причинам производительности. Вместо этого я использовал ваше решение плюс ISNULL и MAX. Статус RETURN теперь выглядит следующим образом: SELECT ISNULL (MAX (E.EntityID), CAST ('Lookup (' + @LookupVariable + ') не существует.' Как Int)) [EntityID] FROM Entity как E, ГДЕ E. Lookup = @ LookupVariable
— MikeTeeVee

Да, вы можете выдать ошибку, но не похоже, что вы можете условно выдать ошибку. Функция выполняется независимо от пути кода.
— Сатнхак

10
Отличное решение, но для тех, кто использует TVF, это не может быть частью возврата. Для тех:declare @error int; set @error = 'Error happened here.';
— Тим Ленер

20
Я ненавижу это силой тысячи горящих солнц. Других вариантов нет? Хорошо. Но крипы ...
— Реми

18

Обычный трюк - форсировать деление на 0. Это вызовет ошибку и прервет текущий оператор, который оценивает функцию. Если разработчик или специалист службы поддержки знают об этом поведении, расследование и устранение неполадок достаточно просто, поскольку ошибка деления на 0 понимается как признак другой, не связанной проблемы.

Как бы плохо это ни выглядело с любой точки зрения, к сожалению, дизайн функций SQL на данный момент не дает лучшего выбора. Использование RAISERROR должно быть абсолютно разрешено в функциях.


7

Исходя из ответа Владимира Королева, условно выдать ошибку

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)    

6

Я думаю, что самый простой способ - просто принять, что функция может возвращать 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 в пошаговую процедуру?

Из статьи Эрланда Соммарского, Обработка ошибок в SQL Server - фон :

Пользовательские функции обычно вызываются как часть инструкций SET, SELECT, INSERT, UPDATE или DELETE. Я обнаружил, что если в многозначной табличной функции или в скалярной функции появляется ошибка, то выполнение функции немедленно прерывается, как и утверждение, частью которого является функция. Выполнение продолжается на следующей строке, если ошибка не прервала пакет. В любом случае @@ error равен 0. Таким образом, нет способа обнаружить, что произошла ошибка в функции из T-SQL.

Проблема не возникает с встроенными табличными функциями, так как встроенная табличная функция является в основном макросом, который обработчик запросов вставляет в запрос.

Вы также можете выполнять скалярные функции с помощью оператора EXEC. В этом случае выполнение продолжается, если возникает ошибка (если только это не ошибка прерывания пакета). @@ error установлен, и вы можете проверить значение @@ error в функции. Это может быть проблематично сообщить об ошибке вызывающей стороне, хотя.


4

Верхний ответ обычно лучший, но он не работает для встроенных табличных функций.

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

1
для тех, кто читает, я не смотрел на потенциальные эффекты производительности ... я не удивлюсь, если оператор hack union + case
— замедлит

4

Несколько человек спрашивали о возникновении ошибок в табличных функциях, так как вы не можете использовать вещи типа « 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.


2

Я не могу комментировать ответ 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

-3

Один из способов (взлом) - иметь функцию / хранимую процедуру, которая выполняет недопустимое действие. Например, следующий псевдо 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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.