概述

最近因为业务的需求写了一段时间存储过程,发现之前写的存储过程存在一些不严谨的地方,特别是TRY...CATCH中嵌套事务的写法;虽然之前写的并没有错,但是还是埋藏着很大的隐患在里面。希望这篇文章能给大家一些参考;文章内容有点长还望耐心阅读。

1.插入测试数据

----创建表
DROP TABLE score
GO
CREATE TABLE [dbo].[score](
id INT NOT NULL PRIMARY KEY IDENTITY(1,1),
name VARCHAR(50) NOT NULL,
score INT NOT NULL CHECK (score>=0),
months INT NOT NULL,
createtime DATETIME NOT NULL DEFAULT GETDATE()
) ---根据姓名月份查询分数
CREATE INDEX IX_score_name ON score(name,months) include(score)
---根据月份查询最高分数
CREATE INDEX IX_score_months ON score(months) include(name,score)
---创建姓名和月份组合的唯一索引
CREATE UNIQUE INDEX IX_score_months_name ON score(months,name) ------插入测试数据
TRUNCATE TABLE score INSERT INTO score(name,score,months)
VALUES('li',50,10),('chen',70,10),('zhang',80,10),('wang',90,10),('li',50,11),('chen',70,11),('zhang',80,11),('wang',90,11) SELECT * FROM score;

2、THROW

THROW是在2012版本中引入的,在有些场景当中,应用程序端不做一些合法性的验证,这些验证会被放在数据库端来验证。当数据库端验证输入的信息不合法时需要主动抛出异常来中断代码的执行。

THROW既可以接收错误信息抛错提示,同时也可以手动抛出错误到CATCH中。语法如下:

;THROW

THROW [ { error_number | @local_variable }, 

        { message | @local_variable }, 

        { state | @local_variable } ]  

[ ; ]

参数

error_number
表示异常的常量或变量。 error_number是int并且必须为大于或等于 50000 且小于或等于 2147483647,如果CATCH中使用RAISERROR来接收错误信息那么指定的error_number必须在sys.messages 中存在;如果使用CATCH来接收则不需要。 消息
描述异常的字符串或变量。 消息是nvarchar(2048)。 状态
在 0 到 255 之间的常量或变量,指示与消息关联的状态。 状态是tinyint。

注意:

1.THROW代码前必须要用分号,因为THROW会中断代码的执行,所以如果将THROW放在CATCH中时必须放在ROLLBACK TRAN之后,否则不会回滚事务导致对象一直处于提交状态被锁。

2.THROW放CATCH中可以达到RAISERROR一样的效果,同时还简便了代码。

3. THROW能返回正确的错误代码行号,而RAISERROR没办法

参考:https://docs.microsoft.com/zh-cn/sql/t-sql/language-elements/throw-transact-sql

3.sp_addmessage

自定义错误号

EXEC sp_addmessage
@msgnum = 60000,
@severity = 16,
@msgtext = N'Manual cast wrong ',
@lang = 'us_english'; EXEC sp_addmessage
@msgnum = 60000,
@severity = 16,
@msgtext = N'手动抛错',
@lang = '简体中文';

注意:自定义错误号必须大于50000

二、调用存储过程

1.查询存储过程

----查询存储过程
CREATE PROCEDURE Pro_score
(@Option VARCHAR(50),
@name VARCHAR(50)='',
@months INT=''
)
AS
BEGIN ---查询指定人分数
IF @Option='GetScore'
BEGIN
SELECT name,
score
FROM score
WHERE name=@name END ----查询指定月份最高分数
IF @Option='MonthMaxScore'
BEGIN
SELECT Top 1
name,
score
FROM score
WHERE months=@months
ORDER BY score END END

调用存储过程:

EXEC Pro_score @Option='GetScore',@name='li'
EXEC Pro_score @Option='MonthMaxScore',@months=11

3.修改存储过程

 1 CREATE PROCEDURE [dbo].[Pro_Insert_score]
2 (@Option VARCHAR(50),
3 @name VARCHAR(50)='',
4 @months INT=0,
5 @score INT=0
6 )
7 AS
8 BEGIN
9 DECLARE @ErrorNum INT,@ErrorSeverity INT,@ErrorState INT,@ErrorLine INT,@ErrorPro VARCHAR(200),@ErrorMessage NVARCHAR(4000);
10 IF @Option='InsertScore'
11 BEGIN
12
13 -----使用事务
14 BEGIN TRY
15 BEGIN TRAN
16 INSERT INTO score(name,score,months)
17 VALUES(@name,@score,@months)
18
19 ----插入重复值报错事务回滚
20 INSERT INTO score(name,score,months)
21 VALUES(@name,@score,@months)
22
23 COMMIT TRAN
24
25 END TRY
26 BEGIN CATCH
27 SELECT @ErrorMessage = ERROR_MESSAGE(),@ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE();
28 RAISERROR (@ErrorMessage,@ErrorSeverity,@ErrorState) ;
29 ROLLBACK TRAN
30 ;THROW
31 ----执行失败
32 RETURN 1
33 END CATCH
34
35 ----执行成功
36 RETURN 0
37 END
38
39 END

