SQL Server:错误处理及事务控制
目录:
解读错误信息
RAISERROR
THROW
实例
使用 @@ERROR
使用 XACT_ABORT
使用TRY/CATCH
现实中的事务语句
删除
更新
银行取钱
解读错误信息
Msg 547, Level 16, State 0, Line 11 The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Products_Categories". The conflict occurred in database "TSQL2012", table "Production.Categories", column 'categoryid'.
Error number
● SQL Server 错误信息的编号从1~49999
● 自定义错误信息从50001开始
● 错误编号50000是为没有错误编号的自定义信息准备的。
Severity level
SQL Server 一共26个严重级别 0~25。
● 严重级别>= 16的会记录SQL Server日志和Windows 应用程序日志
● 严重级别19~25 只能由 sysadmin觉得的成员处理
● 严重级别20~25被认为是致命错误。 会中断终端连接并回滚所有打开的事务。
● 严重级别0~10只是提示信息。
State int 类型,最大值127, MS internal purposes
Error message 支持255个Unicode 字符
● SQL Server 错误信息都在 sys.messages里面
● 可以用sp_addmessage 添加自定义错误信息
RAISERROR(不会中断事务)
简单的传递信息可以使用级别0~9 。
如果你有sysadmin的角色,可以使用WITH LOG选项并设置一个严重级别>20的错误。error 发生的时候SQL Server会中断连接。
使用NOWAIT选项可以直接发送信息,而不用等大赛buffer
RAISERROR ('Error in usp_InsertCategories stored procedure', 16, 0); -- Formatting the RAISERROR string RAISERROR ('Error in % stored procedure', 16, 0, N'usp_InsertCategories'); -- In addition, you can use a variable: GO DECLARE @message AS NVARCHAR(1000) = N'Error in % stored procedure'; RAISERROR (@message, 16, 0, N'usp_InsertCategories'); -- And you can add the formatting outside RAISERROR using the FORMATMESSAGE function: GO DECLARE @message AS NVARCHAR(1000) = N'Error in % stored procedure'; SELECT @message = FORMATMESSAGE (@message, N'usp_InsertCategories'); RAISERROR (@message, 16, 0);
THROW (会中断事务)
-- You can issue a simple THROW as follows: THROW 50000, 'Error in usp_InsertCategories stored procedure', 0; -- Because THROW does not allow formatting of the message parameter, you can use FORMATMESSAGE() GO DECLARE @message AS NVARCHAR(1000) = N'Error in % stored procedure'; SELECT @message = FORMATMESSAGE (@message, N'usp_InsertCategories'); THROW 50000, @message, 0;
-- RAISERROR does not normally terminate a batch: RAISERROR ('Hi there', 16, 0); PRINT 'RAISERROR error'; -- Prints GO -- However, THROW does terminate the batch: THROW 50000, 'Hi there', 0; PRINT 'THROW error'; -- Does not print GO
实例
使用 @@ERROR
DECLARE @errnum AS int; BEGIN TRAN; SET IDENTITY_INSERT Production.Products ON; INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); SET @errnum = @@ERROR; IF @errnum <> 0 -- Handle the error BEGIN PRINT 'Insert into Production.Products failed with error ' + CAST(@errnum AS VARCHAR); END
DECLARE @errnum AS int; BEGIN TRAN; SET IDENTITY_INSERT Production.Products ON; -- Insert #1 will fail because of duplicate primary key INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); SET @errnum = @@ERROR; IF @errnum <> 0 BEGIN IF @@TRANCOUNT > 0 ROLLBACK TRAN; PRINT 'Insert #1 into Production.Products failed with error ' + CAST(@errnum AS VARCHAR); END; -- Insert #2 will succeed INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(101, N'Test2: Bad categoryid', 1, 1, 18.00, 0); SET @errnum = @@ERROR; IF @errnum <> 0 BEGIN IF @@TRANCOUNT > 0 ROLLBACK TRAN; PRINT 'Insert #2 into Production.Products failed with error ' + CAST(@errnum AS VARCHAR); END; SET IDENTITY_INSERT Production.Products OFF; IF @@TRANCOUNT > 0 COMMIT TRAN; -- Remove the inserted row DELETE FROM Production.Products WHERE productid = 101; PRINT 'Deleted ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
使用 XACT_ABORT
使用XACT_ABORT,语句中发生错误,整段语句都会中止。
SET XACT_ABORT ON; PRINT 'Before error'; SET IDENTITY_INSERT Production.Products ON; INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); SET IDENTITY_INSERT Production.Products OFF; PRINT 'After error'; GO PRINT 'New batch'; SET XACT_ABORT OFF;
-- Using THROW with XACT_ABORT. USE TSQL2012; GO SET XACT_ABORT ON; PRINT 'Before error'; THROW 50000, 'Error in usp_InsertCategories stored procedure', 0; PRINT 'After error'; GO PRINT 'New batch'; SET XACT_ABORT OFF;
@@ERROR第二个例子中使用XACT_ABORT以后,第二条语句这回就无效了。
DECLARE @errnum AS int; SET XACT_ABORT ON; BEGIN TRAN; SET IDENTITY_INSERT Production.Products ON; -- Insert #1 will fail because of duplicate primary key INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); SET @errnum = @@ERROR; IF @errnum <> 0 BEGIN IF @@TRANCOUNT > 0 ROLLBACK TRAN; PRINT 'Error in first INSERT'; END; -- Insert #2 no longer succeeds INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(101, N'Test2: Bad categoryid', 1, 1, 18.00, 0); SET @errnum = @@ERROR; IF @errnum <> 0 BEGIN -- Take actions based on the error IF @@TRANCOUNT > 0 ROLLBACK TRAN; PRINT 'Error in second INSERT'; END; SET IDENTITY_INSERT Production.Products OFF; IF @@TRANCOUNT > 0 COMMIT TRAN; GO DELETE FROM Production.Products WHERE productid = 101; PRINT 'Deleted ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows'; SET XACT_ABORT OFF; GO SELECT XACT_STATE(), @@TRANCOUNT;
使用TRY/CATCH
格式
--Transactions extend batches BEGIN TRY BEGIN TRANSACTION INSERT INTO Sales.SalesOrderHeader... --Succeeds INSERT INTO Sales.SalesOrderDetail... --Fails COMMIT TRANSACTION -- If no errors, transaction completes END TRY BEGIN CATCH --Inserted rows still exist in Sales.SalesOrderHeader SELECT ERROR_NUMBER() ROLLBACK TRANSACTION --Any transaction work undone END CATCH;
BEGIN TRY BEGIN TRAN; SET IDENTITY_INSERT Production.Products ON; INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(101, N'Test2: Bad categoryid', 1, 10, 18.00, 0); SET IDENTITY_INSERT Production.Products OFF; COMMIT TRAN; END TRY BEGIN CATCH IF ERROR_NUMBER() = 2627 -- Duplicate key violation BEGIN PRINT 'Primary Key violation'; END ELSE IF ERROR_NUMBER() = 547 -- Constraint violations BEGIN PRINT 'Constraint violation'; END ELSE BEGIN PRINT 'Unhandled error'; END; IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; END CATCH;
-- revise the CATCH block using variables to capture error information and re-raise the error using RAISERROR. USE TSQL2012; GO SET NOCOUNT ON; DECLARE @error_number AS INT, @error_message AS NVARCHAR(1000), @error_severity AS INT; BEGIN TRY BEGIN TRAN; SET IDENTITY_INSERT Production.Products ON; INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(101, N'Test2: Bad categoryid', 1, 10, 18.00, 0); SET IDENTITY_INSERT Production.Products OFF; COMMIT TRAN; END TRY BEGIN CATCH SELECT XACT_STATE() as 'XACT_STATE', @@TRANCOUNT as '@@TRANCOUNT'; SELECT @error_number = ERROR_NUMBER(), @error_message = ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(); RAISERROR (@error_message, @error_severity, 1); IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; END CATCH;
-- use a THROW statement without parameters re-raise (re-throw) the original error message and send it back to the client. USE TSQL2012; GO BEGIN TRY BEGIN TRAN; SET IDENTITY_INSERT Production.Products ON; INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(1, N'Test1: Ok categoryid', 1, 1, 18.00, 0); INSERT INTO Production.Products(productid, productname, supplierid, categoryid, unitprice, discontinued) VALUES(101, N'Test2: Bad categoryid', 1, 10, 18.00, 0); SET IDENTITY_INSERT Production.Products OFF; COMMIT TRAN; END TRY BEGIN CATCH SELECT XACT_STATE() as 'XACT_STATE', @@TRANCOUNT as '@@TRANCOUNT'; IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; THROW; END CATCH; GO SELECT XACT_STATE() as 'XACT_STATE', @@TRANCOUNT as '@@TRANCOUNT';
现实中的事务语句
删除
--删除 CREATE PROCEDURE [dbo].[Students_Delete](@ID int) WITH EXECUTE AS CALLER AS BEGIN --Check to make sure the ID does exist --If not does, return error DECLARE @existing AS int = 0 SELECT @existing = count(ID) FROM Students WHERE ID = @ID IF @existing <> 1 BEGIN RAISERROR ('ID does not exist', 1, 1) RETURN 0 END --Attempt Delete DELETE FROM [dbo].[Students] WHERE ID = @ID --check to see if update occured --and return status IF @@ROWCOUNT = 1 BEGIN INSERT INTO StudentDeleteLog VALUES (suser_sname(), @ID, getdate()) RETURN 1 END ELSE RETURN 0 END GO
更新
CREATE PROCEDURE [dbo].[Students_Update] ( @ID int, @LASTNAME varchar(50), @FIRSTNAME varchar(50), @STATE varchar(50), @PHONE varchar(50), @EMAIL varchar(50), @GRADYEAR int, @GPA decimal(20,10), @PROGRAM varchar(50), @NEWSLETTER bit ) AS BEGIN --Check to make sure the ID does exist --If not does, return error DECLARE @existing AS int = 0 SELECT @existing = count(ID) FROM Students WHERE ID = @ID IF @existing <> 1 BEGIN RAISERROR ('ID does not exist', 1, 1) RETURN 0 END --Can not subscribe to newsletter if email is null IF (@email IS NULL) SET @NEWSLETTER = 0 --Attempt Update UPDATE [dbo].[Students] SET [LASTNAME] = @LASTNAME ,[FIRSTNAME] = @FIRSTNAME ,[STATE] = @STATE ,[PHONE] = @PHONE ,[EMAIL] = @EMAIL ,[GRADYEAR] = @GRADYEAR ,[GPA] = @GPA ,[PROGRAM] = @PROGRAM ,[NEWSLETTER] = @NEWSLETTER WHERE ID = @ID --check to see if update occured --and return status IF @@ROWCOUNT = 1 RETURN 1 ELSE RETURN 0 END GO
银行取钱
BEGIN TRAN; IF NOT EXISTS ( SELECT * FROM Accounts WITH(UPDLOCK) --只有当前的事务可以查看 WHERE AccountID = 47387438 AND Balance >= 400 ) BEGIN ROOLBACK TRAN; THROW 50000,'Tobias is too poor',1; END UPDATE Accounts SET Balance -=400 WHERE AccountID = 47387438; COMMIT TRAN; --银行取钱高效版本 BEGIN TRAN; UPDATE Accounts SET Balance -= 400 WHERE AccountID = 47387438 AND Balance >= 400 IF(@@ROWCOUNT <> 1) BEGIN ROLLBACK TRAN; THROW 50000,'Tobias is too poor ',1; END COMMIT TRAN;
参考文档
Database Engine Error Severities
https://msdn.microsoft.com/en-us/library/ms164086.aspx
SET XACT_ABORT (Transact-SQL)
https://msdn.microsoft.com/zh-tw/library/ms188792.aspx