Cara melaporkan kesalahan dari fungsi yang ditentukan pengguna SQL Server

146

Saya sedang menulis fungsi yang ditentukan pengguna dalam SQL Server 2008. Saya tahu bahwa fungsi tidak dapat meningkatkan kesalahan dengan cara biasa - jika Anda mencoba memasukkan pernyataan RAISERROR, SQL akan dikembalikan:

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

Tetapi faktanya, fungsi mengambil beberapa input, yang mungkin tidak valid dan, jika ada, tidak ada nilai yang berarti fungsi dapat kembali. Apa yang harus saya lakukan?

Saya bisa, tentu saja, mengembalikan NULL, tetapi akan sulit bagi pengembang mana pun yang menggunakan fungsi untuk memecahkan masalah ini. Saya juga bisa menyebabkan pembagian dengan nol atau sesuatu seperti itu - ini akan menghasilkan pesan kesalahan, tetapi salah kaprah. Apakah ada cara agar pesan kesalahan saya sendiri dilaporkan?

EMP
sumber

Jawaban:

223

Anda dapat menggunakan CAST untuk melempar kesalahan yang berarti:

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

Kemudian Sql Server akan menampilkan beberapa informasi bantuan:

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error happened here.' to data type int.
Vladimir Korolev
sumber
112
Jawaban yang bagus, tetapi JEEZ harus meretas. > :(
JohnL4
5
Untuk fungsi inline-table-dihargai-di mana RETURN adalah pilihan sederhana, ini saja tidak berfungsi karena tidak ada yang dikembalikan - bahkan tidak nol, dan dalam kasus saya saya ingin melempar kesalahan ketika tidak ada yang ditemukan. Saya tidak ingin memecah fungsi inline menjadi multi-statment untuk alasan kinerja yang jelas. Sebaliknya saya menggunakan solusi Anda plus ISNULL dan MAX. Statemen RETURN sekarang terlihat seperti ini: SELECT ISNULL (MAX (E.EntityID), CAST ('The Lookup (' + @LookupVariable + ') tidak ada.' Sebagai Int)) [EntityID] DARI Entitas seperti E WHERE E. Lookup = @ LookupVariable
MikeTeeVee
Ya, Anda bisa melempar kesalahan, tetapi sepertinya Anda tidak bisa melemparkan kesalahan. Fungsi dieksekusi terlepas dari jalur kode.
satnhak
10
Solusi hebat, tetapi bagi mereka yang menggunakan TVF, ini tidak dapat dengan mudah menjadi bagian dari pengembalian. Bagi mereka:declare @error int; set @error = 'Error happened here.';
Tim Lehner
20
Aku benci ini dengan kekuatan seribu matahari yang membakar. Tidak ada pilihan lain? Baik. Tapi melumpuhkan ...
Remi Despres-Smyth
18

Trik yang biasa adalah memaksa pembagian dengan 0. Ini akan menimbulkan kesalahan dan mengganggu pernyataan saat ini yang mengevaluasi fungsi. Jika pengembang atau orang yang mendukung tahu tentang perilaku ini, menyelidiki dan memecahkan masalah masalahnya cukup mudah karena kesalahan pembagian oleh 0 dipahami sebagai gejala dari masalah yang berbeda, yang tidak terkait.

Seburuk ini terlihat dari sudut pandang manapun, sayangnya desain fungsi SQL saat ini tidak memungkinkan pilihan yang lebih baik. Menggunakan RAISERROR harus benar-benar diizinkan dalam fungsi.

Remus Rusanu
sumber
7

Sebagai lanjutan dari jawaban Vladimir Korolev, idiom untuk membuang kesalahan secara kondisional adalah

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

Saya pikir cara terbersih adalah dengan hanya menerima bahwa fungsi dapat mengembalikan NULL jika argumen yang tidak valid dilewatkan. Selama ini jelas didokumentasikan maka ini harus baik-baik saja?

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

RAISEERRORatau @@ERRORtidak diizinkan di UDF. Bisakah Anda mengubah UDF menjadi prosedur bertingkat?

Dari artikel Erland Sommarskog Penanganan Kesalahan di SQL Server - Latar Belakang :

Fungsi yang ditentukan pengguna biasanya dipanggil sebagai bagian dari pernyataan SET, SELECT, INSERT, UPDATE atau DELETE. Apa yang saya temukan adalah bahwa jika kesalahan muncul dalam fungsi tabel bernilai multi-pernyataan atau dalam fungsi skalar, eksekusi fungsi dibatalkan segera, dan begitu juga pernyataan fungsi adalah bagian dari. Eksekusi berlanjut di baris berikutnya, kecuali kesalahan membatalkan bets. Dalam kedua kasus, @@ kesalahan adalah 0. Dengan demikian, tidak ada cara untuk mendeteksi bahwa kesalahan terjadi pada fungsi dari T-SQL.

Masalahnya tidak muncul dengan fungsi tabel inline, karena fungsi bernilai tabel inline pada dasarnya adalah makro yang ditempelkan oleh prosesor kueri ke dalam kueri.

Anda juga dapat menjalankan fungsi skalar dengan pernyataan EXEC. Dalam hal ini, eksekusi berlanjut jika terjadi kesalahan (kecuali jika itu adalah kesalahan batch-dibatalkan). @@ error diatur, dan Anda dapat memeriksa nilai @@ error dalam fungsi tersebut. Ini bisa bermasalah untuk mengkomunikasikan kesalahan kepada pemanggil.

Mitch Wheat
sumber
4

Jawaban teratas umumnya adalah yang terbaik, tetapi tidak berfungsi untuk fungsi bernilai tabel inline.

MikeTeeVee memberikan solusi untuk ini dalam komentarnya pada jawaban atas, tetapi diperlukan penggunaan fungsi agregat seperti MAX, yang tidak bekerja dengan baik untuk keadaan saya.

Saya main-main dengan solusi alternatif untuk kasus di mana Anda memerlukan tabel inline bernilai udf yang mengembalikan sesuatu seperti select * bukan agregat. Contoh kode yang memecahkan kasus khusus ini ada di bawah ini. Seperti yang telah ditunjukkan oleh seseorang ... "JEEZ wotta hack" :) Saya menyambut solusi yang lebih baik untuk kasus ini!

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
sumber
1
bagi siapa pun yang membaca, saya tidak melihat potensi efek kinerja ... saya tidak akan terkejut jika pernyataan peretasan + kasus hack memperlambat segalanya ...
davec
4