调用存储过程

----调用存储过程2
DECLARE @status INT
EXEC @status=Pro_Insert_score @Option='InsertScore',@name='chen',@months=12,@score=90
SELECT @status

可以发现使用RAISERROR抛错出来的行号和消息号都是错误的,50000这个消息号其实是不存在的,它是保留的一个统一的消息号。

可以通过查询sys.message查询对应的消息号

SELECT * FROM score WHERE name='chen'
SELECT * FROM sys.messages WHERE message_id=2601 and language_id=2052

4.手动抛错中断

手动抛错也是这篇文章主要要讲的一个知识点,在有一些业务场景当中有一些验证操作需要在数据库中进行,甚至必须在更新之后进行但是又是正常的提交操作,在这种情况下就需要手动进行验证是否需要执行下面的代码。,见过很多程序员写存储过程喜欢在每一个判断的地方加上RETURN操作,目的是为了不执行后面的代码,同时又在RETURN前加上ROLLBACK操作。这虽然是一个办法,但是在事务中运用RETURN是一个很危险的操作,弄不好会导致事务一直处于打开操作导致表一直被锁住,在生成环境是很危险的操作。

建议使用THROW来手动进行抛错,THROW抛错会相当于触发一个11-19级别的错误,这样会跳到CATCH中做ROLLBACK操作。

注意:THROW前必须以分号开头,如果THROW前有代码以分号结尾也可以。

CREATE PROCEDURE [dbo].[Pro_score_throw]
(@Option VARCHAR(50),
@name VARCHAR(50)='',
@months INT=0,
@score INT=0
)
AS
BEGIN
DECLARE @ErrorNum INT,@ErrorSeverity INT,@ErrorState INT,@ErrorLine INT,@ErrorPro VARCHAR(200),@ErrorMessage NVARCHAR(4000);
IF @Option='UpdateScore'
BEGIN -----使用事务
BEGIN TRY
BEGIN TRAN
UPDATE score
SET score=score+@score
WHERE name=@name AND months=@months ----在有些业务场景有些判断必须等操作完了才能去做判断是否能继续执行下去
IF (SELECT score FROM score WHERE name=@name AND months=@months)>100
BEGIN ;THROW 60000,'分数不能大于100',111 END
COMMIT TRAN END TRY
BEGIN CATCH ROLLBACK TRAN
;THROW
END CATCH ----执行成功
RETURN 0
END END

调用存储过程

DECLARE @status INT
EXEC @status=Pro_score_throw @Option='UpdateScore',@name='chen',@months=10,@score=40
SELECT @status

5.存储过程调用存储过程

CREATE PROCEDURE [dbo].[Pro_score_ProcToProc]
(@Option VARCHAR(50),
@name VARCHAR(50)='',
@months INT=0,
@score INT=0
)
AS
BEGIN
DECLARE @ErrorNum INT,@ErrorSeverity INT,@ErrorState INT,@ErrorLine INT,@ErrorPro VARCHAR(200),@ErrorMessage NVARCHAR(4000);
IF @Option='Update'
BEGIN
----判断修改的人是否存在
IF NOT EXISTS(SELECT * FROM score WHERE name=@name)
BEGIN
---修改人不存在
RETURN 2
END
ELSE
BEGIN
-----使用事务
BEGIN TRY
BEGIN TRAN
UPDATE score
SET createtime='1900-01-01 00:00:000'
WHERE name=@name AND months=@months SELECT name,months,createtime,score FROM score WHERE name=@name AND months=@months
---定义事务保存点
---SAVE TRAN TRAN1
----调用别的存储过程
EXEC Pro_score_ProcToProc @Option='UpdateScore',@name=@name,@months=@months,@score=@score COMMIT TRAN END TRY
BEGIN CATCH
SELECT name,months,createtime,score FROM score WHERE name=@name AND months=@months
IF @@TRANCOUNT > 0
ROLLBACK TRAN ;
SELECT name,months,createtime,score FROM score WHERE name=@name AND months=@months
;THROW
END CATCH
END
----执行成功
RETURN 0
END IF @Option='UpdateScore'
BEGIN ---使用事务
BEGIN TRY
BEGIN TRAN
UPDATE score
SET score=score+@score
WHERE name=@name AND months=@months ----在有些业务场景有些判断必须等操作完了才能去做判断是否能继续执行下去
IF (SELECT score FROM score WHERE name=@name AND months=@months)>100
BEGIN ;THROW 60000,'分数不能大于100',111 END
COMMIT TRAN END TRY
BEGIN CATCH
----回滚到指定保存点
----ROLLBACK TRAN TRAN1 --回滚事务
ROLLBACK TRAN
----执行失败
;THROW
END CATCH END
END

