在SQL Server的存储过程开发中,运行错误如果不进行处理,很容易导致数据状态不一致,比如部分数据插入成功、部分失败,后续排查问题也会非常困难。通过TRY CATCH模块结合事务管理,可以在存储过程中捕获运行错误,并且在错误发生时回滚所有未完成的事务,保证数据操作的完整性。

TRY CATCH模块的基本结构
SQL Server的TRY CATCH模块用于捕获运行时错误,基本结构分为TRY块和CATCH块两部分。TRY块中放置可能会出现错误的业务代码,当TRY块中的代码执行出错时,程序会立即跳转到CATCH块中执行错误处理逻辑,而不会直接抛出错误终止整个存储过程的执行。
基本语法结构如下:
BEGIN TRY
-- 可能出现错误的业务代码
END TRY
BEGIN CATCH
-- 捕获错误后的处理逻辑
END CATCH
结合事务实现异常回滚
要让错误发生时回滚操作,需要把业务逻辑放在事务中,在TRY块中开启事务,如果执行成功就提交事务,一旦出现错误进入CATCH块,就回滚事务。同时可以通过系统函数获取错误的详细信息,方便后续排查问题。
常用的错误相关系统函数有:
ERROR_NUMBER():返回错误的编号ERROR_MESSAGE():返回错误的描述信息ERROR_SEVERITY():返回错误的严重级别ERROR_STATE():返回错误的状态编号
完整示例代码
下面是一个完整的存储过程示例,实现插入用户数据到用户表,同时插入对应的用户日志,如果任意一步出错,就回滚所有操作,并且返回错误信息。
-- 创建示例表
CREATE TABLE Test_User (
UserId INT IDENTITY(1,1) PRIMARY KEY,
UserName NVARCHAR(50) NOT NULL,
Age INT
)
CREATE TABLE Test_UserLog (
LogId INT IDENTITY(1,1) PRIMARY KEY,
UserId INT,
OperateTime DATETIME DEFAULT GETDATE()
)
GO
-- 创建存储过程
CREATE PROCEDURE Insert_User_With_Log
@UserName NVARCHAR(50),
@Age INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
-- 开启事务
BEGIN TRANSACTION;
-- 插入用户表
INSERT INTO Test_User (UserName, Age) VALUES (@UserName, @Age);
-- 获取插入的用户ID
DECLARE @NewUserId INT = SCOPE_IDENTITY();
-- 插入用户日志表
INSERT INTO Test_UserLog (UserId) VALUES (@NewUserId);
-- 执行成功,提交事务
COMMIT TRANSACTION;
-- 返回成功标识
SELECT 1 AS Result, '操作成功' AS Msg;
END TRY
BEGIN CATCH
-- 出现错误,回滚事务
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- 获取错误信息
DECLARE @ErrNum INT = ERROR_NUMBER();
DECLARE @ErrMsg NVARCHAR(4000) = ERROR_MESSAGE();
DECLARE @ErrSev INT = ERROR_SEVERITY();
DECLARE @ErrState INT = ERROR_STATE();
-- 返回错误信息
SELECT 0 AS Result,
CONCAT('操作失败,错误编号:', @ErrNum, ',错误信息:', @ErrMsg) AS Msg,
@ErrSev AS Severity,
@ErrState AS State;
END CATCH
END
GO
注意事项
在使用TRY CATCH处理存储过程错误时,需要注意以下几点:
- TRY块和CATCH块之间不能有其他语句,必须紧邻
- 不是所有错误都会被TRY CATCH捕获,比如严重的系统级错误可能会导致整个会话终止,这类错误无法被捕获
- 在CATCH块中回滚事务前,最好先判断
@@TRANCOUNT的值,避免没有开启事务时执行回滚操作报错 - 如果存储过程中有嵌套的事务,需要合理控制事务的提交和回滚逻辑,避免外层事务受影响
测试验证
我们可以执行存储过程测试效果,先插入正常数据:
-- 正常插入测试 EXEC Insert_User_With_Log @UserName = '张三', @Age = 25; -- 查询数据,会发现用户表和日志表都有对应数据 SELECT * FROM Test_User; SELECT * FROM Test_UserLog;
再测试错误场景,比如插入年龄为负数的数据,假设表中有年龄大于0的约束:
-- 插入错误数据测试 EXEC Insert_User_With_Log @UserName = '李四', @Age = -5; -- 查询数据,用户表和日志表都不会有新数据,因为事务已经回滚 SELECT * FROM Test_User; SELECT * FROM Test_UserLog;