我们在写SQL脚本时,特别是针对封装的存储过程,很难保证脚本在执行过程中不会出现异常和错误。一个优秀负责的程序员,会花费足够的时间、预先在脚本中设置异常捕捉和处理语句,以方便脚本的调试和与客户端消息互通。
在PostgreSQL中,可以使用raise抛出异常、使用begin ... exception ... when ... then ... end的方式捕捉异常,MySQL则使用declare ... handler定义异常。SQL Server的异常处理以2008为分水岭,之前的方式非常繁琐、之后的方式则非常清爽。
今天我们就以SQL Server为例,详细说明下如何在SQL脚本中捕捉错误。
MSSQL2000的错误捕捉
在SQLServer2000中,捕捉异常是非常繁琐的。要通过@@ERROR跟踪SQL执行的情况,如果SQL执行正常,@@ERROR值是0,如果出现错误,@@ERROR返回对应的错误号,错误处理就是根据@@ERROR的值进行处理的。
笔者之前在SQLServer2000环境写的存储过程中,每执行一句SQL,就要跟着写一句错误判断,非常繁琐。抓个当年写的片段了解下:
在执行完一条SQL语句之后,马上判断@@error的值,如果@@error<>0,则设置错误提示内容,并使用goto语句跳转到错误处理部分。
MSSQL2008及以后的错误捕捉
可喜的是,从2008开始,SQLServer开始支持使用try...catch方式捕捉和处理SQL异常。不要小看这个变化,这可是质变,从此我们就摆脱了对异常繁琐的跟踪。
我们可以将大段的SQL脚本用try语句包裹起来,在try后面跟着catch语句,当sql执行出现错误时,会自动跳转到catch段落进行错误处理。我们先来看看try...catch的语法格式:
BEGIN TRY SQL段落END TRY BEGIN CATCH 处理段落END CATCH;
语法格式可能与我们想象的有点不一样,搞编程的朋友都知道,比如在Delphi中,使用的try...except...end和try...finally...end捕捉和处理异常;Java和C#的方式最优雅,使用try...catch...finally捕捉和处理异常。MSSQL的try...catch虽然比不过Java和C#的清爽,但比起每执行依据都要判断的方式,不知道要好了多少倍呢。
MSSQL抛出异常之后,提供了一系列的异常函数获取异常的说明,主要包括以下几个内置函数:
- ERROR_NUMBER() 返回错误编号。
- ERROR_SEVERITY() 返回严重性。
- ERROR_STATE() 返回错误状态号。
- ERROR_PROCEDURE() 返回出现错误的存储过程或触发器的名称。
- ERROR_LINE() 返回导致错误的脚本中的行号。
- ERROR_MESSAGE() 返回错误消息的完整文本。
结合这些内置错误说明的函数,我们就可以在catch段落进行相应处理了。下面我们举一个捕捉异常的例子:
begin try declare @x int=1,@y int=0; set @x=@x/@y;end trybegin catch select error_number() as 错误编号, error_severity() as 严重性, error_state() as 错误状态号, error_procedure() as 存储过程, error_line() as 错误行号, error_message() as 错误信息;end catch;
这是一个明显的被零除错误,运行效果参看下图:
脚本中第3行,出现了被零除的错误,在catch段的异常捕捉部分,我们返回了错误的说明。在系统表sys.messages中,存放着所有系统错误的说明,“8134”则对应表中的message_id字段,对应的错误描述如下图所示:
MSSQL每一种内置错误,针对每种支持的语言,都有对应的错误描述,这里我们就不展开说明了。
使用THROW主动抛出异常
很多时候,我们并不是非要等着SQL语句执行错误才返回异常,而是根据业务需要返回提示错误。比如用户输入的用户名密码不匹配,我们抛出异常,告知客户端用户名或密码错,这就要使用throw语句来抛出异常了。
throw的语法格式如下:
THROW 错误编号, 错误信息, 错误状态
- 错误编号:表示异常的常量或变量,类型int,要求大于等于50000且小于等于2147483647,以避开内置错误消息内码。
- 错误信息:描述异常的字符串或变量。message类型为nvarchar(2048)。
- 错误状态:0到 255之间的常量或变量,指示与消息关联的状态。
这里的错误编号,进入catch块后,使用error_numer()进行捕捉,错误信息使用error_message()捕捉,错误状态使用error_state()捕捉,Throw的严重性代码永远都是16。
参看下面的示例脚本:
begin try declare @username nvarchar(100)='张三'; declare @password varchar(255)='12345'; if not (@username='张三' and @password='123456') throw 50000,'用户名或密码错误!',1;end trybegin catch select error_number() as 错误编号, error_severity() as 严重性, error_state() as 错误状态号, error_procedure() as 存储过程, error_line() as 错误行号, error_message() as 错误信息;end catch;
下图是运行效果:
这是人为抛出的异常,与SQL语句执行的异常是不同的,但都会跳转到catch段落统一处理。
一般在存储过程中,我们在try开始前执行begin tran,语句块最后执行commit tran,在catch模块执行rollback tran,从而实现事务的控制。这里就不再赘述了。
如果我们能活用throw触发异常、使用try ... cath ...捕捉和处理异常,我们的脚本就显得更加健壮安全。这对一个优秀的数据库程序员显得非常重要。您说呢?