存储过程调用存储过程事务的三种处理方法:

1.内部存储过程不要包含事务,因为内部ROLLBACK会直接回滚到外部的BEGIN TRAN导致外部的ROLLBACK没有对应的COMMIT;

2.还有一种方法是在调用内部存储过程之前使用保存点“SAVE TRAN TRAN1”,同时内部存储过程的ROLLBACK TRAN必须指定事务保存点,例如“ROLLBACK TRAN TRAN1”,这样内部存储过程回滚就只会回滚到保持点.

3.在外部存储过程的CATCH块的ROLLBACK前加上IF @@TRANCOUNT > 0判断条件

事务嵌套事务的理解

---事务1
BEGIN TRAN
---事务2
BEGIN TRAN COMMIT TRAN /ROLLBACK TRAN COMMIT TRAN /ROLLBACK TRAN

对于事务嵌套事务,事务2的ROLLBACK操作会直接回滚到事务1的BEGIN TRAN,会导致事务1的ROLLBACK没有对应的BEGIN TRAN。处理方法可以在调用事务2之前定义一个事务保存点或者在事务1的ROLLBACK前加上IF @@TRANCOUNT > 0判断条件是否存在事务需要回滚。

SET XACT_ABORT ON

并不是所有的错误都能被CATCH所接收。对于严重级别为0-10(信息性消息)和20-25(致命的消息)是不能被CATCH所接收的,这时如果在事务中遇到了这类的报错那么通用会导致事务处理打开状态,这时就需要开启XACT_ABORT。当开启XACT_ABORT后只要代码中存在报错就会执行回滚操作,而不管错误的级别。例如:

CREATE TABLE [dbo].[AA](
[id] [int] NULL
) ON [PRIMARY]
GO
CREATE PROC Pro_bb
(@Option VARCHAR(50))
AS
BEGIN
IF @OPTION='a'
BEGIN
TRUNCATE TABLE AA;
SELECT * FROM AA;
----事务1
BEGIN TRY
BEGIN TRAN
INSERT INTO AA SELECT 2
SELECT * FROM AA;
INSERT INTO #BB SELECT 1
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN;
;THROW
END CATCH
END
END

由于临时表#BB不存在,导致插入报错,但是严重级别又小于11导致CATCH接收不到错误,这时查看发现事务处于打开状态,而且表AA也被锁住。

EXEC Pro_bb @OPTION='a';
DBCC OPENTRAN;

加上事务前加上 SET XACT_ABORT ON

ALTER TABLE [dbo].[AA](
[id] [int] NULL
) ON [PRIMARY]
GO
CREATE PROC Pro_bb
(@Option VARCHAR(50))
AS
BEGIN
IF @OPTION='a'
BEGIN
SET XACT_ABORT ON
TRUNCATE TABLE AA;
SELECT * FROM AA;
----事务1
BEGIN TRY
BEGIN TRAN
INSERT INTO AA SELECT 2
SELECT * FROM AA;
INSERT INTO #BB SELECT 1
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN;
;THROW
END CATCH
END
END

再次执行

EXEC Pro_bb @OPTION='a';
DBCC OPENTRAN;

没有处于打开的事务而且事务也执行了回滚操作。

总结

1.建议2012以后版本所有的接收抛错改成使用THROW,不要使用THROW抛错又使用RAISERROR来介绍错误,在事务嵌套事务的写法中如果内部事务使用RAISERROR来接收THROW返回的报错不会执行后面的ROLLBACK。

2.建议在ROLLBACK前统一加上IF @@TRANCOUNT > 0判断条件,这样可以避免因为内部的ROLLBACK回滚或者RETURN操作导致ROLLBACK没有对应的COMMIT。

3.建议不要在事务内使用RETURN返回代码错误位置,RETURN会跳出事务导致提示ROLLBACK没有对应的COMMIT,严重的会导致事务一直处于打开不提交,THROW也可以指定错误位置。