Beberapa orang bertanya tentang meningkatkan kesalahan dalam fungsi Table-Valued, karena Anda tidak dapat menggunakan hal-hal " RETURN [pemain tidak valid] ". Menetapkan pemain tidak valid ke variabel berfungsi juga.

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

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

RETURN

END

Ini menghasilkan:

Msg 245, Level 16, State 1, Line 14 Conversion gagal ketika mengonversi nilai varchar 'booooom!' untuk tipe data int.

NightShovel
sumber
2

Saya tidak bisa berkomentar di bawah jawaban davec mengenai fungsi nilai tabel, tetapi menurut pendapat saya ini adalah solusi yang lebih mudah:

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

Salah satu cara (peretasan) adalah memiliki fungsi / prosedur tersimpan yang melakukan tindakan yang tidak valid. Misalnya, pseudo SQL berikut

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;

Dimana pada tabel tbl_throw_error, ada batasan unik pada kolom err_msg. Efek sampingnya (setidaknya pada MySQL), adalah nilai err_msg digunakan sebagai deskripsi pengecualian ketika ia kembali ke objek pengecualian level aplikasi.

Saya tidak tahu apakah Anda dapat melakukan sesuatu yang mirip dengan SQL Server, tetapi patut dicoba.

Alex
sumber
5
Ide yang menarik, tetapi INSERT juga tidak diizinkan dalam suatu fungsi.
EMP