So melden Sie einen Fehler von einer benutzerdefinierten SQL Server-Funktion

146

Ich schreibe eine benutzerdefinierte Funktion in SQL Server 2008. Ich weiß, dass Funktionen nicht auf die übliche Weise Fehler auslösen können - wenn Sie versuchen, die RAISERROR-Anweisung einzuschließen, die SQL zurückgibt:

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

Tatsache ist jedoch, dass die Funktion einige Eingaben benötigt, die möglicherweise ungültig sind, und wenn dies der Fall ist, gibt es keinen aussagekräftigen Wert, den die Funktion zurückgeben kann. Was mache ich dann?

Ich könnte natürlich NULL zurückgeben, aber es wäre für jeden Entwickler schwierig, die Funktion zu verwenden, um dies zu beheben. Ich könnte auch eine Division durch Null oder so etwas verursachen - dies würde eine Fehlermeldung erzeugen, aber eine irreführende. Gibt es eine Möglichkeit, meine eigene Fehlermeldung irgendwie melden zu lassen?

EMP
quelle

Antworten:

223

Sie können CAST verwenden, um einen aussagekräftigen Fehler auszulösen:

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

Dann zeigt SQL Server einige Hilfeinformationen:

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error happened here.' to data type int.
Vladimir Korolev
quelle
112
Tolle Antwort, aber JEEZ muss hacken. > :(
JohnL4
5
Für eine Inline-Tabellenwertfunktion, bei der RETURN eine einfache Auswahl ist, funktioniert dies allein nicht, da nichts zurückgegeben wird - nicht einmal null, und in meinem Fall wollte ich einen Fehler auslösen, wenn nichts gefunden wurde. Ich wollte die Inline-Funktion aus offensichtlichen Leistungsgründen nicht in eine Multi-Statment-Funktion zerlegen. Stattdessen habe ich Ihre Lösung plus ISNULL und MAX verwendet. Die RETURN-Anweisung sieht nun folgendermaßen aus: SELECT ISNULL (MAX (E.EntityID), CAST ('Die Suche (' + @LookupVariable + ') existiert nicht.' Als Int)) [EntityID] FROM Entity as E WHERE E. Lookup = @ LookupVariable
MikeTeeVee
Ja, Sie können einen Fehler auslösen, aber es scheint nicht, dass Sie einen Fehler bedingt auslösen können. Die Funktion wird unabhängig vom Codepfad ausgeführt.
Satnhak
10
Tolle Lösung, aber für diejenigen, die einen TVF verwenden, kann dies nicht einfach Teil der Rendite sein. Für diejenigen:declare @error int; set @error = 'Error happened here.';
Tim Lehner
20
Ich hasse das mit der Kraft von tausend brennenden Sonnen. Keine anderen Optionen? Fein. Aber Cripes ...
Remi Despres-Smyth
18

Der übliche Trick besteht darin, eine Division durch 0 zu erzwingen. Dies löst einen Fehler aus und unterbricht die aktuelle Anweisung, die die Funktion bewertet. Wenn der Entwickler oder Supportmitarbeiter über dieses Verhalten Bescheid weiß, ist die Untersuchung und Fehlerbehebung des Problems ziemlich einfach, da der Fehler durch Division durch 0 als Symptom eines anderen, nicht verwandten Problems verstanden wird.

So schlecht dies aus jeder Sicht auch aussieht, leider lässt das Design der SQL-Funktionen derzeit keine bessere Wahl zu. Die Verwendung von RAISERROR sollte in Funktionen unbedingt zulässig sein.

Remus Rusanu
quelle
7

In Anlehnung an die Antwort von Vladimir Korolev lautet die Redewendung, einen Fehler bedingt auszulösen

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

Ich denke, der sauberste Weg ist, einfach zu akzeptieren, dass die Funktion NULL zurückgeben kann, wenn ungültige Argumente übergeben werden. Solange dies klar dokumentiert ist, sollte dies in Ordnung sein?

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

RAISEERRORoder @@ERRORsind in UDFs nicht erlaubt. Können Sie die UDF in ein strenges Verfahren verwandeln?

Aus Erland Sommarskogs Artikel Fehlerbehandlung in SQL Server - ein Hintergrund :

Benutzerdefinierte Funktionen werden normalerweise als Teil einer SET-, SELECT-, INSERT-, UPDATE- oder DELETE-Anweisung aufgerufen. Ich habe festgestellt, dass die Ausführung der Funktion sofort abgebrochen wird, wenn ein Fehler in einer Tabellenwertfunktion mit mehreren Anweisungen oder in einer Skalarfunktion auftritt, ebenso wie die Anweisung, zu der die Funktion gehört. Die Ausführung wird in der nächsten Zeile fortgesetzt, es sei denn, der Fehler hat den Stapel abgebrochen. In beiden Fällen ist @@ error 0. Daher kann nicht erkannt werden, dass in einer Funktion von T-SQL ein Fehler aufgetreten ist.

Das Problem tritt bei Inline-Tabellenfunktionen nicht auf, da eine Inline-Tabellenwertfunktion im Grunde ein Makro ist, das der Abfrageprozessor in die Abfrage einfügt.

Sie können Skalarfunktionen auch mit der EXEC-Anweisung ausführen. In diesem Fall wird die Ausführung fortgesetzt, wenn ein Fehler auftritt (es sei denn, es handelt sich um einen Fehler beim Abbrechen des Stapels). @@ error ist gesetzt und Sie können den Wert von @@ error innerhalb der Funktion überprüfen. Es kann jedoch problematisch sein, den Fehler dem Anrufer mitzuteilen.

Mitch Wheat
quelle
4

Die beste Antwort ist im Allgemeinen die beste, funktioniert jedoch nicht für Funktionen mit Inline-Tabellenwerten.

MikeTeeVee gab in seinem Kommentar zur Top-Antwort eine Lösung dafür an, erforderte jedoch die Verwendung einer Aggregatfunktion wie MAX, die für meine Umstände nicht gut funktionierte.

Ich habe mit einer alternativen Lösung für den Fall herumgespielt, dass Sie eine Inline-Tabelle mit dem Wert udf benötigen, die anstelle eines Aggregats so etwas wie select * zurückgibt . Beispielcode zur Lösung dieses speziellen Falls finden Sie unten. Wie schon jemand darauf hingewiesen hat ... "JEEZ muss hacken" :) Ich begrüße jede bessere Lösung für diesen Fall!

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
quelle
1
Für jeden, der liest, habe ich mir keine möglichen Leistungseffekte angesehen ... Ich wäre nicht überrascht, wenn die Hack Union + Case-Aussage die Dinge verlangsamt ...
Davec
4