SQL Server 数据库try catch 存储过程的更多相关文章

  1. 生成sql server 数据库 脚本的 存储过程和调用

    USE [db_datadown] GO /****** Object: StoredProcedure [dbo].[GetTBScript] Script Date: 03/05/2015 09: ...

  2. 在SQL Server数据库中执行存储过程很快,在c#中调用很慢的问题

    记录工作中遇到的问题,分享出来: 原博客地址:https://blog.csdn.net/weixin_40782680/article/details/85038281 今天遇到一个比较郁闷的问题, ...

  3. sql server 数据库作业备份存储过程

    DECLARE @fileName nvarchar(100) SET @fileName='D:\HFS\DataBase' + REPLACE(REPLACE(REPLACE(REPLACE(CO ...

  4. SQL Server数据库存储过程的异常处理

    SQL Server数据库存储过程的异常处理是非常重要的,明确的异常提示能够帮助我们快速地找到问题的根源,节省很多时间.本文我们就以一个插入数据为例来说明SQL Server中的存储过程怎么捕获异常的 ...

  5. 在易语言中调用MS SQL SERVER数据库存储过程方法总结

    Microsoft SQL SERVER 数据库存储过程,根据其输入输出数据,笼统的可以分为以下几种情况或其组合:无输入,有一个或多个输入参数,无输出,直接返回(return)一个值,通过output ...

  6. 基于Spring Boot,使用JPA调用Sql Server数据库的存储过程并返回记录集合

    在上一篇<基于Spring Boot,使用JPA操作Sql Server数据库完成CRUD>中完成了使用JPA对实体数据的CRUD操作. 那么,有些情况,会把一些查询语句写在存储过程中,由 ...

  7. Sql Server数据库之存储过程

    阅读目录 一:存储过程概述 二:存储过程分类 三:创建存储过程 1.创建无参存储过程 2.修改存储过程 3.删除存储过程 4.重命名存储过程 5.创建带参数的存储过程   简单来说,存储过程就是一条或 ...

  8. SQL Server数据库存储过程中拼接字符串注意的问题

    在SQL Server数据库中书写复杂的存储过程时,一般的做法是拼接字符串,最后使用EXEC sp_executesql '拼接的字符串' 查询出结果. 先看一段代码: -- ============ ...

  9. SQL Server - 数据库初识

      在互联网笔试中,常遇到数据库的问题,遂来简单总结,注意,以 Sql Server 数据库为例. 数据库 数据库系统,Database System,由数据库和数据库管理系统组成. 数据库,Data ...

随机推荐

  1. Django 分页器

    Django作为Python Web开发框架的一哥,提供了企业级网站开发所需要的几乎所有功能,其中就包括自带分页功能.利用Django自带的Paginator类,我们可以很轻松地实现分页.Django ...

  2. PAT A1055 The World's Richest (25 分)——排序

    Forbes magazine publishes every year its list of billionaires based on the annual ranking of the wor ...

  3. Android 读取后台数据并显示。模拟小区车辆管理系统

    帮别人做的演示系统,只具有基本的增删查改功能. 核心是android端和后台通过http传输数据 后台是asp.net,数据库是ms sql 2008 android端 private void ge ...

  4. 【出错记录】Tomcat非root用户启动无法拥有权限读写文件

    简单记录下,如有必要,将深入补充: 一.非root用户运行Tomcat及原因 由于项目中,为了安全需要,Tomcat将禁止以root形式启动,原因很简单,举个例子,一旦有人恶意将jsp文件透过某个别的 ...

  5. 面试 9:Java 玩转冒泡排序

    面试 9:用 Java 实现冒泡排序 南尘的朋友们,新的一周好,原本打算继续讲链表考点算法的,这里姑且是卡一段.虽然在我们 Android 开发中,很少涉及到排序算法,因为基本官方都帮我们封装好了,但 ...

  6. golang中的context包

    标准库的context包 从设计角度上来讲, golang的context包提供了一种父routine对子routine的管理功能. 我的这种理解虽然和网上各种文章中讲的不太一样, 但我认为基本上还是 ...

  7. 失物找寻APP软件需求规格说明书——第三次团队作业

    ⭐对于软件需求规格说明书的理解 在没写这份软件需求规格说明书的时候我们组成员都不是很理解它的必要性,当然,写完之后才知道它的作用. 软件需求说明书的存在是为了使用户和软件开发者双方对该软件的初始规定有 ...

  8. orleans发送广播消息

    一个client发送消息给orleans, 就只需要掉用Grain的函数就行了. 但是有时候Grain需要发送消息给client, 在orleans里面, 就只能通过Observer来实现. publ ...

  9. docker的4种网络模型

    我们在使用docker run创建Docker容器时,可以用--net选项指定容器的网络模式,Docker有以下4种网络模式: · host模式,使用--net=host指定. · container ...

  10. UVA - 12169 -扩展欧几里得算法

    #include<iostream> #include<string.h> #include<algorithm> #include<stdio.h> ...