本文介绍什么是存储过程,为什么要使用存储过程,如何使用存储过程,以及创建和使用存储过程的基本语法。
1. 存储过程
迄今为止,我们使用的大多数SQL语句都是针对一个或多个表的单条语句。并非所有操作都这么简单,经常会有一些复杂的操作需要多条语句才能完成,例如以下的情形:
-
为了处理订单,必须核对以保证库存中有相应的物品。
-
如果物品有库存,需要预定,不再出售给别的人,并且减少物品数据以反映正确的库存量。
-
库存中没有的物品需要订购,这需要与供应商进行某种交互。
-
关于哪些物品入库(并且可以立即发货)和哪些物品退订,需要通知相应的顾客。
这显然不是一个完整的例子,但足以表达我们的意思了。执行这个处理需要针对许多表的多条SQL语句。此外,需要执行的具体SQL语句及其次序也不是固定的,它们可能会根据物品是否在库存中而变化。
那么,怎样编写代码呢?可以单独编写每条SQL语句,并根据结果有条件地执行其他语句。在每次需要这个处理时(以及每个需要它的应用中),都必须做这些工作。
可以创建存储过程。简单来说,存储过程就是为以后使用而保存的一条或多条SQL语句。可将其视为批文件,虽然它们的作用不仅限于批处理。
说明:不适用于SQLite
SQLite 不支持存储过程。
说明:还有更多内容
存储过程很复杂,全面介绍它需要很大篇幅。市面上有专门讲存储过程的书。本课不打算讲解存储过程的所有内容,只给出简单介绍,让读者对它们的功能有所了解。因此,这里给出的例子只提供Oracle和SQL Server的语法。
2. 为什么要使用存储过程
我们知道了什么是存储过程,那么为什么要使用它们呢?理由很多,下面列出一些主要的:
-
通过把处理封装在一个易用的单元中,可以简化复杂的操作(如前面例子所述)。
-
由于不要求反复建立一系列处理步骤,因而保证了数据的一致性。如果所有开发人员和应用程序都使用同一存储过程,则所使用的代码都是相同的。
-
防止错误。需要执行的步骤越多,出错的可能性就越大。防止错误保证了数据的一致性。
-
简化对变动的管理。如果表名、列名或业务逻辑(或别的内容)有变化,那么只需要更改存储过程的代码。使用它的人员甚至不需要知道这些变化。
-
安全性。通过存储过程限制对基础数据的访问,减少了数据讹误(无意识的或别的原因所导致的数据讹误)的机会。
-
因为存储过程通常以编译过的形式存储,所以DBMS处理命令所需的工作量少,提高了性能。
-
存在一些只能用在单个请求中的SQL元素和特性,存储过程可以使用它们来编写功能更强更灵活的代码。
换句话说,使用存储过程有三个主要的好处,即简单、安全、高性能。显然,它们都很重要。不过,在将SQL代码转换为存储过程前,也必须知道它的一些缺陷。
-
不同DBMS中的存储过程语法有所不同。事实上,编写真正的可移植存储过程几乎是不可能的。不过,存储过程的自我调用(名字以及数据如何传递)可以相对保持可移植。因此,如果需要移植到别的DBMS,至少客户端应用代码不需要变动。
-
一般来说,编写存储过程比编写基本SQL语句复杂,需要更高的技能,更丰富的经验。因此,许多数据库管理员把限制存储过程的创建作为安全措施(主要受上一条缺陷的影响)。
尽管有这些缺陷,存储过程还是非常有用的,并且应该使用。事实上,多数DBMS都带有用于管理数据库和表的各种存储过程。更多信息请参阅具体的DBMS文档。
说明:不会写存储过程?你依然可以使用
大多数DBMS将编写存储过程所需的安全和访问权限与执行存储过程所需的安全和访问权限区分开来。这是好事情,即使你不能(或不想)编写自己的存储过程,也仍然可以在适当的时候执行别的存储过程。
3. 执行存储过程
存储过程的执行远比编写要频繁得多,因此我们先介绍存储过程的执行。执行存储过程的SQL语句很简单,即EXECUTE
。EXECUTE
接受存储过程名和需要传递给它的任何参数。请看下面的例子(你无法运行这个例子,因为AddNewProduct
这个存储过程还不存在):
sql
EXECUTE AddNewProduct('JTS01',
'Stuffed Eiffel Tower',
6.49,
'Plush stuffed toy with the text La Tour Eiffel in red white and blue');
分析: 这里执行一个名为AddNewProduct
的存储过程,将一个新产品添加到Products
表中。AddNewProduct
有四个参数,分别是:供应商ID(Vendors
表的主键)、产品名、价格和描述。这4个参数匹配存储过程中4个预期变量(定义为存储过程自身的组成部分)。此存储过程将新行添加到Products
表,并将传入的属性赋给相应的列。
我们注意到,在Products
表中还有另一个需要值的列prod_id
列,它是这个表的主键。为什么这个值不作为属性传递给存储过程?要保证恰当地生成此ID,最好是使生成此ID的过程自动化(而不是依赖于最终用户的输入)。这也是这个例子使用存储过程的原因。以下是存储过程所完成的工作:
-
验证传递的数据,保证所有4个参数都有值;
-
生成用作主键的唯一ID;
-
将新产品插入
Products
表,在合适的列中存储生成的主键和传递的数据。
这就是存储过程执行的基本形式。对于具体的DBMS,可能包括以下的执行选择:
-
参数可选,具有不提供参数时的默认值。
-
不按次序给出参数,以"参数=值"的方式给出参数值。
-
输出参数,允许存储过程在正执行的应用程序中更新所用的参数。
-
用
SELECT
语句检索数据。 -
返回代码,允许存储过程返回一个值到正在执行的应用程序。
4. 创建存储过程
正如所述,存储过程的编写很重要。为了获得感性认识,我们来看一个简单的存储过程例子,它对邮件发送清单中具有邮件地址的顾客进行计数。
下面是该过程的Oracle版本:
sql
CREATE PROCEDURE MailingListCount (
ListCount OUT INTEGER
)
IS
v_rows INTEGER;
BEGIN
SELECT COUNT(*) INTO v_rows
FROM Customers
WHERE NOT cust_email IS NULL;
ListCount := v_rows;
END;
分析: 这个存储过程有一个名为ListCount
的参数。此参数从存储过程返回一个值而不是传递一个值给存储过程。关键字OUT
用来指示这种行为。Oracle支持IN
(传递值给存储过程)、OUT
(从存储过程返回值,如这里)、INOUT
(既传递值给存储过程也从存储过程传回值)类型的参数。存储过程的代码括在BEGIN
和END
语句中,这里执行一条简单的SELECT
语句,它检索具有邮件地址的顾客。然后用检索出的行数设置ListCount
(要传递的输出参数)。
调用Oracle例子可以像下面这样:
sql
var ReturnValue NUMBER
EXEC MailingListCount(:ReturnValue);
SELECT ReturnValue;
分析: 这段代码声明了一个变量来保存存储过程返回的任何值,然后执行存储过程,再使用SELECT
语句显示返回的值。
下面是该过程的SQL Server版本:
sql
CREATE PROCEDURE MailingListCount
AS
DECLARE @cnt INTEGER
SELECT @cnt = COUNT(*)
FROM Customers
WHERE NOT cust_email IS NULL;
RETURN @cnt;
分析: 此存储过程没有参数。调用程序检索SQL Server的返回代码提供的值。其中用DECLARE
语句声明了一个名为@cnt
的局部变量(SQL Server中所有局部变量名都以@
起头);然后在SELECT
语句中使用这个变量,让它包含COUNT()
函数返回的值;最后,用RETURN @cnt
语句将计数返回给调用程序。
调用SQL Server例子可以像下面这样:
sql
DECLARE @ReturnValue INT
EXECUTE @ReturnValue = MailingListCount;
SELECT @ReturnValue;
分析: 这段代码声明了一个变量来保存存储过程返回的任何值,然后执行存储过程,再使用SELECT
语句显示返回的值。
下面是另一个例子,这次在Orders
表中插入一个新订单。此程序仅适用于SQL Server,但它说明了存储过程的某些用途和技术:
sql
CREATE PROCEDURE NewOrder @cust_id CHAR(10)
AS
-- 为订单号声明一个变量
DECLARE @order_num INTEGER
-- 获取当前最大订单号
SELECT @order_num = MAX(order_num)
FROM Orders
-- 决定下一个订单号
SELECT @order_num = @order_num + 1
-- 插入新订单
INSERT INTO Orders(order_num, order_date, cust_id)
VALUES(@order_num, GETDATE(), @cust_id)
-- 返回订单号
RETURN @order_num;
分析: 此存储过程在Orders
表中创建一个新订单。它只有一个参数,即下订单顾客的ID。订单号和订单日期这两列在存储过程中自动生成。代码首先声明一个局部变量来存储订单号。接着,检索当前最大订单号(使用MAX()
函数)并增加1(使用SELECT
语句)。然后用INSERT
语句插入由新生成的订单号、当前系统日期(用GETDATE()
函数检索)和传递的顾客ID组成的订单。最后,用RETURN @order_num
返回订单号(处理订单物品需要它)。请注意,此代码加了注释,在编写存储过程时应该多加注释。
说明:注释代码
应该注释所有代码,存储过程也不例外。增加注释不影响性能,因此不存在缺陷(除了增加编写时间外)。注释代码的好处很多,包括使别人(以及你自己)更容易地理解和更安全地修改代码。
对代码进行注释的标准方式是在之前放置--
(两个连字符)。有的DBMS还支持其他的注释语法,不过所有DBMS都支持--
,因此在注释代码时最好都使用这种语法。
下面是相同SQL Server代码的一个很不同的版本:
sql
CREATE PROCEDURE NewOrder @cust_id CHAR(10)
AS
-- 插入新订单
INSERT INTO Orders(cust_id)
VALUES(@cust_id)
-- 返回订单号
SELECT order_num = @@IDENTITY;
分析: 此存储过程也在Orders
表中创建一个新订单。这次由DBMS生成订单号。大多数DBMS都支持这种功能;SQL Server中称这些自动增量的列为标识字段(identity field),而其他DBMS称之为自动编号(auto number)或序列(sequence)。传递给此过程的参数也是一个,即下订单的顾客ID。订单号和订单日期没有给出,DBMS对日期使用默认值(GETDATE()
函数),订单号自动生成。怎样才能得到这个自动生成的ID?在SQL Server上可在全局变量@@IDENTITY
中得到,它返回到调用程序(这里使用SELECT
语句)。
可以看到,借助存储过程,可以有多种方法完成相同的工作。不过,所选择的方法受所用DBMS特性的制约。
5. 小结
本文介绍了什么是存储过程,为什么使用存储过程。我们介绍了执行和创建存储过程的语法,使用存储过程的一些方法。存储过程是个相当重要的主题,一课内容无法全部涉及。各种DBMS对存储过程的实现不一,你使用的DBMS可能提供了一些这里提到的功能,也有其他未提及的功能,更详细的介绍请参阅具体的DBMS文档。