Cómo informar un error desde una función definida por el usuario de SQL Server

146

Estoy escribiendo una función definida por el usuario en SQL Server 2008. Sé que las funciones no pueden generar errores de la manera habitual; si intenta incluir la declaración RAISERROR, SQL devuelve:

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

Pero el hecho es que la función toma alguna entrada, que puede ser inválida y, si lo es, no hay un valor significativo que la función pueda devolver. ¿Qué hago entonces?

Podría, por supuesto, devolver NULL, pero sería difícil para cualquier desarrollador que use la función solucionar este problema. También podría causar una división por cero o algo así: esto generaría un mensaje de error, pero uno engañoso. ¿Hay alguna manera de que pueda informar mi propio mensaje de error de alguna manera?

EMP
fuente

Respuestas:

223

Puede usar CAST para arrojar un error significativo:

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

Luego SQL Server mostrará información de ayuda:

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error happened here.' to data type int.
Vladimir Korolev
fuente
112
Gran respuesta, pero JEEZ va a hackear. > :(
JohnL4
55
Para una función con valores de tabla en línea donde RETURN es una selección simple, esto solo no funciona porque no se devuelve nada, ni siquiera nulo, y en mi caso quería arrojar un error cuando no se encontraba nada. No quería dividir la función en línea en una de múltiples estados por razones obvias de rendimiento. En cambio, usé su solución más ISNULL y MAX. La declaración de RETORNO ahora se ve así: SELECCIONE ISNULL (MAX (E.EntityID), CAST ('The Lookup (' + @LookupVariable + ') no existe.' Como Int)) [EntityID] FROM Entity como E WHERE E. Lookup = @ LookupVariable
MikeTeeVee
Sí, puede arrojar un error, pero no parece que pueda arrojar un error condicionalmente. La función se ejecuta independientemente de la ruta del código.
satnhak
10
Gran solución, pero para aquellos que usan un TVF, esto no puede ser fácilmente parte de la devolución. Para aquellos:declare @error int; set @error = 'Error happened here.';
Tim Lehner,
20
Odio esto con el poder de mil soles ardientes. No hay otras opciones? Multa. Pero cielos ...
Remi Despres-Smyth
18

El truco habitual es forzar una división entre 0. Esto generará un error e interrumpirá la declaración actual que está evaluando la función. Si el desarrollador o la persona de soporte conocen este comportamiento, investigar y solucionar el problema es bastante fácil ya que el error de división por 0 se entiende como un síntoma de un problema diferente y no relacionado.

Por malo que parezca desde cualquier punto de vista, desafortunadamente el diseño de las funciones SQL en este momento no permite una mejor opción. El uso de RAISERROR debería estar absolutamente permitido en las funciones.

Remus Rusanu
fuente
7

Siguiendo la respuesta de Vladimir Korolev, el modismo para arrojar un error condicionalmente es

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
fuente
6

Creo que la forma más limpia es aceptar que la función puede devolver NULL si se pasan argumentos no válidos. Mientras esto esté claramente documentado, ¿debería estar bien?

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

RAISEERRORo @@ERRORno están permitidos en UDF. ¿Puedes convertir el UDF en un procedimiento almacenado?

Del artículo de Erland Sommarskog Manejo de errores en SQL Server: antecedentes :

Las funciones definidas por el usuario generalmente se invocan como parte de una instrucción SET, SELECT, INSERT, UPDATE o DELETE. Lo que he encontrado es que si aparece un error en una función con valores de tabla de varias instrucciones o en una función escalar, la ejecución de la función se cancela de inmediato, y también lo es la declaración de la que forma parte la función. La ejecución continúa en la siguiente línea, a menos que el error anule el lote. En cualquier caso, el error @@ es 0. Por lo tanto, no hay forma de detectar que ocurrió un error en una función de T-SQL.

El problema no aparece con las funciones de tabla en línea, ya que una función con valores de tabla en línea es básicamente una macro que el procesador de consultas pega en la consulta.

También puede ejecutar funciones escalares con la instrucción EXEC. En este caso, la ejecución continúa si ocurre un error (a menos que sea un error de aborto por lotes). Se establece @@ error, y puede verificar el valor de @@ error dentro de la función. Sin embargo, puede ser problemático comunicar el error a la persona que llama.

Trigo Mitch
fuente
4

La respuesta principal es generalmente la mejor, pero no funciona para funciones con valores de tabla en línea.

MikeTeeVee dio una solución para esto en su comentario sobre la respuesta principal, pero requirió el uso de una función agregada como MAX, que no funcionó bien para mi circunstancia.

Me equivoqué con una solución alternativa para el caso en el que necesita una tabla en línea con valor udf que devuelve algo como select * en lugar de un agregado. A continuación se muestra un código de muestra para resolver este caso en particular. Como alguien ya ha señalado ... "JEEZ wotta hack" :) ¡ Agradezco cualquier solución mejor 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
davec
fuente
1
para la lectura a nadie, yo no miro efectos potenciales de rendimiento ... No me sorprendería si la unión de los estados caso ralentiza truco + cosas ...
davec
4

Algunas personas estaban preguntando acerca de cómo generar errores en las funciones con valor de tabla, ya que no se puede usar el tipo de cosas " RETURN [conversión inválida] ". Asignar la conversión no válida a una variable funciona igual de bien.

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

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

RETURN

END

Esto resulta en:

Mensaje 245, Nivel 16, Estado 1, Línea 14 La conversión falló al convertir el valor varchar 'booooom!' al tipo de datos int.

NightShovel
fuente
2

No puedo comentar bajo la respuesta de davec con respecto a la función de valor de tabla, pero en mi humilde opinión, esta es la solución más 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
Michal Zglinski
fuente
-3

Una forma (un truco) es tener una función / procedimiento almacenado que realice una acción no válida. Por ejemplo, el siguiente 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;

Donde en la tabla tbl_throw_error, hay una restricción única en la columna err_msg. Un efecto secundario de esto (al menos en MySQL) es que el valor de err_msg se usa como la descripción de la excepción cuando vuelve a subir al objeto de excepción de nivel de aplicación.

No sé si puedes hacer algo similar con SQL Server, pero vale la pena intentarlo.

Alex
fuente
55
Idea interesante, pero INSERT tampoco está permitido en una función.
EMP