Come segnalare un errore da una funzione definita dall'utente di SQL Server


146

Sto scrivendo una funzione definita dall'utente in SQL Server 2008. So che le funzioni non possono generare errori nel solito modo - se si tenta di includere l'istruzione RAISERROR restituisce SQL:

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

Ma il fatto è che la funzione accetta alcuni input, che potrebbero non essere validi e, in tal caso, non esiste alcun valore significativo che la funzione può restituire. Cosa faccio allora?

Potrei, ovviamente, restituire NULL, ma sarebbe difficile per qualsiasi sviluppatore che utilizza la funzione per risolvere questo problema. Potrei anche causare una divisione per zero o qualcosa del genere - questo genererebbe un messaggio di errore, ma fuorviante. C'è un modo in cui posso avere il mio messaggio di errore segnalato in qualche modo?

Risposte:


223

È possibile utilizzare CAST per generare un errore significativo:

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

Quindi Sql Server mostrerà alcune informazioni di aiuto:

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error happened here.' to data type int.

112
Ottima risposta, ma JEEZ doveva hackerare. > :(
JohnL4,

5
Per una funzione con valori di tabella inline in cui RETURN è una selezione semplice, questo da solo non funziona perché non viene restituito nulla, nemmeno nullo, e nel mio caso volevo lanciare un errore quando non veniva trovato nulla. Non volevo suddividere la funzione inline in una multi-statment per ovvie ragioni di prestazione. Invece ho usato la tua soluzione più ISNULL e MAX. La dichiarazione RETURN ora appare così: SELECT ISNULL (MAX (E.EntityID), CAST ('The Lookup (' + @LookupVariable + ') non esiste.' Come Int)) [EntityID] DA Entità come E DOVE E. Lookup = @ LookupVariable
MikeTeeVee

Sì, è possibile generare un errore, ma non sembra che sia possibile generare un errore in modo condizionale. La funzione viene eseguita indipendentemente dal percorso del codice.
sabato

10
Ottima soluzione, ma per coloro che utilizzano un TVF, questo non può facilmente far parte del ritorno. Per quelli:declare @error int; set @error = 'Error happened here.';
Tim Lehner,

20
Lo odio con il potere di mille soli ardenti. Nessuna altra opzione? Belle. Ma cripes ...
Remi Despres-Smyth,

18

Il solito trucco è forzare una divisione per 0. Ciò genererà un errore e interromperà l'istruzione corrente che sta valutando la funzione. Se lo sviluppatore o la persona di supporto è a conoscenza di questo comportamento, indagare e risolvere il problema è abbastanza semplice poiché la divisione per errore 0 è intesa come sintomo di un problema diverso e non correlato.

Per quanto sembri da nessun punto di vista, sfortunatamente il design delle funzioni SQL al momento non consente una scelta migliore. L'uso di RAISERROR dovrebbe essere assolutamente consentito nelle funzioni.


7

In seguito alla risposta di Vladimir Korolev, l'idioma di gettare condizionalmente un errore è

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)    

6

Penso che il modo più pulito sia semplicemente accettare che la funzione possa restituire NULL se vengono passati argomenti non validi. Finché è chiaramente documentato, allora dovrebbe andare bene?

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

5

RAISEERRORo @@ERRORnon sono ammessi negli UDF. Riesci a trasformare l'UDF in una procedura articolata?

Dall'articolo di Erland Sommarskog Gestione degli errori in SQL Server - uno sfondo :

Le funzioni definite dall'utente vengono generalmente invocate come parte di un'istruzione SET, SELECT, INSERT, UPDATE o DELETE. Quello che ho scoperto è che se viene visualizzato un errore in una funzione con valori di tabella multiistruzione o in una funzione scalare, l'esecuzione della funzione viene immediatamente interrotta, così come l'istruzione di cui fa parte la funzione. L'esecuzione continua sulla riga successiva, a meno che l'errore non abbia interrotto il batch. In entrambi i casi, l'errore @@ è 0. Pertanto, non è possibile rilevare che si è verificato un errore in una funzione da T-SQL.

Il problema non si presenta con le funzioni di tabella incorporate, poiché una funzione con valori di tabella incorporata è sostanzialmente una macro che il processore di query incolla nella query.

È inoltre possibile eseguire funzioni scalari con l'istruzione EXEC. In questo caso, l'esecuzione continua se si verifica un errore (a meno che non si tratti di un errore di interruzione batch). L'errore @@ è impostato ed è possibile verificare il valore dell'errore @@ all'interno della funzione. Tuttavia, può essere problematico comunicare l'errore al chiamante.


4

La risposta migliore è generalmente la migliore, ma non funziona per le funzioni con valori di tabella incorporate.

MikeTeeVee ha fornito una soluzione per questo nel suo commento sulla risposta principale, ma ha richiesto l'uso di una funzione aggregata come MAX, che non ha funzionato bene per le mie circostanze.

Ho pasticciato con una soluzione alternativa per il caso in cui hai bisogno di una tabella inline valutata udf che restituisce qualcosa come select * invece di un aggregato. Di seguito è riportato un esempio di codice che risolve questo caso particolare. Come qualcuno ha già sottolineato ... "JEEZ wotta hack" :) Accolgo con favore qualsiasi soluzione migliore per questo 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

1
per chiunque legga, non ho esaminato i potenziali effetti sulle prestazioni ... non sarei sorpreso se l'istruzione hack union + case case rallenta le cose ...
davec

4

Alcune persone si chiedevano di sollevare errori nelle funzioni con valori di tabella, poiché non è possibile utilizzare una sorta di " RETURN [cast non valido] ". L'assegnazione del cast non valido a una variabile funziona altrettanto bene.

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

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

RETURN

END

Questo risulta in:

Messaggio 245, livello 16, stato 1, riga 14 Conversione non riuscita durante la conversione del valore varchar 'booooom!' al tipo di dati int.


2

Non posso commentare sotto la risposta di David per quanto riguarda la funzione con valori di tabella, ma a mio modesto parere questa è la soluzione più semplice:

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

Un modo (un hack) consiste nell'avere una funzione / procedura memorizzata che esegue un'azione non valida. Ad esempio, il seguente 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;

Dove sulla tabella tbl_throw_error, esiste un vincolo univoco sulla colonna err_msg. Un effetto collaterale di questo (almeno su MySQL) è che il valore di err_msg viene utilizzato come descrizione dell'eccezione quando viene ripristinato nell'oggetto eccezione a livello di applicazione.

Non so se puoi fare qualcosa di simile con SQL Server, ma vale la pena provare.


5
Un'idea interessante, ma INSERT non è consentito nemmeno in una funzione.
EMP,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.