Comment signaler une erreur à partir d'une fonction définie par l'utilisateur SQL Server

146

J'écris une fonction définie par l'utilisateur dans SQL Server 2008. Je sais que les fonctions ne peuvent pas générer d'erreurs de la manière habituelle - si vous essayez d'inclure l'instruction RAISERROR, SQL renvoie:

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

Mais le fait est que la fonction prend une entrée, qui peut être invalide et, si c'est le cas, il n'y a pas de valeur significative que la fonction peut renvoyer. Que dois-je faire alors?

Je pourrais, bien sûr, retourner NULL, mais il serait difficile pour tout développeur utilisant la fonction de résoudre ce problème. Je pourrais aussi provoquer une division par zéro ou quelque chose comme ça - cela générerait un message d'erreur, mais trompeur. Est-il possible que je puisse faire rapporter mon propre message d'erreur d'une manière ou d'une autre?

EMP
la source

Réponses:

223

Vous pouvez utiliser CAST pour générer une erreur significative:

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

Ensuite, Sql Server affichera des informations d'aide:

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error happened here.' to data type int.
Vladimir Korolev
la source
112
Excellente réponse, mais JEEZ veut pirater. > :(
JohnL4
5
Pour une fonction de table en ligne où RETURN est une simple sélection, cela seul ne fonctionne pas car rien n'est retourné - même pas nul, et dans mon cas, je voulais lancer une erreur lorsque rien n'a été trouvé. Je ne voulais pas décomposer la fonction en ligne en une fonction multi-états pour des raisons évidentes de performances. Au lieu de cela, j'ai utilisé votre solution plus ISNULL et MAX. La déclaration RETURN ressemble maintenant à ceci: SELECT ISNULL (MAX (E.EntityID), CAST ('The Lookup (' + @LookupVariable + ') does not exist.' As Int)) [EntityID] FROM Entity as E WHERE E. Lookup = @ LookupVariable
MikeTeeVee
Oui, vous pouvez générer une erreur, mais il ne semble pas que vous puissiez générer une erreur de manière conditionnelle. La fonction est exécutée quel que soit le chemin du code.
satnhak
10
Excellente solution, mais pour ceux qui utilisent un TVF, cela ne peut pas facilement faire partie du retour. Pour ceux:declare @error int; set @error = 'Error happened here.';
Tim Lehner
20
Je déteste cela avec la puissance de mille soleils brûlants. Pas d'autres options? Bien. But cripes ...
Remi Despres-Smyth
18

L'astuce habituelle consiste à forcer une division par 0. Cela soulèvera une erreur et interrompra l'instruction courante qui évalue la fonction. Si le développeur ou la personne de support connaît ce comportement, il est assez facile d'enquêter et de résoudre le problème, car l'erreur de division par 0 est considérée comme le symptôme d'un problème différent et non lié.

Aussi mauvais que cela puisse paraître à tout point de vue, malheureusement, la conception des fonctions SQL pour le moment ne permet pas de meilleur choix. L'utilisation de RAISERROR doit absolument être autorisée dans les fonctions.

Remus Rusanu
la source
7

Suite à la réponse de Vladimir Korolev, l'idiome pour lancer conditionnellement une erreur est

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
la source
6

Je pense que le moyen le plus propre est d'accepter simplement que la fonction puisse retourner NULL si des arguments non valides sont passés. Tant que cela est clairement documenté, cela devrait être correct?

-- =============================================
-- 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
la source
5

RAISEERRORou @@ERRORne sont pas autorisés dans les UDF. Pouvez-vous transformer l'UDF en une procédure stricte?

De l'article d'Erland Sommarskog Gestion des erreurs dans SQL Server - un contexte :

Les fonctions définies par l'utilisateur sont généralement appelées dans le cadre d'une instruction SET, SELECT, INSERT, UPDATE ou DELETE. Ce que j'ai trouvé, c'est que si une erreur apparaît dans une fonction table à plusieurs instructions ou dans une fonction scalaire, l'exécution de la fonction est immédiatement interrompue, de même que l'instruction dont la fonction fait partie. L'exécution se poursuit sur la ligne suivante, sauf si l'erreur a annulé le lot. Dans les deux cas, @@ error vaut 0. Ainsi, il n'y a aucun moyen de détecter qu'une erreur s'est produite dans une fonction de T-SQL.

Le problème n'apparaît pas avec les fonctions de table en ligne, car une fonction de table en ligne est essentiellement une macro que le processeur de requêtes colle dans la requête.

Vous pouvez également exécuter des fonctions scalaires avec l'instruction EXEC. Dans ce cas, l'exécution se poursuit en cas d'erreur (à moins qu'il ne s'agisse d'une erreur d'abandon de lot). @@ error est défini et vous pouvez vérifier la valeur de @@ error dans la fonction. Cependant, il peut être problématique de communiquer l'erreur à l'appelant.

Blé Mitch
la source
4

La meilleure réponse est généralement la meilleure, mais ne fonctionne pas pour les fonctions de table en ligne.

MikeTeeVee a donné une solution à cela dans son commentaire sur la première réponse, mais cela nécessitait l'utilisation d'une fonction d'agrégation comme MAX, qui ne fonctionnait pas bien pour ma situation.

J'ai déconné avec une solution alternative pour le cas où vous avez besoin d'une table en ligne valorisée udf qui renvoie quelque chose comme select * au lieu d'un agrégat. Un exemple de code résolvant ce cas particulier est présenté ci-dessous. Comme quelqu'un l'a déjà souligné ... "JEEZ wotta hack" :) Je salue toute meilleure solution pour ce cas!

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
la source
1
pour tous ceux qui lisent, je n'ai pas regardé les effets potentiels sur les performances ... je ne serais pas surpris si la déclaration hack union + case ralentissait les choses ...
davec
4

Quelques personnes ont posé des questions sur le déclenchement d'erreurs dans les fonctions table, car vous ne pouvez pas utiliser le genre de choses " RETURN [cast non valide] ". L'affectation du cast invalide à une variable fonctionne aussi bien.

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

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

RETURN

END

Cela se traduit par:

Msg 245, niveau 16, état 1, ligne 14 La conversion a échoué lors de la conversion de la valeur varchar 'booooom!' au type de données int.

Pelle de nuit
la source
2

Je ne peux pas commenter la réponse de davec concernant la fonction de table, mais à mon humble avis, c'est une solution plus simple:

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
la source
-3

Une façon (un hack) est d'avoir une fonction / procédure stockée qui effectue une action non valide. Par exemple, le pseudo SQL suivant

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;

Où sur la table tbl_throw_error, il y a une contrainte unique sur la colonne err_msg. Un effet secondaire de ceci (au moins sur MySQL), est que la valeur de err_msg est utilisée comme description de l'exception lorsqu'elle revient dans l'objet d'exception au niveau de l'application.

Je ne sais pas si vous pouvez faire quelque chose de similaire avec SQL Server, mais ça vaut le coup.

Alex
la source
5
Idée intéressante, mais INSERT n'est pas non plus autorisé dans une fonction.
EMP le