Einige Leute fragten nach Fehlern in Tabellenwertfunktionen, da Sie keine " RETURN [ungültige Besetzung] " verwenden können. Das Zuweisen der ungültigen Umwandlung zu einer Variablen funktioniert genauso gut.

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

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

RETURN

END

Das führt zu:

Meldung 245, Ebene 16, Status 1, Zeile 14 Die Konvertierung ist beim Konvertieren des Varchar-Werts 'booooom!' Fehlgeschlagen. zum Datentyp int.

Nachtschaufel
quelle
2

Ich kann unter Davecs Antwort keine Kommentare zur Tabellenwertfunktion abgeben, aber meiner bescheidenen Meinung nach ist dies eine einfachere Lösung:

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
quelle
-3

Eine Möglichkeit (ein Hack) besteht darin, eine Funktion / gespeicherte Prozedur zu haben, die eine ungültige Aktion ausführt. Zum Beispiel das folgende 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;

In der Tabelle tbl_throw_error gibt es eine eindeutige Einschränkung für die Spalte err_msg. Ein Nebeneffekt davon (zumindest unter MySQL) ist, dass der Wert von err_msg als Beschreibung der Ausnahme verwendet wird, wenn sie wieder in das Ausnahmeobjekt auf Anwendungsebene aufgenommen wird.

Ich weiß nicht, ob Sie mit SQL Server etwas Ähnliches tun können, aber einen Versuch wert.

Alex
quelle
5
Interessante Idee, aber INSERT ist auch in einer Funktion nicht erlaubt.
EMP