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

EMP
источник

Ответы:

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)    
satnhak
источник
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
AndyM
источник
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
davec
источник
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.

NightShovel
источник
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, но стоит попробовать.

Alex
источник
5
Интересная идея, но INSERT также не разрешен в функции.
EMP