2009-09-28 6 views
120

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 retourne:Comment signaler une erreur d'une fonction définie par l'utilisateur SQL Server

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

Mais le fait est, la fonction prend une entrée, qui peut être invalide et, si elle est, il n'y a pas de valeur significative la fonction peut revenir. Qu'est-ce que je fais alors?

Je pourrais, bien sûr, renvoyer NULL, mais il serait difficile pour tout développeur utilisant la fonction pour 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. Y a-t-il un moyen de signaler mon propre message d'erreur?

Répondre

182

Vous pouvez utiliser CAST pour lancer une erreur significative:

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

Ensuite, Sql Server montrera 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. 
+1

OUI !!! Ça marche! C'est brillant! – EMP

+86

Bonne réponse, mais JEEZ veut pirater. > :( – JohnL4

+3

Pour une fonction inline-table-value où le RETURN est un select simple, cela seul ne fonctionne pas car rien n'est renvoyé - pas même null, et dans mon cas je voulais lancer une erreur quand rien n'a été trouvé Je ne voulais pas décomposer la fonction inline en multi-statuts pour des raisons évidentes de performance.J'ai plutôt utilisé votre solution plus ISNULL et MAX.La déclaration RETURN ressemble maintenant à ceci: SELECT ISNULL (MAX (E.EntityID) , CAST ('La recherche (' + @LookupVariable + ') n'existe pas.' As Int)) [EntityID] FROM Entité comme E WHERE E.Lookup = @ LookupVariable – MikeTeeVee

5

RAISEERROR ou @@ERROR ne sont pas autorisés dans les fonctions définies par l'utilisateur. Pouvez-vous transformer l'UDF en une procédure strored?

de l'article de Erland Sommarskog Error Handling in SQL Server – a Background:

User-defined functions are usually invoked as part of a SET, SELECT, INSERT, UPDATE or DELETE statement. What I have found is that if an error appears in a multi-statement table-valued function or in a scalar function, the execution of the function is aborted immediately, and so is the statement the function is part of. Execution continues on the next line, unless the error aborted the batch. In either case, @@error is 0. Thus, there is no way to detect that an error occurred in a function from T-SQL.

The problem does not appear with inline table-functions, since an inline table-valued function is basically a macro that the query processor pastes into the query.

You can also execute scalar functions with the EXEC statement. In this case, execution continues if an error occurs (unless it is a batch-aborting error). @@error is set, and you can check the value of @@error within the function. It can be problematic to communicate the error to the caller though.

-3

Une façon (un hack) est d'avoir une fonction/procédure stockée qui effectue une action non valide. Par exemple, les éléments suivants 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; 

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 quand 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.

+5

Idée intéressante, mais INSERT n'est pas autorisé dans une fonction, non plus. – EMP

13

L'astuce habituelle consiste à forcer une division de 0. Cela provoque une erreur et interrompt l'instruction en cours qui évalue la fonction. Si le développeur ou la personne de support est au courant de ce comportement, il est assez facile d'étudier et de résoudre le problème car l'erreur de division par 0 est interprétée comme le symptôme d'un problème différent et sans rapport.

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

3

Je pense que le moyen le plus propre est de simplement accepter que la fonction peut renvoyer NULL si des arguments non valides sont passés. Tant que cela est clairement documenté alors cela devrait être bon?

-- ============================================= 
-- 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 
     [email protected] AND 
     [email protected] AND 
     DateID=dbo.DateToIntegerKey(@OnDate) 

    RETURN @ClosingRate 

END 
GO 
7

À la suite de la réponse de Vladimir Korolev, l'idiome à certaines conditions lancer 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)  
3

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 pour cela dans son commentaire sur la réponse du haut, mais il a fallu utiliser une fonction d'agrégat comme MAX, ce qui ne fonctionnait pas bien pour mon cas.

Je foiré autour avec une solution de rechange pour le cas où vous avez besoin d'une valeur de table udf en ligne qui retourne quelque chose comme select * au lieu d'un agrégat. Exemple de code pour résoudre ce cas particulier est ci-dessous. Comme quelqu'un a déjà souligné ... "JEEZ wotta hack" :) Je salue toute meilleure solution pour cette affaire!

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 
+0

pour ceux qui lisent, je n'ai pas regardé les effets de performance potentiels ... je ne serais pas surpris si le hack union + déclaration de cas ralentit les choses ... – davec

2

Je ne peux pas commenter en fonction au sujet d'une valeur de table de réponse DaveC, mais à mon humble avis, c'est une solution plus facile:

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

Quelques gens demandaient à élever des erreurs dans les fonctions de table Précieuses , puisque vous ne pouvez pas utiliser "RETURN [cast invalide]" sorte de choses. Assigner la distribution 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 

Il en résulte:

Msg 245, Level 16, State 1, Line 14 Conversion failed when converting the varchar value 'booooom!' to data type int.

Questions connexes