SQL Server 存储过程
本章内容简介:
• 存储过程的定义以及何时需要使用一个存储过程
• 如何创建、修改和删除存储过程
• 传递输入和输出参数的方式
• 错误处理
• 性能考虑事项
• 如何使用调试器
存储过程很有用。如果您一直使用的是过程式编程语言,那么本章的内容可能正适合您。现在,看一下SQLServer代码的主要种类,不过在深入研究这一主题之前,需要了解的是——其种类数目可能低于也可能高于您所期望的。从SQL Server 2008以来,您已经有了.NET支持,因此具备了T-SQL存储过程功能——使得原先很多不可能的事都变得可能。
存储过程(stored procedure)有时也称为sproc,它是真正的脚本——或者更准确地说,它是批处理(batch)——它存储于数据库中而不是单独的文件中。无论如何,这个比较并不是很确切——存储过程中有输入参数、输出参数以及返回值等,而脚本中不会有这些内容——但也不是偏得太离谱。
目前,SQL Server中唯一的编程语言依然是T-SQL,如果要说到真正的过程或面向对象编程语言,它离过程语言还是有差距的。但是,如果论及T-SQL的作用——数据的定义、操作和访问,T-SQL要强于C、C++、Visual Basic、Java、Delphi以及其他的语言。但是T-SQL的强大功能在数据访问和管理上也是有限的。简而言之,它有能力完成大部分简单的任务,但它并不总是完成这些任务的最佳方式。
就本章而言,不必太在意T-SQL的缺点——相反,要将精力集中在如何最大限度地利用T-SQL,以及.NET所提供的一些支持。本章将介绍参数、返回值、流程控制、循环结构、基础的和高级的错误捕获等。简而言之,本章将介绍很多主题。所有主要的主题内容都单独分节进行介绍,这样可以每次学习一节的内容,但是首先学习一下创建存储过程的基础知识。
12.1 创建存储过程:基本语法
创建存储过程的方法和创建数据库中的任何其他对象一样,除了它使用的AS关键字外(在介绍视图时出现过AS关键字)。存储过程的基本语法如下所示:
CREATE PROCEDURE|PROC <sproc name>
[<parameter name> [schema.] <datatype> [VARYING] [=<defaultvalue>] [OUT [PUT] ] [READONLY]
[,<parameter name> [schema.] <datatype> [VARYING] [=<defaultvalue>] [OUT [PUT] ] [READONLY]
[, …
…
]]
[WITH
RECOMPILE| ENCRYPTION | [EXECUTE AS {CALLER|SELF|OWNER|<'user name'>} ] [FOR REPLICATION]
AS
<code> | EXTERNAL NAME <assemblyname>.<assembly class>.<method>
在这里,仍然使用相同的基本CREATE <Object Type> <Object Name>语法,它是每个CREATE语句的主干。唯一特别之处是要选择创建PROCEDURE还是PROC。这两个选项都可行,但是和以往一样,建议对所选择的选项保持一致(就个人而言,我喜欢选用PROC,因为输入的字母更少, 但是我也承认PROCEDURE更易于阅读)。存储过程的名称必须遵循第1章中概述的命名规则。
在对存储过程命名之后,接着是参数列表。参数是可选的,本章稍后将对其进行讨论。
最后一点也很重要,就是关键字AS后面的实际代码。
基本存储过程的示例
要演示基本的存储过程语法,最佳示例要属那种最基本的存储过程,该存储过程返回表中所有数据行的所有列——简而言之,就是针对表中的所有数据。
【试一试】 创建基本存储过程
这里,需要您已经知道如何用查询返回表中的所有内容(提示:使用SELECT* FROM...)。 如果不会的话,建议您参考有关基本查询语法的章节。要创建能执行该基本查询功能(这里将对HumanResources模式中的Employee表执行查询)的存储过程,只要在存储过程语法的代码区域中加入查询语句:
USE AdventureWorks;
GO
CREATE PROC spEmployee
AS
SELECT* FROM HumanResources.Employee;
GO
示例说明
这很简单。您可能会问为什么要在CREATE语法之前加上GO关键字(如果只是运行简单的SELECT语句,就不需要使用它),这是因为大多数非表(non-table)的CREATE语句不能与其他任何代码共享批处理。事实上,即使是CREATE TABLE语句,不使用GO也可能有危险。在这里,禁止将USE命令与CREATE PROC语句一起使用,否则会产生错误。
既然创建了存储过程,那么执行它来看一下结果:
EXEC spEmployee;
如果运行嵌入在存储过程中的SELECT语句,就会得到应该得到的结果:
注意:此处删除了结果集中右侧的几列(受本书的页面宽度所限,只能放这么多列)——实际将获得更多的列,如JobTitle、BirthDate等。
这就创建了第一个存储过程。这很简单。坦率地说,对于大部分情况,存储过程的编写并不像数据库工作人员所说的那么难(出于职业保护的目的)。不过,也可能会有比较难的情况,毕竟这里只是介绍初级内容。
12.2 使用ALTER修改存储过程
必须承认的是,本书从视图相关章节中剪切并粘贴了将在本节和12.3节(“删除存储过程”)中讲述的几乎所有内容。说明这一点是想让读者知道从ALTER语句的使用方式来看,不管是视图还是存储过程,其工作原理都是一样的。
在使用T-SQL编辑存储过程时要记住,这是在完全替换现有的存储过程。使用ALTER PROC和CREATE PROC语句的唯一区别包括以下几点:
• ALTER PROC:期望找到一个已有的存储过程,而CREATE则不是。
• ALTER PROC:保留了存储过程上已经建立的任何权限。它在系统对象中保留了相同的对象ID并允许保留依赖关系。例如,如果过程A调用过程B,并删除和重新创建了过程B,那么不能再看到这两者间的依赖关系。如果使用ALTER,则依赖关系仍然存在。
• ALTER PROC:在可能调用被修改的存储过程的其他对象上保留了任何依赖信息。其中最后这一点非常重要。
注意:如果先执行DROP,然后使用CREATE,那么其效果与使用ALTER PROC语句几乎是一样的,除了一个很重要的区别——如果使用DROP和CREATE,则需要完全重建有关允许和不允许谁使用这个存储过程的权限。
12.3 删除存储过程
这是最简单的,通过下列语句:
DROP PROC|PROCEDURE <sproc name>[;]
就完成了存储过程的删除。
12.4 参数化存储过程
存储过程提供了一些(或者对于.NET来说是很多的)过程式的能力,它也提升了性能(稍后将详细介绍),但是如果存储过程没有接受一些数据,告诉其要完成的任务,则在大多数情况下存储过程不会有太多的帮助。例如,对于存储过程spDeleteVendor,如果不指定要删除的供应商,则它不会有太多用处,所以这里使用输入参数。同样地,我们经常想从存储过程中获取信息——不只是一个或多个表数据的记录集,而是更直接的一些信息。例如,更新了表中的一些记录,并且想知道更新的数量。通常,不太容易以记录集的形式获取该信息,而要使用输出参数。
在存储过程的外部,可以通过位置或者引用传递参数。在存储过程的内部,不用关心参数传递的方式——因为它们使用同样的方式声明。
12.4.1 声明参数
声明参数时需要以下2〜4条信息:
• 名称
• 数据类型
• 默认值
• 方向
其语法如下所示:
@parameter_name [AS] datatype [= default|NULL][VARYING] [OUTPUT|OUT]
对于名称,有一组简单的规则。首先,它必须以@符号(和变量一样)开始。此外,除了参数名不能内嵌空格外,其规则和第1章介绍的命名规则是相同的。
数据类型和名称一样,必须像变量那样声明,采用SQLServer内置的或用户自定义的有效数据类型。
注意:声明数据类型时需要记住的一点是,当声明CURSOR类型参数时,必须也使用VARYING和OUTPUT选项。一般很少使用这种类型的参数,而且这也超出了本书的讨论范围,但还是要记住它,以防在联机丛书或其他文档中看到它时产生疑问。
同样要注意的是,OUTPUT可简写为OUT。
在默认值方面,参数与变量类似。使用变量时,可以提供变量,也可以不提供;如果没有提供,则总是初始化为NULL值。类似地,参数可以有默认值,也可以没有。然而,如果不提供默认值,则会假设参数是必不可少的,并且当调用存储过程时需要提供一个初始值。
例如,创建一个与前面的存储过程稍有不同的版本。这次将从Person.Person表提供姓名信息并采用一个针对姓的过滤器:
USE AdventureWorks;
GO
CREATE PROC spEmployeeByName
@LastName nvarchar(50)
AS
SELECT p.LastName, p.FirstName, e.JobTitle,e.HireDate
FROM Person.Person p
JOIN HumanResources.Employee e
ON p.BusinessEntityID = e.BusinessEntityID
WHERE p.LastName LIKE @LastName + '%';
GO
首先提供所需的默认值,尝试这个存储过程:
EXEC spEmployeeByName 'Dobney';
结果是一个非常简短的列表(事实上只是一个雇员):
注意:要谨慎使用通配符匹配,例如前面代码中使用的LIKE语句。这个特别的示例很可能正常工作,因为通配符在最后。记住,在搜索开始处运用通配符可有效避免SQL Server使用索引,因为任何开始字符都可能是匹配的。
现在看一下,如果不提供默认值会发生什么情况:
EXEC spEmployeeByName;
SQL Server立刻通知出错:
Msg 201, Level 16, State 4, ProcedurespEmployeeByNmen, Line 0
Procedure or Function 'spEmployeeByName' expectsparameter '@LastName',
Which was not supplied.
因为没有提供默认值,所以就假定参数是必需的。
1. 提供默认值
为了使参数是可选的,必须提供默认值。这看起来类似于初始化变量:在数据类型后和逗号之前添加“=”符号和作为默认值的值。这样做之后,存储过程的用户可决定对此参数不提供值或是提供他们自己的值。
例如,如果希望前面示例中的参数为可选的,可修改参数声明使之包括默认值:
USE AdventureWorks;
DROP PROC spEmployeeByName; -- Get rid of theprevious version
GO
CREATE PROC spEmployeeByName
@LastName nvarchar(50) = NULL
AS
IF @LastName IS NOT NULL
SELECT p.LastName, p.FirstName, e.JobTitle, e.HireDate
FROMPerson.Person p
JOINHumanResources.Employee e
ON p.BusinessEntityID = e.BusinessEntityID
WHEREp.LastName LIKE @LastName + '%';
ELSE
SELECT p.LastName, p.FirstName, e.JobTitle, e.HireDate
FROMPerson.Person p
JOINHumanResources.Employee e
ON p.BusinessEntityID = e.BusinessEntityID;
GO
注意这里是如何在脚本中使用第11章介绍的流程控制结构来决定运行哪个更好的查询的。两者区别很小,只是添加了WHERE子句。
提供了新默认值后,现在可在无参数的情况下运行存储过程:
EXEC spEmployeeByName;
正如所预料的那样,会得到更完整的结果集:
如果像前面一样,用同样的参数(Dobney)运行该存储过程,那么结果是一样的——唯一不同之处是现在允许参数是可选的,可以处理不提供参数的情况。
2. 创建输出参数
有时希望给调用存储过程的对象传递非记录集的信息。此处在上面两个存储过程基础上创建一个修改版本作为示例。 最常见的就是使用存储过程对表执行生成标识值的插入操作。通常,调用存储过程的代码希望知道这一过程完成时的标识值。
为此,将利用Adventure Works数据库中的一个存储过程——uspLogError,如下所示:
-- uspLogError logs error information in theErrorLog table about the
-- error that caused execution to jump to theCATCH block of a
-- TRY...CATCH construct. This should beexecuted from within the scope
-- of a CATCH block otherwise it will returnwithout inserting error
-- information.
CREATE PROCEDURE [dbo].[uspLogError]
@ErrorLogID [int] = 0 OUTPUT -- containsthe ErrorLogID of the row inserted
AS -- by uspLogError in theErrorLog table
BEGIN
SETNOCOUNT ON;
--Output parameter value of 0 indicates that error
--information was not logged
SET@ErrorLogID = 0;
BEGINTRY
--Return if there is no error information to log
IFERROR_NUMBER() IS NULL
RETURN;
--Return if inside an uncommittable transaction.
--Data insertion/modification is not allowed when
--a transaction is in an uncommittable state.
IFXACT_STATE() = -1
BEGIN
PRINT'Cannot log error since the current transaction is in an
uncommittable state.'
+'Rollback the transaction before executing uspLogError in
order to successfully log errorinformation.';
RETURN;
END
INSERT [dbo].[ErrorLog]
(
[UserName],
[ErrorNumber],
[ErrorSeverity],
[ErrorState],
[ErrorProcedure],
[ErrorLine],
[ErrorMessage]
)
VALUES
(
CONVERT(sysname,CURRENT_USER),
ERROR_NUMBER(),
ERROR_SEVERITY(),
ERROR_STATE(),
ERROR_PROCEDURE(),
ERROR_LINE(),
ERROR_MESSAGE()
);
--Pass back the ErrorLogID of therow inserted
SET @ErrorLogID = @@IDENTITY;
ENDTRY
BEGINCATCH
PRINT'An error occurred in stored procedure uspLogError: ';
EXECUTE[dbo].[uspPrintError];
RETURN-1;
ENDCATCH
END;
注意这里突出显示的部分——它们是输出参数的核心所在。第一个部分声明参数为输出参数。第二个部分利用标识值进行插入,最后,SET语句捕获标识值。如果这一过程存在,@ErrorLogID中的值就传递给调用脚本。
这里利用第11章最后的TRY/CATCH示例,不过这次调用uspLogError:
USE AdventureWorks;
BEGIN TRY
-- Try and create our table
CREATE TABLE OurIFTest(
Col1 int PRIMARY KEY
)
END TRY
BEGIN CATCH
-- Uhoh, something went wrong, see if it's something
-- weknow what to do with
DECLARE @MyOutputParameter int;
IF ERROR_NUMBER() = 2714 -- Object existserror, we knew this might happen
BEGIN
PRINT 'WARNING: Skipping CREATE astable already exists';
EXEC dbo.uspLogError @ErrorLogID =@MyOutputParameter OUTPUT;
PRINT 'An error was logged. The Log IDfor our error was '
+ CAST(@MyOutputParameter ASvarchar);
END
ELSE -- hmm, we don't recognize it, so report it and bail
RAISERROR('something not good happenedthis time around', 16, 1 );
END CATCH
GO
如果在没有OurlFTest表的数据库中运行它,那么将得到简单的结果:
Command(s) completed successfully.
但如果在OurlFTest表已存在的情况下运行上述代码(例如,如果之前未运行过CREATE代码,而运行上述代码两次),那么将得到下列错误:
WARNING: Skipping CREATE as table alreadyexists
An error was logged. The Log ID for your errorwas 1
注意:实际错误日志ID号取决于在您运行该测试前,ErrorLog表中是否插入了其他错误。
现在对错误日志表运行简单的SELECT查询:
SELECT *
FROM ErrorLog
WHERE ErrorLogID = 1; -- change this value towhatever your
-- results said it waslogged as
GO
可看到错误被正确记录了:
对于存储过程本身以及调用脚本对它的使用,需要注意以下几点:
• 对于存储过程声明中的输出参数,需要使用OUTPUT关键字。
• 和声明存储过程时一样,调用存储过程时必须使用OUTPUT关键字。这样就对SQL Server作了提前通知,告诉它参数所需要的特殊处理。但需要注意的是,如果忘记包含OUTPUT关键字,不会产生运行时错误(不会得到关于它的任何消息),但是输出参数的值不会传入变量中(得到的很有可能是NULL值)。这意味着碰到了“非预期的结果”。
• 赋给输出结果的变量不需要和存储过程中的内部参数拥有相同的名称。例如,在前面的存储过程中,内部参数叫做@ErrorLogID,而被传递值的变量叫做@MyOutputParameter。
• EXEC(或EXECUTE)关键字是必需的,因为对存储过程的调用并不是批处理要做的第一件事。如果存储过程的调用是批处理要做的第一件事,则可以不使用EXEC。个人建议您使用它。
12.4.2 通过返回值确认成功或失败
可以看到返回值用于多种用途。首先就是实际地返回数据,例如标识值或者是存储过程影响的行数。而其实际作用是返回值可用来确定存储过程的执行状态。
注意:如果说这里看上去像是介绍如何使用返回值,那么也不用感到奇怪,因为此处确实有此意图。我最初学到的是利用返回值作为一种“技巧”来避免使用输出参数——实际上,是作为一种捷径。和大部分捷径一样,其问题是删减了一些内容,在这里则是删减了相当重要的内容。
当需要返回真正的错误代码时,使用返回值作为给调用例程返回数据的方法会使返回代码的意思变得模糊。简单地说,不要这样做!
返回值指示了存储过程的成功或者失败,甚至是成功或失败的程度或属性。对于C程序员来说,这是相当简单的策略——使用函数的返回值作为成功代码是很常见的,任何非0值都说明某种问题。如果在SQL Server中坚持使用默认的返回代码,则会发现规则同样适用。
12.4.3 如何使用RETURN
事实上,不管是否提供返回值,程序都会收到一个返回值。SQL Server默认会在完成存储过程时自动返回一个0值。
为了从存储过程向调用代码传递返回值,只需要使用RETURN语句:
RETURN [<integer value to return>]
注意:返回值必须为整数。
关于RETURN语句,最重要的是知道它是无条件地从存储过程中退出的。这是指,无论运行到存储过程的哪个位置,在调用RETURN语句之后将不会执行任何一行代码。
这里的无条件并不是说无论执行到代码的何处都将执行RETURN语句。相反,可以在存储过程中有多个RETURN语句,只有当代码的标准条件结构发出命令时,才会执行这些 RETURN语句。一旦这种情况发生,就不能再返回。
此处通过编写一个简单的测试存储过程来说明RETURN语句的影响:
USE AdventureWorks;
GO
CREATE PROC spTestReturns
AS
DECLARE @MyMessage varchar(50);
DECLARE @MyOtherMessage varchar(50);
SELECT@MyMessage = 'Hi, it''s that line before the RETURN';
PRINT@MyMessage;
RETURN;
SELECT@MyOtherMessage = 'Sorry, but we won''t get this far';
PRINT@MyOtherMessage;
RETURN;
GO
注意:该存储过程并未选择在声明中初始化两个消息变量。原因何在?在这里,我认为这样可使代码更具可读性——对于大多数其初始值不只是几个字符的字符串变量来说,情况同样是如此。
现在已经有了存储过程,但需要一小段脚本作测试。您想看到的运行结果是:
• 输出的结果
• RETURN语句返回的值
为了能捕获RETURN语句的值,需要在EXEC语句中把值赋给变量。例如,下面的代码将把返回值赋变量:
EXEC @ReturnVal = spMySproc;
现在使用一个更有用的脚本来测试该存储过程:
DECLARE @Return int;
EXEC @Return = spTestReturns;
SELECT @Return;
GO
虽然简单,但是效果还不错。当运行时,可以看到RETURN语句确实在运行其他代码前终止了代码运行:
同样也得到了该存储过程的返回值,该返回值为0。注意即使没有指定一个特定的返回值,这个返回值仍为0——这是因为默认值为0。
注意:考虑一下这个问题——如果返回值默认为0,就意味着默认的返回值实际上“没有错误”。这有些危险。此处的重点是确保总是显式定义返回值,这样可以确保返回期望的值而不是意外的值。
现在修改存储过程来验证可以用返回值形式传递任何整数值:
USE AdventureWorks;
GO
ALTERPROC spTestReturns
AS
DECLARE @MyMessage varchar(50);
DECLARE @MyOtherMessage varchar(50);
SELECT@MyMessage = 'Hi, it''s that line before the RETURN';
PRINT@MyMessage
RETURN 100;
SELECT@MyOtherMessage = 'Sorry, but we won''t get this far';
PRINT@MyOtherMessage;
RETURN;
GO
再次运行测试脚本,除了返回值改变之外,会得到相同的结果:
12.5 错误处理
第11章在介绍TRY/CATCH块时讲到了一些错误处理的内容。实际上,那些只是在只需要支持SQL Server 2005或更高版本时执行传统错误处理的方法。对于SQL Server错误和错误处理,考虑的问题要比TRY/CATCH块中的多得多。因此,下面将作详细讨论。
注意:的确,可能并不需要这一节的内容。但是代码可能永远没有错误,而且永远都不会碰到问题吗?这大概只是幻想而已,现实情况并非如此。和软件工程中一样,代码总是会出错。幸运的是,可以对它进行处理。但是,可能现有的工具并不理想。然而再次幸运的是,在SQL领域中总有物尽其用的方法,也有许多隐藏错误处理不足之处的方法。
SQL Server中有以下3种常见的错误类型:
• 产生运行时错误并终止代码继续运行的错误。
• SQL Server知道的、但不产生使代码停止运行的运行时错误的错误。这类错误也称为内联错误。
• 更具逻辑性但在SQL Server中不太引起注意的错误。
现在,问题变得有些棘手,并且版本也变得重要,但还是要继续面对它。
注意:在编写本书的时候,大部分关于SQL Server 2012的书还没有出版——但我可以大胆地猜测,大多数的入门书籍不会对以前的版本过多讨论。本书也会尽量回避这个问题,因为这会增加更多的复杂性。不过本节将涉及以前版本的内容。这是因为大多数的数据库开发人员可能还在使用SQL Server 2005以前版本的代码(在TRY/CATCH首次被引入时)。在SQL Server 2000和更早的版本中并没有正式的错误处理程序。
因此,这里会给出说明以前如何错误处理的一个简要介绍——因为它有助于理解可能会在以前代码中碰到的“为什么会以这种方式处理”这样的问题。如果您确定将只会面对“SQL Server 2005或更新版本的代码”的问题,那么可以跳过下面的内容。
旧的错误处理模型和新的错误处理模型之间有一个共同点——更高级别的运行时错误。
可能会产生导致SQL Server立即结束脚本的错误。这在TRY/CATCH引入之前就已经是事实了,而且在TRY/CATCH引入之后也是如此。那些会产生运行时错误的严重性错误从SQL Server方面来讲是有问题的。相比于SQL Server 2005之前的版本,TRY/CATCH逻辑在处理某些错误方面有更多的灵活性,但是仍有可能存在存储过程不知道发生错误的情况,因为存储过程立即终止,未注意到错误(至少存储过程没有注意到)。令人高兴的是,当前所有的数据访问对象模型都通过了这类错误的消息,所以可能在客户端应用程序中知道这些错误并对它们进行处理。
以前的错误处理方式
----------------------------------------------------------------------------------------------------------------------
在SQL Server 2000及更早版本中,没有正式的错误处理程序。无法实现“如果发生错误,就略过这段代码”。而是要在代码中监视错误的条件,然后决定在检测到错误时(很可能是在发生实际错误以后)要做的处理。
12.5.1 处理内联错误
内联错误是指那些能让SQL Server继续运行,但是因为某种原因而不能成功完成指定任务的错误。例如,试着向Person.BusinessEntityContact表中插入一条记录,而在BusinessEntity表或Person表中没有与之对应的记录:
USE AdventureWorks;
GO
INSERT INTO Person.BusinessEntityContact
(BusinessEntityID
,PersonID
,ContactTypeID)
VALUES
(0,0,1);
GO
SQL Server不会执行该插入,因为在BusinessEntitylD和PersonID上有外键约束,这些外键约束引用其他表。由于在两个表中没有匹配记录,因此试图向Person.BusinessEntityContact表插入记录违反了外键约束,并且会被拒绝:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with theFOREIGN KEY constraint "FK_BusinessEntityContact_Person_PersonID".Theconflict occurred in database "AdventureWorks", table"Person.Person", column "BusinessEntitylD".
The statement has been terminated.
注意上面的547错误——这是后面可以利用的错误。
注意:在SQL Server提供的错误中只显示违反了第一个外键。这是因为SQL Server到达了该错误点,并且知道继续运行没有任何意义。如果修正了第一个错误,那么将检测到第二个错误,会再次给出错误消息。
12.5.2 利用@@ERROR
在介绍脚本的时候就已经讨论过这个系统函数了,现在更进一步研究它。
回顾一下,@@ERROR包含了最后执行的T-SQL语句的错误号。如果该值为0,则没有发生错误。这有点类似于在第11章第一次讨论TRY/CATCH块时看到的ERROR_ NUMBER()函数。ERROR_NUMBER()函数只在CATCH块中有效(并且不管在CATCH块中的哪个位置都有效),而@@ERROR根据执行的每条语句接收一个新值。
注意:有关@@ERROR的警告是每条新语句都会使之重置。这意味着如果想延迟分析该值,或者想多次使用它的话,则需要把该值放入其他的保存容器中——即为此而声明的一个局部变量。
使用以前的INSERT示例作演示:
USE AdventureWorks;
GO
DECLARE @Error int;
-- Bogus INSERT - there is no PersonID orBusinessEntityID of 0. Either of
-- these could cause the error we see whenrunning this statement.
INSERT INTO Person.BusinessEntityContact
(BusinessEntityID
,PersonID
,ContactTypeID)
VALUES
(0,0,1);
-- Move our error code into safekeeping. Notethat, after this statement,
-- @@Error will be reset to whatever errornumber applies to this statement
SELECT @Error = @@ERROR;
-- Print out a blank separator line
PRINT '';
-- The value of our holding variable is justwhat we would expect
PRINT 'The Value of @Error is ' +CONVERT(varchar, @Error);
-- The value of @@ERROR has been reset - it'sback to zero
-- since our last statement (the PRINT) didn'thave an error.
PRINT 'The Value of @@ERROR is ' +CONVERT(varchar, @@ERROR);
现在执行脚本,检查一下是如何影响@@ERROR的:
Msg 547, Level 16, State 0, Line 4
The INSERT statement conflicted with theFOREIGN KEY constraint
"FK_BusinessEntityContact_Person_PersonID".The conflict occurred in database
"AdventureWorks", table"Person.Person", cilumn 'BusinessEntitylD' .
The statement has been terminated
The Value of @Error is 547
The Value of @@ERR0R is 0
这很快地说明了通过@@ERROR保存值的问题。第一个错误语句实际上只是一条信息。SQL Server已经抛出了这个错误,但是并没有停止运行代码。实际上,存储过程访问该消息的唯一一个部分就是错误号。这个驻留在@@ERR0R中的错误号只是针对下一条T-SQL语句——在这之后就没有该错误号了。
注意:@Error和@@ERROR是两个截然不同的变量,可以分别引用它们。这不仅是由于大小写的区别(根据配置服务器的方法,区分大小写会影响变量的名称),还因为有作用域的不同。@和@@是名称的一部分,所以前端@符号数目的不同使得这两个变量相互区别。
12.5.3 在存储过程中使用@@ERR0R
首先作个假定:如果使用@@ERROR,那么很可能不使用TRY/CATCH块。如果因为后向兼容问题而未作这一选择,那么此处建议您重新考虑一下——TRY/CATCH其实是更简洁和更通用的方法。
注意:TRY/CATCH将处理之前的版本中终止脚本执行的各种错误。
虽说如此,如果需要向后兼容SQL Server 2000或更早版本,那么TRY/CATCH就不适合,下面简单看一下。
看两个简短的过程。这两者都基于在脚本或之前的存储过程示例中所做的操作,但我们希望看一下内联错误检查的工作方式,用于确定内联错误检查什么时候可以运行,什么时候不能运行,以及为什么不能运行(特别是内联错误检查不能运行,而TRY/CATCH可以运行的情况)。
首先看一下本章前面的参照完整性示例:
USE AdventureWorks;
GO
INSERT INTO Person.BusinessEntityContact
(BusinessEntityID
,PersonID
,ContactTypeID)
VALUES(0,0,1);
GO
您可能知道这会引发简单的547错误。这是可捕获的错误之一。可以通过一个简单的脚本捕获它,但这里使用存储过程,因为在此处处理过程化的内容。
USE AdventureWorks
GO
CREATE PROCspInsertValidatedBusinessEntityContact
@BusinessEntityID int,
@PersonID int,
@ContactTypeID int
AS
BEGIN
DECLARE @Error int;
INSERT INTO Person.BusinessEntityContact
(BusinessEntityID
,PersonID
,ContactTypeID)
VALUES
(@BusinessEntityID, @PersonID, @ContactTypeID);
SET@Error = @@ERROR;
IF@Error = 0
PRINT 'New Record Inserted';
ELSE
BEGIN
IF @Error = 547 -- Foreign Key violation. Tell them about it.
PRINT 'At least one provided parameter was not found. Correct andretry';
ELSE -- something unknown
PRINT 'Unknown error occurred. Please contact your system admin';
END
END
GO
现在试着用适当的值执行上述代码:
EXEC spInsertValidatedBusinessEntityContact 1,1, 11;
插入操作正确进行,没有检测到错误条件(因为没有):
(1 row(s) affected)
New Record Inserted
现在试一下下列代码:
EXEC spInsertValidatedBusinessEntityContact 0,1, 11;
这样不仅看到实际的SQL Server消息,还有错误捕获中的消息。注意,没有办法通过使用内联错误检查消除SQL Server消息;需要TRY/CATCH块才能完成该操作。
Msg 547, Level 16, State 0, ProcedurespInsertValidatedBusinessEntityContact, Line 11
The INSERT statement conflicted with theFOREIGN KEY constraint "FK_BusinessEntityContact_Person_PersonID".The conflict occurred in database "AdventureWorks", table"Person.Person", column '®BusinessEntityID'.
The statement has been terminated.
At least one provided parameter was not found.Correct and retry
如您所见,可以在不使用TRY/CATCH块的情况下检测错误。
现在,通过一个示例说明为什么采用TRY/CATCH块会更好的原因——其中TRY/CATCH块工作得很好,但内联错误检查会失败。为此,需要使用前面给出的TRY/CATCH示例,如下所示:
BEGIN TRY
--Try and create our table
CREATE TABLE OurIFTest(
Col1 int PRIMARY KEY
)
END TRY
BEGIN CATCH
-- Uhoh, something went wrong, see if it's something
-- weknow what to do with
DECLARE @ErrorNo int,
@Severity tinyint,
@State smallint,
@LineNo int,
@Message nvarchar(4000);
SELECT
@ErrorNo = ERROR_NUMBER(),
@Severity = ERROR_SEVERITY(),
@State = ERROR_STATE(),
@LineNo = ERROR_LINE (),
@Message = ERROR_MESSAGE();
IF@ErrorNo = 2714 -- Object exists error, we knew this might happen
PRINT'WARNING: Skipping CREATE as table already exists';
ELSE-- hmm, we don't recognize it, so report it and bail
RAISERROR(@Message, 16, 1 );
END CATCH
GO
这工作得很好。但如果试图使用内联错误检查,就会有问题:
CREATE TABLE OurIFTest(
Col1 int PRIMARY KEY
);
IF @@ERROR != 0
PRINT'Problems!';
ELSE
PRINT'Everything went OK!';
GO
运行上述代码(如果表不存在,需要运行代码两次来产生错误),您很快会发现,没有TRY块,SQL Server在此处生成的特定错误处完全中止脚本运行:
Msg 2714, Level 16, State 6, Line 2
There is already an object named 'OurlFTest' inthe database.
可注意到,PRINT语句没有机会执行——SQL Server已终止运行。通过TRY/CATCH块,可以捕获并处理这一错误;但使用内联错误检查,尝试捕获这样的错误就会失败。
12.5.4 在错误发生前处理错误
有时,有些错误是SQL Server没有办法知道的,更别说进行提醒了。而有时,我们想在错误发生前预防错误。这些都需要检测并自行处理。
为了作演示,这里为 AdventrueWorks 中现有的名为 HumanResources.uspUpdateEmployeeHirelnfo的存储过程创建一个新版本——即 HumanResources.uspUpdateEmployeeHireInfo2。在这里,要采用一些业务规则,它们在本质上是逻辑性的,但在数据库中不一定实现(或在这里,甚至可以用约束处理)。首先看一下现有的存储过程:
USE AdventureWorks;
GO
CREATE PROCEDUREHumanResources.uspUpdateEmployeeHireInfo2
@BusinessEntityID int,
@JobTitle nvarchar(50),
@HireDate datetime,
@RateChangeDate datetime,
@Ratemoney,
@PayFrequency tinyint,
@CurrentFlag dbo.Flag
WITH EXECUTE AS CALLER
AS
BEGIN
SETNOCOUNT ON;
BEGINTRY
BEGIN TRANSACTION;
UPDATE HumanResources.Employee
SET JobTitle = @JobTitle,
HireDate = @HireDate,
CurrentFlag = @CurrentFlag
WHERE BusinessEntityID = @BusinessEntityID;
INSERT INTO HumanResources.EmployeePayHistory
(BusinessEntityID,
RateChangeDate,
Rate,
PayFrequency)
VALUES (@BusinessEntityID, @RateChangeDate, @Rate, @PayFrequency);
COMMIT TRANSACTION;
ENDTRY
BEGINCATCH
-- Rollback any active or uncommittable transactions before
-- inserting information in the ErrorLog
IF @@TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION;
END
EXECUTE dbo.uspLogError;
ENDCATCH;
END;
GO
这个存储过程中的内容是很简单的:有两条语句(一个更新现有雇员记录,另一个处理额外的历史记录),外加一个普通的错误处理程序。要做的是向该存储过程中添加一些新代码,识别可能发生的错误,并提供返回值,向客户端通知更具体的错误信息。这里并不准备花时间捕获潜在的所有错误,但会捕获一些基本的错误以显示其工作原理(读者可自行进一步研究)。
这个存储过程中的错误处理程序是非常普通的,并不会真正做一些特别针对这个存储过程的工作,因此首先考虑一下这个存储过程中可能发生的错误,例如:
• 其BusinessEntitylD不存在的雇员:这个存储过程中的UPDATE语句对于没有有效的BusinessEntitylD的情况会运行良好(没有错误)。它只是会找不到匹配,而不影响任何行;这一错误在本质上是逻辑性的,SQL Server完全不会认为其有错。应自己检测这一错误,并在存储过程继续运行前在此捕获它(因为EmployeePayHistory和Employee之间有外键关系,所以当无法找到匹配的BusinessEntitylD时,SQL Server将在INSERT语句上 引发错误)。
• 两个更新操作在同一RateChangeDate中影响同一BusinessEntitylD:同样,UPDATE语句进行这类更新时没有什么问题,但INSERT语句会出错(EmployeePayHistory的主键由 BusinessEntitylD 和 RateChangeDate 组成)。
现在在新版存储过程中解决这两个问题。
首先,做一些基本工作。尽管SQL Server中没有常量的概念,本书还是将把一些变量作为常量来使用。为此,将不准备只是返回错误号,而是返回一个变量名,通过表明返回错误的性质使代码更具可读性:
…
…
SET NOCOUNT ON;
--Set up "constants" for error codes
DECLARE @BUSINESS_ENTITY_ID_NOT_FOUND int =-1000,
@DUPLICATE_RATE_CHANGE int = -2000;
BEGIN TRY
…
…
注意:您可能会对这里对错误使用负值感到奇怪。尽管这方面没有标准可言,本书还是习惯于用正值表示信息类型的返回代码(可能有多个可能的成功结果,此处希望表明完成的何种成功操作),而用负值表示错误。确保遵循一致性原则。该示例还故意使用从SQL Server 2008开始提供的初始化语法。如果您正在使用较早的版本,则需要采用其他的语法(将声明改为DECLARE @BUSINESS_ENTITY_ID_NOT_FOUND int;SET@BUSINESS_ENTITY_ID_NOT_FOUND = -1000;)。这纯粹是为了向后兼容,因此作相应调整。
接下来,可以测试对HumanResources.Employee的UPDATE操作影响了多少行,并利用测试结果检测 BusinessEntitylD Not Found 错误:
…
…
UPDATE HumanResources.Employee
SET JobTitle = @JobTitle,
HireDate = @HireDate,
CurrentFlag = @CurrentFlag
WHERE BusinessEntitylD = @BusinessEntityID;
IF@@ROWCOUNT > 0
--things happened as expected
INSERT INTO HumanResources.EmployeePayHistory
(BusinessEntitylD,
RateChangeDate,
Rate,
PayFrequency)
VALUES (@BusinessEntityID, QRateChangeDate,@Rate, @PayFrequency);
ELSE
-- ruhroh, the update didn't happen, so skip the insert,
-- setthe return value and exit
BEGIN
PRINT 'BusinessEntitylD NotFound';
ROLLBACK TRAN;
RETURN@BUSINESS_ENTITY_ID_NOT_FOUND;
END
…
…
注意:从UPDATE语句中删除HireDate列。
正如前面所讨论的那样,RETURN将立即退出该存储过程,并给出所提供的返回值(这里是-1000,这是与变量相匹配的值)。客户端应用程序现在可测试该返回值并将返回值与已知的可能错误列表匹配。
注意:如果计划使用常量错误代码,则需要执行某些操作来连接您的值与客户端应用程序中的值。SQL Server并没有真正的“常量”类型,因此需要通过自己的设备来执行该操作,但是至少应该从常见的电子数据表取出值。另一个可能的解决方案是错误值的查找表,存储过程和应用程序代码都使用该表。
现在来看第二个潜在的错误。有多种方法处理该错误。可以预查询EmployeePayHistory表,查看是否已有一个匹配行,然后完全避免INSERT操作。或者,可以允许SQL Server检测该错误,采用错误处理程序处理这种可能性。在这里将采用后者。通常更好的做法都是依据规则,处理异常。本书将这一特别错误视为会经常发生的,因此下面将大胆推测它不会发生,如果发生,就处理它。了解了这一点后,只需要对错误处理程序作些更改:
…
…
BEGIN CATCH
-- Rollback any active or uncommittabletransactions before
-- inserting information in the ErrorLog
IF @@TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION;
END
EXECUTE dbo.uspLogError;
IF ERROR_NUMBER() = 2627 -- Primary Keyviolation
BEGIN
PRINT 'Duplicate Rate Change Found';
RETURN @DUPLICATE_RATE_CHANGE;
END
END CATCH;
…
…
作了这些更改后,看一下这个新的存储过程。尽管这是新的存储过程,还是突出显示了作了更改的代码行:
CREATE PROCEDUREHumanResources.uspEmployeeHireInfo2
@BusinessEntityID [int],
@JobTitle [nvarchar](50),
@HireDate [datetime],
@RateChangeDate [datetime],
@Rate[money],
@PayFrequency [tinyint],
@CurrentFlag [dbo].[Flag]
WITH EXECUTE AS CALLER
AS
BEGIN
SETNOCOUNT ON;
--Set up "constants" for error codes
DECLARE @BUSINESS_ENTITY_ID_NOT_FOUND int = -1000,
@DUPLICATE_RATE_CHANGE int = -2000;
BEGINTRY
BEGIN TRANSACTION;
UPDATE HumanResources.Employee
SET JobTitle = @JobTitle,
HireDate = @HireDate,
CurrentFlag = @CurrentFlag
WHERE BusinessEntityID = @BusinessEntityID;
IF @@ROWCOUNT > 0
-- things happened as expected
INSERT INTO HumanResources.EmployeePayHistory
(BusinessEntityID,
RateChangeDate,
Rate,
PayFrequency)
VALUES
(@BusinessEntityID,
@RateChangeDate,
@Rate,
@PayFrequency);
ELSE
-- ruh roh, the update didn't happen, so skip the insert,
-- set the return value and exit
BEGIN
PRINT 'BusinessEntityID Not Found';
ROLLBACK TRAN;
RETURN @BUSINESS_ENTITY_ID_NOT_FOUND;
END
COMMIT TRANSACTION;
ENDTRY
BEGINCATCH
-- Rollback any active or uncommittable transactions before
-- inserting information in the ErrorLog
IF @@TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION;
END
EXECUTE dbo.uspLogError;
IF ERROR_NUMBER() = 2627 --Primary Key violation
BEGIN
PRINT 'Duplicate Rate Change Found';
RETURN @DUPLICATE_RATE_CHANGE;
END
ENDCATCH;
END;
GO
继续运行下列代码:
DECLARE @Return int;
EXEC @Return =HumanResources.uspEmployeeHireInfo2
@BusinessEntityID = 1,
@JobTitle = 'His New Title',
@HireDate = '1996-07-01',
@RateChangeDate = '2008-07-31',
@Rate= 15,
@PayFrequency = 1,
@CurrentFlag = 1;
SELECT @Return;
GO
一切工作正常,但第二次运行上述代码会得到一些不同的结果:
刚才尝试插入有着同样的支付历史记录的另一行,但SQL Server拒绝了。使用PRINT提供一些信息性输出,因为有可能不是通过“查询”窗口来执行语句;并且还输出了一个特定的返回值,客户端可将该返回值与资源列表中的值相匹配。
现在,尝试同样的基本测试,但使用一个无效的BusinessEntitylD:
DECLARE @Return int;
EXEC @Return =HumanResources.uspEmployeeHireInfo2
@BusinessEntityID = 99999,
@JobTitle = 'My Invalid Employee',
@HireDate = '2008-07-31',
@RateChangeDate = '2008-07-31',
@Rate= 15,
@PayFrequency = 1,
@CurrentFlag = 1;
SELECT @Return;
GO
结果得到一个类似的错误消息和返回代码,但对于检测到的特定错误会略有不同:
注意这不是SQL Server的错误——就SQL Server而言,没有任何问题。这样做的好处是,如果使用客户端应用程序(可以使用C#、VB.NET、C++或其他语言编写),则和重复插入支付历史记录项一样,可以通过一个已知常量来跟踪 - 1000错误并向终端用户发送一条特定的消息。
12.5.5 手动引发错误
有时会出现SQL Server不能识别的错误,但是您希望SQL Server能识别这些错误。例如,在前一个示例中并不想返回- 1000,而是想在客户端创建一个运行时错误,客户端可以使用它来调用错误处理程序并执行相应的操作。为此,可使用T-SQL中的RAISERROR命令。其语法很简单:
RAISERROR (<message ID | message string |variable>,<severity>, <state> [,<argument>
[,<...n>] ] )
[WITH option [,...n] ]
下面介绍RAISERROR命令的具体用法。
1. 消息ID/消息字符串
消息ID或消息字符串决定了向客户端发送的消息。
使用消息ID可以手动引发带有指定的ID以及和该ID相关消息的错误,该错误消息可以在master数据库中的sys.messages系统视图中找到。
注意:如果想要了解SQL Server中预定义的消息,那么可以执行SELECT * FROMmaster.sys.messages。这包括了使用 sp_addmessage 存储过程或通过 SQL Server Management Studio手动添加到系统的任何消息。
可以以特殊文本的形式提供消息字符串,而不用在系统中创建一条更持久的消息:
RAISERROR ('Hi there, I''m an error', 1, 1);
GO
这会引发一条简单的错误消息:
Hi there, I'm an error
Msg 50000, Level 1, State 1
注意,默认指定的消息号是50000,尽管并没有提供该默认值。这是针对任何特别错误的默认错误值。可以使用WITH SETERROR选项来重写该默认值。
2. 严重性
在第11章介绍TRY/CATCH块时提到过此内容。对于熟悉Windows服务器的用户来说, 他们对严重性还是相当熟悉的。严重性表明了错误的严重程度。不过对于SQL Server来说,严重性代码的意义有些古怪。它们包括了信息性的(严重性1〜18)、系统级的(19〜25),甚至是灾难性的(20〜25)。如果引发的错误的严重性是19或更高(系统级的),那么必须要指定WITH LOG选项。20以及更高级别的错误严重性会自动终止用户的连接。
现在回到刚才提到的古怪之处。SQL Server对其行为划分的门类要多于Windows,甚至比联机丛书中提到的都要多。其错误被分成6类,如表12-1所示。
表12-1 SQLServer错误严重性级别
组 |
说 明 |
1~10 |
纯信息错误,但在消息信息中会返回特定的错误代码 |
11~16 |
如果没有设置TRY/CATCH块,那么这些错误会终止过程执行并在客户端引发错误。状态显示为设置的任何值。如果定义了TRY/CATCH 块,那么将调用错误处理程序,而不是在客户端引发错误 |
17 |
通常,只有SQL Server会使用这个错误严重性级别。基本上,它表明 SQL Server用完了资源(例如tempdb已满),而且不能完成请求。同样,TRY/CATCH块将在客户端之前获取该错误 |
18~19 |
这些都是严重的错误,而且暗示系统管理员要注意底层原因。对于错误严重性级别19来说,需要使用WITH LOG选项,事件将在Windows Event Log中显示。这是可用TRY/CATCH块捕获的最后一个级别的错误——超过这一级别的错误将直接在客户端引发 |
20~25 |
遇到这些错误就没有办法了。基本上,它们是致命的错误,会终止连接。和错误级别19 一样,必须使用WITH LOG选项,如果可应用的话,消息将会出现在 Windows Event Log 中 |
状态是一个特殊的值。它会识别在代码中的多个地方发生的相同错误。这就可以发送位置标记表明发生错误的地方。
状态值可以在1〜127之间。如果您正在与Microsoft技术支持人员一起对错误进行排查,那么他们显然拥有一些只为他们所了解的知识,而不能与我们分享状态值所表示的含义。如果向Microsoft寻求技术支持,则技术支持人员很可能会询问并利用这个状态信息。
4. 错误参数
一些预定义的错误会接受参数。这就可能通过改变错误特性来使错误更具动态性。也可以格式化错误消息来接受参数。
如果想在静态错误消息中使用动态信息,就需要格式化消息的固定部分,为消息的参数化部分留下空间。可以通过使用占位符来完成该操作。如果您是一位C或C++程序员,那么您将立刻识别这些参数占位符——它们和printf命令参数是相似的。如果您不是C程序员,那么可能会觉得这有点怪异。所有的占位符都以%符号开始,然后是传递的信息类型(考虑一下数据类型)的编码,如表12-2所示。
表12-2 参数占位符
占位符类型指示符 |
值 类 型 |
D |
有符号的整数;联机丛书中指出it也是可接受的选项,但本书在使用它时碰到了一些问题 |
O |
无符号的八进制数 |
P |
指针 |
S |
字符串 |
U |
无符号的整数 |
X或x |
无符号的十六进制数 |
另外,还有一些使用一些额外的标记和宽度信息来作为占位符的前缀的选项,如表12-3 所示。
表12-3 占位符选项
标 记 |
作 用 |
-(短划线或减号) |
左对齐;只在提供固定宽度时有所不同 |
+(加号) |
如果参数是有符号的数值类型,则指示参数为正或为负 |
0 |
告诉SQL Server在数值的左边填充0,直到满足宽度选项中指定的宽度 |
#(镑符号) |
只应用于八进制和十六进制数;告诉SQL Server根据是八进制还是十六进制来使用适当的前缀(0或0x) |
'' |
如果数值为正,则在该值的左边填充空格 |
最后一点也很重要,可以设定参数的宽度、精度和数据类型的长/短。
• 宽度(Width):通过提供整数值来设定想要为参数值所保留的空间。也可以指定“*”,在这种情况下,SQL Server会自动根据设定的精度值来决定宽度。
• 精度(Precision):确定了数值型数据输出的最大数据位数。
• 长/短(Long/Short):当参数类型为整数、八进制数或十六进制数时,使用h(short)或l(long)来设定。
在下面这个简单示例中应用上述内容:
RAISERROR('This is a sample parameterized %s, along with a zero padding and asign%+010d',1,1, 'string', 12121);
GO
如果执行这段代码,就会得到和引号中稍有不同的信息:
This isa sample parameterized string, along with a zero
paddingand a sign+000012121
Msg50000, Level 1, State 1
提供的额外信息是按顺序插入到占位符中的,最终的值根据指定的形式重新格式化。
5. WITH <option>
目前,在引发错误时可混合搭配以下3个选项:
• LOG:这告诉SQL Server在SQL Server错误日志和Windows应用程序日志中记录错误。对于严重性级别大于或等于19的错误,这个选项是必需的。
• SETERROR:默认情况下,RAISERROR命令不会将@@ERROR设置为所生成的错误值。而@@ERROR反映了实际的RAISERROR命令是成功或失败的。SETERROR重写了这个值并把@@ERROR的值设置为特定的错误ID。
• NOWAIT:这将立即向客户端通知错误。
12.5.6 重新抛出错误
开发人员使用的不限于T-SQL领域的经验法则是只捕获您可以处理的错误。您不知道如何处理的任何错误都应该直接传递给调用者,调用者可能知道(也可能不知道)如何处理该错误,但是至少不需要您来处理。
在SQLServer 2012之前,T-SQL开发人员很难实现该经验法则。CATCH块不允许仅指定要捕获的特定错误,您唯一能够做的(关于向上传递错误)是调用RAISERROR。这在某些方面是可行的,但不是真正的解决方案。RAISERROR可以向上发送错误,但是原始的上下文(例如行号)会丢失。您并不是真正向调用者抛出相同的错误,而只是抛出不完整的副本。
现在情况有了改变。在SQL Server 2012中,如果您没有准备好处理一个错误,则可以向下抛出(THROW)该错误。THROW在某些方面非常类似于RAISERROR,但是有一些重要的区别。首先给出THROW的语法:
THROW [error_number, message, state] [ ; ]
您可能注意到的第一件事情是,THROW的所有参数都是可选的。如果您在CATCH块中,则可以不带任何参数地调用THROW。在这种情况下,它只是重新抛出您最初在CATCH块中引发的错误。这样一来,您就可以亲自处理所有已知的或预期的错误,而将未知的或预料之外的错误向上发送进行处理。
接下来会注意的是,THROW的参数非常类似于RAISERROR的参数,但是没有严重性级别的参数。这是因为在使用指定的参数值调用THROW时,始终使用严重性级别16。注意,16 表示默认情况下不写入Windows日志,但是当前的代码将停止执行(如果您在TRY块中,则控制权将移交给CATCH块)。
THROW和RAISERROR之间的其他区别如下:
• 使用THROW时,error_number参数不需要已经存在于sys.messages中。但是,应该确保在某个位置定义您的错误。
• RAISERROR消息字符串使用printf样式的语法进行动态消息传递,而THROW不这样做。如果您想要自定义抛出的消息,则必须预先连接它。
• THROW之前的语句必须以分号(;)终止。
尝试修改前面的示例,查看THROW的工作方式。下面尝试向具有已定义主键的列添加值:
BEGINTRY
INSERT OurIFTest(Col1) VALUES (1);
END TRY
BEGINCATCH
-- Uh oh, something went wrong, see if it'ssomething
-- we know what to do with
PRINT 'Arrived in the CATCH block.';
DECLARE @ErrorNo int,
@Severity tinyint,
@State smallint,
@LineNo int,
@Message nvarchar(4000);
SELECT
@ErrorNo = ERROR_NUMBER(),
@Severity = ERROR_SEVERITY(),
@State = ERROR_STATE(),
@LineNo = ERROR_LINE (),
@Message = ERROR_MESSAGE();
IF @ErrorNo = 2714 -- Object exists error,not likely this time
PRINT 'WARNING: object already exists';
ELSE -- hmm, we don't recognize it, soreport it and bail
THROW;
ENDCATCH
GO
第一次运行这段代码时,它应该没有任何错误地完成;但是第二次运行这段代码就会产生一个错误。产生错误时,控制权会移交给CATCH块,但会原封不动地将错误重新抛出给调用者(即用户),用户可以根据个人喜好进行处理。
(0row(s) affected)
Msg 2627,Level 14, State 1, Line 3
Violationof PRIMARY KEY constraint 'PK_OurlFTes_A259EE54ABA6728E'.
Cannotinsert duplicate key in object 'dbo.OurlFTest'. The duplicate key value is (1).
12.5.7 添加自定义的错误消息
可以利用特殊的系统存储过程向系统添加自己的消息。这个存储过程称为sp_addmessage,语法如下所示:
sp_addmessage[@msgnum =] <msg id>,
[@severity=] <severity>,
[@msgtext=] <'msg'>
[, [@lang=] <'language'>]
[, [@with_log=] [TRUE|FALSE] ]
[, [@replace=] 'replace']
除了其他的语言和替换参数,以及WITH LOG选项的一点区别外,该存储过程中所有参数的含义与RAISERROR中的是相似的。
1. @lang
这个参数指定了消息应用的语言。在该参数中可以为syslanguages中支持的任何语言指定消息的一个单独版本。
2. @with_log
其工作方式与RAISERROR中的一样,如果设为TRUE,当出现错误的时候,会向SQLServer错误日志和NT应用程序日志自动记录错误消息(只有运行在NT环境下才会实现后一操作)。这里唯一的特别之处就是通过把该参数设置为TRUE而不是使用WITH LOG选项来把消息写入日志。
3. @replace
如果是编辑现有的消息而非创建一个新消息,那么必须把@replace参数设置为REPLACE。倘若不这样做,那么如果消息已经存在,就会得到一个错误。
注意:创建一个额外消息的列表供应用程序使用可以大大增强复用性,但更为重要的是,可以极大地改善应用程序的可读性。想象一下每个数据库应用程序都使用自定义错误代码的常量列表的情况。这样就可以很方便地建立一个常量文件(例如资源或包含库),该文件有一个适当错误的列表;甚至可以创建一个能对部分或所有错误进行通用处理的包含库。简而言之,如果要在同一个环境中创建多个SQL Server应用程序,可考虑使用一个对于所有应用程序公用的错误列表。不过要记住,许多系统管理员并不喜欢特定应用程序的更改影响到master数据库(编写自定义错误消息的地方),因此如果不能完全控制自己的服务器,在编写依赖于自定义错误的代码之前,要确保这些自定义错误消息在服务器上是允许的。
4. 使用sp_addmessage
如前所述,sp_addmessage创建消息的方式和使用RAISERROR创建特殊消息的方式是一样的。
例如,假定AdventrueWorks正实施一个规则,即如果订单超出7天,就不能输入。针对这一规则,可以添加自定义的消息来告诉用户关于订单日期的问题:
sp_addmessage
@msgnum = 60000,
@severity = 10,
@msgtext = '%s is not a valid Order date.
Orderdate must be within 7 days of current date.';
GO
执行该存储过程,它确认添加的新消息:
Command(s)completed successfully.
注意:当运行sp_addmessage时,无论使用何种数据库,实际的消息都被添加到master数据库,任何时候都可通过sys.messages系统视图来查看。这意味着,如果把数据库迁移到一台新的服务器上,则需要在新的服务器上重新添加消息(而以前的消息仍旧在旧服务器的master数据库中)。同样地,强烈建议您把所有自定义的消息存储在一个脚本中,这样可以很方便地将消息添加到新的系统中。
5. 删除已有的自定义消息
要删除自定义消息,可使用下列语句:
sp_dropmessage<message number>
12.6 存储过程的优点
在学习创建存储过程的方法后,我们可能会问“为什么使用它们”这样的问题。其中的一些原因是很基本的,而如果您不熟悉RDBMS,则可能不会立刻想到其原因所在。存储过程的主要优点包括以下几个方面:
• 使得需要过程式动作的进程可调用
• 安全性
• 性能
12.6.1 创建可调用的进程
前面已经提过,存储过程是存储在数据库中的脚本。因为它是数据库对象,好处是可以直接调用它——不需要在执行前从文件中手动地加载存储过程。
存储过程可以调用其他存储过程(称为嵌套)。对于SQLServer 2012来说,可以嵌套32层的深度。这就可以像在传统过程式语言中使用子例程一样,重用单独的存储过程。在一个存储过程中调用另一个存储过程的语法和在脚本中调用存储过程是一样的。
12.6.2 为了安全性而使用存储过程
很多人并没有意识到要充分使用存储过程,使其作为实现安全性的工具。和视图类似,可以创建一个返回记录集的存储过程,而不用赋予用户访问底层数据表的权限。赋予某人执行一个存储过程的权限意味着他们可以在该存储过程中执行任何操作,不过要假设操作是在存储过程的上下文中执行的。也就是说,如果赋予用户执行返回Customers表中所有记录的存储过程的权限,而不是访问实际的Customers表,当用户执行该存储过程时(而尝试直接访问表是行不通的),那么仍然可以从Customers表中得到数据。
真正方便之处在于可以让用户通过存储过程修改数据,但是对于底层表只能允许用户读访问。如果采用存储过程(可能会强制实施一些业务规则),那么可以修改表中的数据。可以通过使用Excel、Access或者其他任何的报表来直接与SQL Server相关联,建立各自的自定义报告,而没有“意外”修改数据的危险。
注意:通过Access或Excel使用户与产品数据库直接相关联是一件对系统非常有用但又很愚蠢的事情。当您对用户授权的同时,从用户可以使用的资源以及他们可以执行的长期查询(自然地,他们并不会注意到这对系统的破坏)来看,实际也是在自找麻烦。
如果您确实想让用户直接访问,那么考虑一下使用镜像、复制或备份以及还原创建一个完全单独的数据库(或仅仅是他们需要访问的表)副本以供他们使用。这有助于保证不会出现记录的锁定、使系统停顿的查询,以及一大堆其他问题。
12.6.3 存储过程和性能
一般来说,存储过程有助于系统性能的提高。但是请记住,和生活中的大多数事情一样,这没有绝对保证——事实上,如果设计的存储过程缺乏智能,那么它会使在其中创建的进程变得非常缓慢。
性能从何而来?当创建一个存储过程时,其进程就像图12-1一样运行。
图 12-1
首先运行CREATE PROC过程。这会分析查询以确保会实际运行这些代码。它与直接运行脚本的一个区别在于CREATE PROC命令可以利用所谓的延迟名称解析。延迟名称解析可以忽略一些对象还不存在的事实。这样就可以稍后创建这些对象。
在创建了存储过程后,它将等待第一次执行。在那时,存储过程被优化,而查询计划被编译并缓存到系统上。后续几次运行该存储过程时,除非通过使用WITH RECOMPILE选项指定,否则都会使用缓存的查询计划,而不是创建一个新的查询计划(也有存储过程被重新编译的情况,不过这超出了本书的讨论范围)。这意味着每次使用该存储过程时,存储过程都会跳过很多优化和编译工作。节省的确切时间取决于批处理的复杂性、批处理中表的大小,以及每个表上索引的数量。通常,节省的时间不是很多——对于大多数场景来说可能是1秒或更少——但通过百分比可以计算出此区别(1秒比2秒快了100%)。当需要进行多次调用时或针对循环的情况,这一区别会变得更明显。
1. 存储过程的不利方面
对于存储过程的不利之处要认识到的最重要的一点是,除非手动地干预(使用WITH RECOMPILE选项),否则只会在第一次运行存储过程时,或者当查询所涉及的表更新了统计信息时,才对存储过程进行优化。
这种“一次优化,多次使用”的策略节省了存储过程的时间,但是该策略也是一把双刃剑。如果查询是动态的(即在使用EXEC命令时建立查询),那么只会在第一次运行时对存储过程进行优化,但是会发现以后再也不进行优化——简而言之,可能会使用错误的计划。
不只是存储过程中的动态查询会导致这样的场景。想象一下网页允许混合搭配几个搜索条件的情况。例如,如果想要向AdventureWorks数据库添加一个存储过程,该存储过程支持一个网页,允许用户根据以下内容来搜索订单:
• Customer name
• Sales Order ID
• Product ID
• Order date
用户可以提供任何混合信息,通过提供的每一条新信息缩小搜索范围,理论上也更加快速。
针对这个问题采用的方法是使用多个查询,并且根据用户提供的信息来选择正确的查询。第一次执行存储过程时,存储过程会运行一些IF...ELSE语句并选择正确的查询来运行。但是, 它只对于这次运行存储过程来说是正确的(其他时候则不一定)。在存储过程第一次选择一个不同的查询运行之后的任何时间,它仍会使用存储过程第一次运行时的查询计划。简而言之,查询性能会受影响。
2. 使用 WITHRECOMPILE选项
可以利用存储过程提供的安全性和代码封装方面的好处,但还是忽略了预编译代码方面的影响。可以回避未使用正确的查询计划的问题,因为可以确保为特定的某次运行创建新的计划。方法就是使用WITH RECOMPILE选项。使用该选项的方式有两种。
• 可以在运行时包含WITH RECOMPILE。只要在执行脚本中包含WITH RECOMPILE:
EXECspMySproc '1/1/2012'
WITH RECOMPILE
GO
这告诉SQL Server抛弃已有的执行计划并且创建一个新的计划——但只是这一次。也就是说,只是这次使用WITH RECOMPILE选项来执行存储过程。如果没有进一步的 操作,SQL就将一直继续使用新的计划。
• 也可以通过在存储过程中包含WITH RECOMPILE选项来使之变得更持久。如果使用这种方式,则在CREATE PROC或ALTER PROC语句中的AS语句前添加WITH RECOMPILE选项即可。
如果通过该选项创建存储过程,那么无论在运行时选择了其他什么选项;每次运行存储过程时都会重新编译它。
12.7 扩展存储过程(XP)
SQL Server中.NET的支持大大改变了扩展存储过程所占的比重。过去,扩展存储过程是核心代码中必不可少的——也就是当SQL Server的基本T-SQL语法和其他功能无法提供所需的内容时,扩展存储过程就可帮助解决问题。
随着.NET可以处理像O/S文件访问和其他外部通信或一些复杂的公式之类的事情,XP被淘汰的日子越来越近。不过,由于下列原因,XP目前还是占有一席之地:
• 性能:由于关键性能的原因,因此希望外部代码真正地在SQL Server进程中运行(这在.NET时代实际是一种极端的做法)。
• 安全性:出于安全的考虑,管理员不允许执行.NET代码(我个人认为,允许XP而不允许.NET是一种愚蠢的做法)。
在本书中,这里只想说明SQL Server允许外部编写的代码作为.DLL在SQL Server进程中运行。XP是使用C或C++创建的。
12.8递归简介
编程中并不经常用到递归。然而,当需要用到它时,您会发现使用递归是再好不过的方法。因此,还是简单回顾一下递归的概念。
简单地说,递归是指一条代码调用自身的情况。其危险性也是非常明显的——如果它调用了自身一次,那么如何防止它反复地调用自身?这完全取决于您自己。也就是说,在代码会递归调用的情况下,您要提供递归检查,确保在适当的时候跳出。
出于演示的目的,此处使用经典的递归示例,它几乎出现在所有有关递归讨论的教材中。它是一个几乎能为任何人所理解的示例,现在来看一下该示例。
这个经典示例是什么?是阶乘(factorial)。对于学过数学(或递归)的人来说,阶乘是指将一个数与比之小1的数相乘,再乘以下一个再小1的数,这样相乘下去,直至乘以1所得到的结果。例如,5的阶乘是120——也就是5X4X3X2X1。
看一下这个递归的存储过程的实现:
CREATEPROC spFactorial
@ValueInint,
@ValueOutint OUTPUT
AS
DECLARE@InWorking int;
DECLARE@OutWorking int;
IF@ValueIn <= 1
BEGIN
SELECT @InWorking = @ValueIn - 1;
EXEC spFactorial @InWorking, @OutWorkingOUTPUT;
SELECT @ValueOut = @ValueIn *@OutWorking;
END
ELSE
BEGIN
SELECT @ValueOut = 1;
END
RETURN;
GO
此处所做的是接受一个值(想计算其阶乘的值)并输出一个值(计算出的阶乘值)。令人惊讶的是,这个存储过程并不是一步完成计算阶乘所需的工作。相反,它取一个数的阶乘值,并且再次调用它本身。第二次调用处理第一次的运行结果,然后再次调用自身。可以一直这样下去,直到到达32层的递归限制。一旦SQL Server进行了32次的递归,则它会引发错误并停止处理。
注意:对.NET程序集的任何调用会作为额外的一层计入递归计数,而在该程序集中的任何行为都不会计入。尽管SQL Server中的.NET功能超出了本书的讨论范围,但仍要记住.NET是解决嵌套层数问题的方法。
使用一个脚本来测试这个递归的存储过程:
DECLARE@WorkingOut int;
DECLARE@WorkingIn int;
SELECT@WorkingIn = 5;
EXECspFactorial @WorkingIn, @WorkingOut OUTPUT;
PRINTCAST(@WorkingIn AS varchar) + ' factorial is ' + CAST(@WorkingOut AS varchar);
GO
得到了预期的结果120:
5factorial is 120
可以尝试为@WorkingIn提供不同的值,除了下面两个重大的问题,一切都运作正常:
• 对于int(甚至是bigint)数据类型,当阶乘的值过大时会发生算术溢出
• 32层的递归限制
可以输入一个大数值来测试一下算术溢出——对于这个示例来说,任何比13大的数都可以引发这种溢出。
测试32层的递归限制需要对存储过程稍做修改。这次求的是三角形数。它和阶乘类似,只是这里使用的是加法而不是乘法。因此,5的三角形数为15(5+4+3+2+1)。此处创建一个新的存储过程来进行测试——除了一些细小的变化外,三角形数和阶乘的存储过程是一样的。
CREATEPROC spTriangular
@ValueInint,
@ValueOutint OUTPUT
AS
DECLARE@InWorking int;
DECLARE@OutWorking int;
IF@ValueIn != 1
BEGIN
SELECT @InWorking = @ValueIn - 1;
EXEC spTriangular @InWorking,@OutWorking OUTPUT;
SELECT @ValueOut = @ValueIn +@OutWorking;
END
ELSE
BEGIN
SELECT @ValueOut = 1;
END
RETURN;
GO
如您所见,没有做多少更改。同样地,只需要对存储过程的调用和测试脚本的PRINT文本作如下改变:
DECLARE@WorkingOut int;
DECLARE@WorkingIn int;
SELECT@WorkingIn = 5;
EXECspTriangular @WorkingIn, @WorkingOut OUTPUT;
PRINTCAST(@WorkingIn AS varchar) + ' Triangular is ' + CAST(@WorkingOut AS varchar);
如果采用@ValueIn值5来运行该存储过程,可得到期望的结果15:
5Triangular is 15
可是,如果使用大于32的@ValueIn值,就会得到以下的错误:
Msg 217,Level 16, State 1, Procedure spTriangular, Line 10
Maximum storedprocedure, function, trigger, or view nesting level exceeded (limit 32).
也有一些解决此问题的方案。但是,除非可以用某种方法将递归调用进行分段(运行到32层的深度,接着从调用堆栈返回,然后再次往下运行),否则是没有什么方法的。只要记住,递归层次太深的函数可以用更标准的循环结构来重写,循环结构没有任何硬性的限制。所以,在必须使用递归之前先看一下是否能使用循环。
12.9 调试
以前(SQLServer 2000时代),Management Studio有真正的实时调试工具。这些调试工具有些笨拙,也就是说它们只能调试存储过程(没有方法只调试脚本,而调试触发器要求创建一个激活触发器的存储过程),但是通过一些替代方案,您也获得了一个寻找良久的调试器。SQL Server 2005出现后,从Management Studio中删除了所有调试功能(把调试功能放到了产品中,但要获得调试功能,必须使用作为SQL Server Data Tools一部分的Visual Studio安装程序——在任何情况下都不是非常方便,但如果出于某些原因而没有安装SSDT,则该安装程序不存在)。令人高兴的是,调试工具目前回到了 Management Studio中,甚至比以前更好了。
12.9.1 启动调试器
SQLServer 2012中的调试器很容易找到。使用调试器的方式与VB或C#中是一样的——就此而言,可能像大多数现代调试器。只需要选择“调试”菜单(当“查询”窗口激活时可用)。 然后从选项中选择启动方式:“开始调试”(Alt+F5)或“逐语句”(F11)。
现在做一些设置,看看调试器在标准脚本和存储过程中如何工作。为此,采用12.8节使用的脚本(用于测试本章前面创建的spTriangular存储过程)。脚本如下所示:
DECLARE@WorkingOut int;
DECLARE@WorkingIn int;
SELECT@WorkingIn = 5;
EXECspTriangular @WorkingIn, @WorkingOut OUTPUT;
PRINTCAST(@WorkingIn AS varchar) + ' Triangular is ' + CAST(@WorkingOut AS varchar);
注意:要使用调试器,必须使用管理员权限运行SQLServer Management Studio。如果没有这样做,您就会获得如图12-2所示的消息。
图 12-2
12.9.2 调试器的组成
当首次弹出“调试”窗口时,需要注意以下问题:
• 左面的红色箭头(如图12-3所示)指示了当前执行行——如果选择“运行”或是开始单步执行代码,那么这就是下一行将要执行的代码。
图 12-3
• 顶部(如图12-4所示)有一些图标来指示不同的选项,包括:
图 12-4
• “继续”:这将运行至存储过程的末尾或下一个断点(包括观察条件)。
• “中断所有”:这将暂停当前调试上下文中的所有处理,并且显示下一行可执行代码。
• “停止调试”:顾名思义,它的功能是立即停止执行。但是调试窗口仍然是打开的。
• “显示下一条语句”:这会将光标移动到将要执行的下一条语句。
• “逐语句”:这将运行下一行代码并在运行接下来的代码行前停止,而不管代码位于哪个过程或函数中。如果执行的当前代码行调用一个存储过程或函数,那么“逐语句”选项会调用该存储过程或函数,把它添加到调用堆栈中,使“本地”窗口显示新嵌套的存储过程而不是父存储过程,并且在嵌套的存储过程的第一行可执行代码处停止。
• “逐过程”:这会执行转到调用堆栈中同一层的下一条语句必需的每一行代码。如果没有调用另外一个存储过程或UDF,那么这个命令和“逐语句”选项一样。如果调用了另一个存储过程或UDF,那么“逐过程”选项会转到紧接着那个存储过程或UDF返回其值的位置的语句。
• “跳出”:这会执行到调用堆栈中下一个最髙点为止的每一行代码。也就是说,会一直运行下去,直到到达了与当前所处的代码调用层次相同的那一层次,然后调试器会再次中断并等待。
• “切换断点”和“移除所有断点”:此外,可以通过单击代码窗口的左边空白区域来设置断点。设置断点是用来告诉SQL Server当在调试模式下运行代码时在此处停止。如果您不想处理大型存储过程或函数的每一行代码,这就很有用——您只是希望它运行到某一点并且每次到达该处时停止。
另外,还有一个选项可以打开“断点”窗口,其中列出了当前设置的所有断点(同样在较大块代码中有用)。还有一些“状态”窗口;下面将介绍其中比较重要的几个窗口。
1. “局部变量”窗口
就像本书一开始所说的那样,假设您在过程语言方面己经有了一些经验。因此,“局部变量”窗口(如图12-5所示,它与图12-3所示的当前语句相匹配)对您来说也不会陌生。简单地说,它显示了当前作用域下所有变量的当前值。如果单步执行嵌套的存储过程并再次停止,“局部变量”窗口的变量列表会随之改变(值也会改变)。记住,这些只是关于下一条将要运行语句的作用域内的那些变量。
图 12-5
在图12-5中,刚开始运行这个存储过程,因此已设置了@ValueIn参数的值,但其他变量和参数还未设置,因此实际上为null。
对于每个变量或参数,都提供了以下3部分的信息:
• 名称
• 当前值
• 数据类型
不过,“局部变量”窗口最好的一个功能是可以编辑每个变量的值。这意味着可以很方便地动态改变变量的值以测试存储过程的一些行为。
2. “监视”窗口
在这里,可以设置要跟踪的变量,而不管当前位于调用堆栈的哪个位置。可以手动输入要观察的变量名,或是可以从代码中选择该变量,右击并选择“添加监视”命令。在图12-6中,对@ValueOut进行观察,但由于在代码中未处理该变量,因此可看到未设置任何值。
图 12-6
在SQLServer 2012中,可以做的一件事情是在“监视”窗口中包括表达式(在之前版本中不能这样做)。您可以包含大多数类型的操作,包括T-SQL查询,前提是它们只返回单个值。在图12-6中,查看对Sales.SalesOrderHeader表中匹@WorkingIn的所有行计数的监视值。
3. “调用堆栈”窗口
“调用堆栈”窗口提供在所运行进程中当前活跃的所有存储过程和函数的清单。当运行嵌套的存储过程或函数时,可以在该窗口中查看嵌套的深度,并且可以改变嵌套的层次,以确认每一层中的当前变量值。
在图12-7中,已经单步执行spTriangular存储过程的代码,因此可看到它正处理工作值3。同样,可只观察“局部变量”窗口中的@ValueIn变量并查看它在单步执行时如何改变。当单步执行时,调用堆栈中有若干spTriangular实例运行(一个针对值5,一个针对值4,现在针对值3),并且指出了当前作用域中的下一条语句是什么。
图 12-7
4. “输出”窗口
“输出”窗口是SQL Server打印输出的地方。这包括了结果集以及存储过程完成运行时的返回值,还包括了来自所调试进程的调试信息。图12-8显示了调试运行过程中间的一些示例输出。
图 12-8
5. “命令”窗口
自从SQLServer 2008以来,“命令”窗口的用处可能要超出其平常所用范围。简而言之,它允许用某种命令行模式访问调试器命令和其他对象。不过,这还是加密的。可采用的命令的示例如下所示:
>Debug.Steplnto
有许多支持IntelliSense功能的命令,但大部分命令在调试时不可用。如果您熟悉Visual Studio调试器命令窗口,那么会发现“命令”窗口与其类似,您可以采用相同的方式使用它。
12.9.3 使用断点进行中断
调试经常被人们认为是一项枯燥乏味的工作,而断点工具可以帮助调试人员降低乏味感。
调试人员通常会发现自己只是完整地执行存储过程(或一系列嵌套的存储过程)并寻找一些bug的成因。调试人员经常指出某个bug不是第一次出现——也就是说,存在一段代码,这段代码本身正常运行,如果发现问题,则该问题包含在其他代码区域中。为了提高效率,您不希望逐步调试所有已知运行良好的代码,期望从中找出问题。这就是断点发挥作用的地方。
前面提及,断点用于向调试器表明“在此处停止”。没有执行逐语句调试、逐过程调试或跳出调试,而是可以仅使用“继续”命令,直接向下运行代码直到下一个断点。这样,您就可以跳过所有已经测试的代码,而直接到达存在问题的部分。
1. 如何设置和使用断点
设置断点的最常见方式是单击您希望停止的代码行并按F9键。令人感兴趣的是,您甚至可以在启动调试器之前设置断点,这些断点将持久保存,当您到达对应的代码位置时会触发它们。您可以根据喜好设置任意多个断点。
在“调试”菜单中,您可以看到操作断点的多种方法。您可以从该菜单中删除所有的断点, 禁用所有的断点,或者启用所有的断点(如果己经禁用了这些断点)。通过禁用断点,可以保留断点位置,在您逐步调试代码时会跳过禁用的断点。
在断点处停止是自动执行的操作。如果您正在使用调试器,就会在所有(启用的)断点处停止。
2. 断点条件
因为断点的作用是缩小搜索范围,所以您希望只有某些时刻在断点处停止。在SQL Server 2012中,您现在可以设置断点条件。
例如,假设您知道较大过程中的某个对象正在调用阶乘函数,而该阶乘函数中包含过大的数值。虽然可以采用许多简单而直观的方法来解决该问题,但是定位存在问题的代码确实会起到帮助作用。
您可以在spFactorial中设置断点,只有当@ValueIn多于给定的值12时才在该断点处停止。当代码在此处中断时,您可以跳出调试并查找造成该过程出现问题的条件。
为了向断点添加条件,可以右击断点(在“断点”窗口中右击断点,右击左列中的断点符号,或者右击“断点”上下文菜单项下的实际代码行)并选择“条件”命令。设置表达式(在此处是@ValueIn>12)和条件(Is True),就完成了所有设置。图12-9给出了完成上述操作后的“断点” 窗口。
图 12-9
3. 使用命中次数
命中次数为断点提供了不同类型的条件。顾名思义,命中次数是在每次到达断点时递增的值。当开始一个调试会话时,命中次数全部为0,它们将在该会话中不断递增。您可以基于如下情况来设置命中次数条件:到达特定的命中次数(“当命中5次时中断”)、次数的倍数(“每命中3次中断”)、或者大于或等于某个次数(“一旦次数到达10,则每次都中断”)。使用命中次数是减少bug搜索范围的另一种方法。
设置命中次数在很大程度上类似于设置断点条件,不同之处在于不是选择“条件”命令,而是选择“命中次数”命令。
4. 断点过滤器
使用过滤器可以限制断点为只在从特定进程或线程中调用时才停止执行。过滤器的使用非常直观:从“断点”菜单中选择“过滤器”命令之后,只需要很少的语法基础知识就可以完成过滤器的设置。
5. 断点位置
在“断点”菜单中,您可以基于行和字符位置设置断点位置;但是您很少需要这样做。该功能有一定的作用,例如当多条语句存在于一行中,并且您希望在第一条语句之外的其他语句上停止时,就可以使用该功能。一般来说,我的解决方案是修正代码,从而在一行上只有一条语句,但如果这会影响到您的工作效率,则可以使用该选项。
12.9.4 使用调试器
既然已了解了一些初步的知识并打开了调试器窗口,下面就开始调试代码。如果您已经开始了一部分调试,则可以选择首先关闭调试器并重启它。
这里的存储过程的第一个可执行代码行具有一些欺骗性——它是@WorkingIn的声明语句。通常,变量声明不是可执行的,但在这里,将初始化变量作为声明的一部分,因此调试器看到了初始化代码。可注意到,现在还未设置任何变量(初始化代码将运行,但还未实际执行)。然后您应该遵循如下步骤:
(1) 往前执行(使用菜单选项、工具提示或按F11键),(通过“局部变量”窗口)将看到 @WorkingIn被初始化为值5——@WorkingOut未作为声明的一部分初始化(参见图12-10)。
(2) 再次使用“逐语句”图标。开始第一次执行spTriangular存储过程并到达存储过程中第一个可执行的IF语句(参见图12-11)。
图 12-10
图 12-11
(3) 因为@ValueIn的值确实不为1,所以单步进入IF语句指定的BEGIN...END块中。特别地,此处转到SELECT语句,该语句初始化@InWorking参数,用于存储过程的特殊执行。稍后将看到,如果@ValueIn的值确实为1,那么执行将立即跳转到ELSE语句。
(4) 再次通过按F11键、使用“逐语句”图标或菜单选项来向前运行一行代码,直到准备进入下一个spTriangular实例之前,如图12-12所示。
图 12-12
注意:特别注意一下“局部变量”窗口中的@InWorking的值。可注意到,它改变为SELECT语句所设置的正确的值(@ValueIn当前为5,所以5 - 1=4)。同样注意一下,“调用堆栈”窗口中只有当前的存储过程实例(外加当前语句)——因为还没有跳转到嵌套的存储过程中,所以只有一个实例。
(5) 现在继续并执行下一条语句。因为这里执行了一个存储过程,所以将在“调试器”窗口中看到一系列的变化(如图12-13所示)。可注意到,指示当前语句的箭头又跳转回IF语句。这是为什么呢?因为这是同一个存储过程的一个新实例。从“调用堆栈”窗口中可看到,现在它有两个列出的存储过程实例。顶部(带有黄色箭头)的实例是当前实例,带有红色断点的实例是等待沿调用堆栈往上的父实例。同样可注意到@ValueIn参数的值为4——这是从存储过程的外部实例传入的值。
图 12-13
(6) 如果想看一下存储过程的外部实例的作用域内的变量值,则双击“调用堆栈”窗口内的该实例行(带有绿色箭头的行),就会看到“调试”窗口中的一些变化,如图12-14所示。
图 12-14
这里需要注意两点:
• 变量的值已经变回存储过程的外部(以及当前选中的)实例作用域内的值。
• 当前执行行的图标有所不同。这个新的绿色箭头意味着这是该存储过程实例的当前行,但它不是整个调用堆栈中的当前行。
(7) 通过双击“调用堆桟”窗口的顶部项可以回到当前实例。接着单步执行3次,就会转到该存储过程的第3个实例的最顶端的一行(IF语句)。注意,此时调用堆栈变成了3层,而且在“局部变量”窗口中的变量值和参数值再次发生了改变。最后一点也很重要,可注意到,这次@ValueIn参数值为3。重复此过程,直到@ValueIn参数值变为1。
(8) 再次单步执行代码,会看到一些行为上的改变。这次,由于@ValueIn中的值为1,因此转到ELSE语句定义的BEGIN...END块中。
(9) 由于已到达底部(或锚点处),下面准备从调用堆栈返回。使用“逐语句”选项执行过程的最后一行,将看到调用堆栈变成4层。同样可注意到,已经适当设置了输出参数 (@OutWorking)。
(10) 这次,尝试使用“跳出”选项(Shift+F11)。如果不仔细观察的话,那么可能会感觉没有什么改变(图12-15显示了“局部变量”和“调用堆栈”窗口)。
注意:这种情况下的结果还是比较有欺骗性的。同样,注意一下“调用堆栈”窗口和“局部变量”窗口中值的变化——这里跳出了当前存储过程的实例并在调用堆栈中向上移了一层。如果继续单步执行代码(F11),那么将完成存储过程的运行并能看到状态窗口的最后形式和它们各自的终值。这里特别需要注意!如果想要看一下真实的终值(例如设置的输出参数),就确保使用“逐语句”选项执行最后一行代码。
注意:如果使用一次执行多行的选项,例如“运行”或“跳出”,则只能得到没有任何最终变量信息的输出窗口。
注意:解决方法是在存储过程最外面的实例中、在希望执行RETURN的位置末尾设置断点。这样的话,可以使用任何调试模式来运行程序,但是仍会在最后中断执行,从而可检查最终的变量。
由此可见,调试器确实是非常便于使用的。
12.10 理解.NET程序集
由于程序集是一个很宽泛的主题,并且程序集会给数据库带来异常的复杂性,所以这里并不过多地讨论它们,只是说明有这样一个概念。
.NET程序集可以和系统关联起来,并用来帮助实现真正复杂的操作。例如,可以在一个用户自定义函数中使用.NET程序集,以从外部数据源(可能是需要动态调用的数据源,如新闻源或股票报价)提供数据,尽管以前的版本是想在所需的结构和复杂通信中消除这一功能的。
目前不过多地讨论程序集的细节,只是看一下向数据库中添加一个程序集的语法:
CREATEASSEMBLY <assembly name> AUTHORIZATION <owner name> FROM <path to assembly> WITH PERMISSION_SET =[SAFE | EXTERNAL_ACCESS | UNSAFE]
这里的CREATE ASSEMBLY和所有CREATE语句的作用一样——它指定了创建的对象类型和对象名称。
接下来是AUTHORIZATION,用于设置运行该程序集的上下文。也就是说,如果它有需要访问的表,那么在AUTHORIZATION中如何设置用户或角色名称将决定是否可以访问这些表。
在这之后是FROM子句。FROM子句指定了该程序集的路径以及该程序集的清单。
最后是WITH PERMISSION_SET,它有以下3个选项:
• SAFE:这个选项指的是安全。它限制程序集访问SQL Server的外部内容。像文件或网络就不能被程序集访问。
• EXTERNAL_ACCESS:允许外部访问,如访问文件或网络,但是要求程序集仍旧以托管代码形式运行。
• UNSAFE:同样,此选项指的是不安全。它不仅允许程序集访问外部系统对象,也能运行非托管代码。
注意:如果不在安全模式下运行.NET程序集,则可能会有危险。即使是在EXTERNAL_ACCESS模式下,该模式也允许系统的用户以别名模式访问网络、文件或其他外部资源——也就是说,他们可能得到您并不想让他们得到的内容,而且在进行访问时,他们在网络上采用的是SQL Server登录名的别名。所以要十分小心这个问题。
12.11 使用存储过程的时机
存储过程是SQL Sever中代码的骨干。您可以创建可重用的代码,同时改进性能和灵活性。您可以使用在其他语言中熟悉的各种编程结构,但是存储过程并不意味着一切。
存储过程的优点包括:
• 通常更佳的性能。
• 可以用作安全隔离层(控制数据库的访问和更新方式)。
• 可重用的代码。
• 划分代码(可以封装业务逻辑:)。
• 根据在运行时建立的动态而可以灵活执行。
存储过程的缺点包括:
• 不能跨平台移植(例如,Oracle有完全不同的存储过程实现)。
• 在一些情况下可能因为错误的执行计划而被锁定(实际地影响性能)。
使用存储过程的优点经常多于缺点。如果您不关心将数据库代码移植到另一个RDBMS(例如Oracle或MySQL),并且有大量的代码需要重复运行,就可以编写存储过程。
12.12 本章小结
本章介绍了很多内容。从开发人员可在SQL Server中实现的功能来看,这一章是本书中最重要的章节。
存储过程被编写为T-SQL的参数化批处理。一旦创建存储过程,就可以独立于底层对象管理存储过程上的权限,从而可以仔细地限制对数据库中对象的访问。此外,可以采用功能强大的错误处理来防止发生严重的系统错误。
第一次运行存储过程时,它会基于当时使用的参数生成查询计划。您可以使用RECOMPILE强制改变这一点,但如果不需要进行修改,则可以通过允许存储和重用该计划来提升性能。
SQLServer Management Studio有一个功能强大的调试器,可以帮助您逐步调试存储过程并查找bug。该调试器在SQL Server 2012中得到了很大的增强,包括添加了条件断点和命中次数。
存储过程不是所有问题的解决方案,但它们仍然是SQL Server编程的基石。在下一章中,您将查看与存储过程密切相关的内容:用户自定义函数(UDF)。
练习题
1. 修改spTriangular存储过程,当使用将超出最大递归层数(32)的输入来调用该存储过程时,向调用者抛出一个合理的错误。
2. 调试spTriangular存储过程,设置只在锚点实例调用(即当@ValueIn=1时)上停止执行的断点。这个断点应该在BEGIN/END块的外部。
► 本章内容总结
主 题 |
概 念 |
存储过程 |
命名的代码批处理,可以由其他SQL批处理调用。创建、修改和删除存储过程类似于对其他SQL对象执行这些操作 |
参数 |
存储过程可以同时具有输入和输出参数。使用以@开头的名称、数据类型和可选的默认值声明参数。必须有不带默认值的任何参数 |
返回值 |
存储过程使用RETURN返回整数值,或者默认返回0。虽然可以任意使用返回值,但可接受的用法是作为结果代码,结果代码0表示成功 |
错误处理 |
在当前的SQL Server版本中,通过TRY/CATCH块完成错误处理。严重性级别超过17的错误是系统错误,不能由TRY/CATCH块处理。但是,我们可以适当地处理其他错误,或者将其抛回给客户端。您可以使用@@ERROR或ERROR_NUMBER()以及相关的函数来提取CATCH块中的错误详情 |
存储过程的目的 |
程序员使用存储过程有3个主要原因:性能、封装和安全性。对于性能来说,存储过程的性能通常好于特殊的查询,这是因为消除了编译时间。对于封装来说,采用了一个已命名的、可调用的过程来封装业务逻辑,从而鼓励代码重用和防止错误。对于安全性来说,您可以限制用户运行存储过程的权限,而不是直接处理表 |
调试器 |
SSMS有一个脚本调试器,可以逐步调试个别的批处理、存储过程、触发器或任意正在运行的T-SQL代码。您可以在“局部变量”窗口中查看局部变量值,或者在“监视”窗口中灵活地设置监视值。可以创建带有各种停止条件的断点,让已知没有错误的代码正常运行,而在怀疑有错误的位置停止 |
.NET程序集 |
可以将.NET代码附加到SQL中,从而可以像调用存储过程一样调用该代码。一旦使用CREATE ASSEMBLY附加了.NET代码,就可以获得.NET代码的全部功能和执行速度。通过可调用的.NET代码允许外部访问是危险的行为,应该仅在配合适当管理资源的情况下执行该操作 |
SQL Server 存储过程相关推荐
- SQL Server存储过程输入参数使用表值
在2008之前如果我们想要将表作为输入参数传递给SQL Server存储过程使比较困难的,可能需要很多的逻辑处理将这些表数据作为字符串或者XML传入. 在2008中提供了表值参数.使用表值参数,可以不 ...
- SQL Server存储过程里全库查找引用的数据库对象(表、存储过程等)
SQL Server存储过程全库匹配数据库对象(表.存储过程等) 简介 可以通过自定义存储过程sp_eachdb来遍历每个数据库然后结合sys.objects 关联sys.sql_modules后的d ...
- SQL server 存储过程的建立和调用
SQL server 存储过程的建立和调用 存储过程的建立和调用 --1.1准备测试需要的数据库:test,数据表:物料表,采购表 if not exists (select * from maste ...
- java调用存储过程 sql server_Java中调用SQL Server存储过程示例
Java中调用SQL Server存储过程示例2007-09-03 08:48来源:论坛整理作者:孟子E章责任编辑:方舟·yesky评论(3) 最近做了个Java的小项目(第一次写Java的项目哦), ...
- Microsoft SQL Server 存储过程
Microsoft SQL Server 存储过程 TRIGGER DDL触发器:主要用于防止对数据库架构.视图.表.存储过程等进行的某些修改:DDL事件是指对数据库CREATE,ALTER,DROP ...
- db2 删除存储过程_数据库教程-SQL Server存储过程使用及异常处理
SQL Server存储过程 存储过程(Procedure)是数据库重要对象之一,也是数据库学习的重点之一.本文,我们以SQL Server为例对存储过程的概念.定义.调用.删除及存储过程调用异常等通 ...
- SQL Server存储过程中使用表值作为输入参数示例
这篇文章主要介绍了SQL Server存储过程中使用表值作为输入参数示例,使用表值参数,可以不必创建临时表或许多参数,即可向 Transact-SQL 语句或例程(如存储过程或函数)发送多行数据,这样 ...
- SQL Server存储过程初学者
In this article, we will learn how to create stored procedures in SQL Server with different examples ...
- oracle如何调试sql,调试oracle与调试sql server存储过程
[IT168 技术]关于存储过程的调试,知道方法以后很简单,但在不知道的时候,为了测试一个存储过程的正性,print,插入临时表等可谓是使出了浑身解数,烦不胜烦.下面就把我工作中调试oracle存储过 ...
- SQL Server 存储过程中使用raiserror抛出异常
转自(SQL Server 存储过程中使用raiserror抛出异常 ) 一 系统预定义错误代码 SQL Server 有3831个预定义错误代码,由master.dbo.sysmessages 表维 ...
最新文章
- 强烈推荐8款高质量的网站,可以解决很多问题
- android设备获取wifi和蓝牙状态并进行打开或关闭操作
- IOS之Swift5.x和OC网络请求JSON
- 深入 JavaScript(4) - new运算符是如何工作的
- 使用app测试Modelarts在线服务
- python输出元组重复的元素_python – 从n个元素生成所有4元组对
- 第二章 TestNG环境搭建
- python 操作同花顺下单程序_Py(76)Python/C API 参考手册:操作系统实用程序
- matlab2c使用c++实现matlab函数系列教程-cat函数
- 数据结构(C语言版 第2版)课后习题答案 严蔚敏 编著
- 外链群发工具-免费外链群发工具
- 四十五、 Redis云平台CacheCloud搭建之二进制文件
- “不学数学就去当厨子”,兰大校友入选全球竞赛最强10人,决赛最后几小时才想起做题...
- 质量保证和质量控制的区别
- 9个妙招教你玩转微信
- 联想大数据“双拳”出击另有深意
- 一方库、二方库、三方库是什么?
- Prometheus部分监控项
- openwrt mwan3配置
- 计算机多媒体论文摘要,急需一篇计算机多媒体论文