Como relatar um erro de uma função definida pelo usuário do SQL Server


146

Estou escrevendo uma função definida pelo usuário no SQL Server 2008. Sei que as funções não podem gerar erros da maneira usual - se você tentar incluir a instrução RAISERROR, o SQL retornará:

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

Mas o fato é que a função recebe alguma entrada, que pode ser inválida e, se for, não há valor significativo que a função possa retornar. O que eu faço então?

É claro que eu poderia retornar NULL, mas seria difícil para qualquer desenvolvedor que utilize a função solucionar esse problema. Eu também poderia causar uma divisão por zero ou algo assim - isso geraria uma mensagem de erro, mas enganosa. Existe alguma maneira de eu ter minha própria mensagem de erro relatada de alguma forma?

Respostas:


223

Você pode usar o CAST para gerar um erro significativo:

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

Em seguida, o Sql Server mostrará algumas informações de ajuda:

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

112
Ótima resposta, mas o JEEZ não quer invadir. > :(
JohnL4

5
Para uma função com valor de tabela embutida, em que RETURN é uma seleção simples, isso por si só não funciona porque nada é retornado - nem mesmo nulo; no meu caso, eu queria gerar um erro quando nada fosse encontrado. Eu não queria dividir a função embutida em uma de várias declarações por razões óbvias de desempenho. Em vez disso, usei sua solução mais ISNULL e MAX. A declaração RETURN agora fica assim: SELECT ISNULL (MAX (E.EntityID), CAST ('A pesquisa (' + @LookupVariable + ') não existe.' Como Int)) [EntityID] FROM Entidade como E ONDE E. Lookup = @ LookupVariable
MikeTeeVee

Sim, você pode gerar um erro, mas não parece que você possa gerar um erro condicional. A função é executada independentemente do caminho do código.
satnhak

10
Ótima solução, mas para aqueles que estão usando um TVF, isso não pode ser facilmente parte do retorno. Para aqueles:declare @error int; set @error = 'Error happened here.';
Tim Lehner

20
Eu odeio isso com o poder de mil sóis ardentes. Não há outras opções? Bem. Mas cripes ...
Remi Despres-Smyth

18

O truque usual é forçar uma divisão por 0. Isso gera um erro e interrompe a instrução atual que está avaliando a função. Se o desenvolvedor ou a pessoa de suporte souber desse comportamento, investigar e solucionar o problema é bastante fácil, pois o erro de divisão por 0 é entendido como sintoma de um problema diferente e não relacionado.

Por pior que isso pareça de qualquer ponto de vista, infelizmente o design das funções SQL no momento não permite uma melhor escolha. O uso de RAISERROR deve ser absolutamente permitido em funções.


7

Seguindo a resposta de Vladimir Korolev, o idioma para gerar um erro condicionalmente é

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

Eu acho que a maneira mais limpa é apenas aceitar que a função possa retornar NULL se argumentos inválidos forem passados. Enquanto isso estiver claramente documentado, tudo ficará bem?

-- =============================================
-- 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

RAISEERRORou @@ERRORnão são permitidos em UDFs. Você pode transformar o UDF em um procedimento estressado?

Do artigo de Erland Sommarskog, Tratamento de erros no SQL Server - um histórico :

As funções definidas pelo usuário geralmente são chamadas como parte de uma instrução SET, SELECT, INSERT, UPDATE ou DELETE. O que descobri é que, se um erro aparecer em uma função com valor de tabela com várias instruções ou em uma função escalar, a execução da função será abortada imediatamente e a instrução da qual a função faz parte. A execução continua na próxima linha, a menos que o erro interrompa o lote. Nos dois casos, @@ error é 0. Portanto, não há como detectar se ocorreu um erro em uma função do T-SQL.

O problema não aparece com funções de tabela embutidas, pois uma função com valor de tabela embutida é basicamente uma macro que o processador de consultas cola na consulta.

Você também pode executar funções escalares com a instrução EXEC. Nesse caso, a execução continua se ocorrer um erro (a menos que seja um erro de interrupção de lote). @@ error está definido e você pode verificar o valor de @@ error dentro da função. Pode ser problemático comunicar o erro ao chamador.


4

A resposta principal é geralmente melhor, mas não funciona para funções com valor de tabela embutida.

MikeTeeVee deu uma solução para isso em seu comentário sobre a resposta principal, mas exigiu o uso de uma função agregada como MAX, que não funcionou bem para a minha circunstância.

Eu brinquei com uma solução alternativa para o caso em que você precisa de uma tabela embutida com valor udf que retorne algo como select * em vez de um agregado. O código de exemplo que resolve este caso específico está abaixo. Como alguém já apontou ... "JEEZ wotta hack" :) Congratulo-me com qualquer solução melhor para este caso!

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
para a leitura ninguém, eu não olhar para os efeitos potenciais de desempenho ... eu não ficaria surpreso se a união + instrução case corte atrasa as coisas ...
davec

4

Algumas pessoas estavam perguntando sobre como gerar erros nas funções com valor de tabela, pois você não pode usar o tipo de coisa " RETURN [conversão inválida] ". Atribuir a conversão inválida a uma variável também funciona.

CREATE FUNCTION fn()
RETURNS @T TABLE (Col CHAR)  
AS
BEGIN

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

RETURN

END

Isto resulta em:

Mensagem 245, nível 16, estado 1, linha 14 Falha na conversão ao converter o valor varchar 'booooom!' para o tipo de dados int.


2

Não posso comentar na resposta da davec sobre a função com valor de tabela, mas, na minha humilde opinião, essa é a solução mais fácil:

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

Uma maneira (um hack) é ter uma função / procedimento armazenado que executa uma ação inválida. Por exemplo, o seguinte pseudo 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;

Onde na tabela tbl_throw_error, há uma restrição exclusiva na coluna err_msg. Um efeito colateral disso (pelo menos no MySQL) é que o valor de err_msg é usado como descrição da exceção quando ele retorna ao objeto de exceção no nível do aplicativo.

Não sei se você pode fazer algo semelhante com o SQL Server, mas vale a pena tentar.


5
Idéia interessante, mas INSERT também não é permitido em uma função.
EMP
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.