web-dev-qa-db-fra.com

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

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. Qu'est-ce que je fais alors?

Bien sûr, je pourrais renvoyer NULL, mais il serait difficile pour tout développeur utilisant cette 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. Existe-t-il un moyen de signaler mon propre message d'erreur?

141
EMP

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.
212
Vladimir Korolev

L'astuce habituelle consiste à forcer une division par 0. Cela provoquera une erreur et interrompra l'instruction en cours qui évalue la fonction. Si le développeur ou le support technique est informé de ce problème, il est relativement facile d'analyser et de résoudre le problème, car l'erreur de division par 0 est comprise comme le symptôme d'un problème différent, non lié.

Aussi mauvais que cela puisse paraître, le design des fonctions SQL ne permet malheureusement pas de meilleur choix. L'utilisation de RAISERROR doit absolument être autorisée dans les fonctions.

15
Remus Rusanu

Suite à la réponse de Vladimir Korolev, l’idiome de jeter 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)    
7
briantyler

Je pense que le moyen le plus propre est simplement d’accepter que la fonction puisse renvoyer NULL si des arguments non valides sont passés. Aussi longtemps que cela est clairement documenté, cela devrait aller.

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

RAISEERROR ou @@ERROR ne sont pas autorisés dans les FDU. Pouvez-vous transformer le fichier UDF en procédure hiérarchisée?

Extrait de l'article d'Erland Sommarskog Traitement des erreurs dans SQL Server - un arrière-plan :

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 multi-instruction ou dans une fonction scalaire, l’exécution de la fonction est immédiatement interrompue, de même que l’instruction à laquelle appartient la fonction. L'exécution continue sur la ligne suivante, sauf si l'erreur a annulé le lot. Dans les deux cas, @@ error vaut 0. Il n'y a donc aucun moyen de détecter une erreur dans une fonction de T-SQL.

Le problème n'apparaît pas avec les fonctions table inline, puisqu’une fonction table inline est fondamentalement une macro que le processeur de requête colle dans la requête.

Vous pouvez également exécuter des fonctions scalaires avec l'instruction EXEC. Dans ce cas, l'exécution continue si une erreur survient (sauf s'il s'agit d'une erreur d'annulation 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.

5
Mitch Wheat

La meilleure réponse est généralement préférable, mais ne fonctionne pas pour les fonctions à valeur 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égat telle que MAX, qui ne fonctionnait pas bien dans mon cas.

J'ai déconné avec une autre solution pour le cas où vous avez besoin d'une table inline de valeur udf qui retourne quelque chose comme , sélectionnez * d'un agrégat. Vous trouverez ci-dessous un exemple de code permettant de résoudre ce cas particulier. Comme quelqu'un l'a déjà fait remarquer ... "JEEZ wotta hack" :) Je me réjouis de toute solution meilleure 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
4
davec

Quelques personnes se demandaient comment générer des erreurs dans les fonctions Table-Valued, car vous ne pouvez pas utiliser le type "RETURN [conversion invalide]". L'affectation de la distribution non valide à une variable fonctionne également.

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, Etat 1, Ligne 14 La conversion a échoué lors de la conversion de la valeur varchar 'booooom!' au type de données int.

4
NightShovel

Je ne peux pas commenter sous la réponse de davec concernant la fonction table value, mais à mon humble avis, cette solution est 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
2
Michal Zglinski