在任何给定的项目中,从实验着手,建立一套核心的设计时存储过程,用它编写数据库表基本的运行时存储过程集。通过创建并使用这些设计时过程,我们不仅节省了大量时间,同时还为我们的客户节省了金钱,此外还留下了一些我们至今仍在使用的好代码。
这些设计时存储过程已经被更新,以便利用 SQL Server 2000 的一些新特性,尤其是用户自定义函数(UDFs)特性。因此代码变得非常模块化,并且我们还有额外的函数来完成其它任务。
另一个设计时自动化的好处是保证所生成的数以百计的存储过程都有一致的结构和标准的命名规范。在我们的例子中,所有产生的运行时存储过程的名字被格式化为:prApp_TableName_Task,这里Task 可以是 Select、Update 或者 Delete。用于 Customers 和 Orders 表的存储过程如下所示:
prApp_Customers_Delete
prApp_Customers_Insert
prApp_Customers_Select
prApp_Customers_Update
prApp_Orders_Delete
prApp_Orders_Insert
prApp_Orders_Select
prApp_Orders_Update
正如你看到的,这个规范添加大量的组织到数据库中,使任何存储过程都容易定位并使每个过程的名字都是自描述的。开发人员可以快速发现它并创建代码。最重要的是这个项目未来的团队成员将会发现这些代码和存储过程很容易理解和上手。当然,如果你已经使用了一个不同的命名规范,那么只需简单地改变几行代码,你的命名规范便可被替换使用。
这四个设计时存储过程不是一成不变的,而是可以将它作为模版在其它项目中使用。将它们安装到项目数据库中,如果需要,可以修改它们适应特定应用程序的需要。例如,在我们的几个应用程序中,我们增加代码在单独的数据库中维护每次记录被修改的稽核记录。
一个简单例子 开始前,先看一个使用数据库 Northwind 中 Order_Details 表的简单例子(该表的表名是被修改过的,用下划线取代了空格字符)。尽管空格和其它字符在对象名字中被允许使用,但我们推荐使用常规分割符来命名对象,以防止在使用这些自动化存储过程时可能出现的问题,请参见 SQL Server 在线书籍中“Using Identifiers”部分来获得更多信息。
第一个任务是运行这个设计时存储过程,以创建修改 Order_Details 表数据的运行时存储过程:
EXEC pr__SYS_MakeUpdateRecordProc ''Order_Details''
运行这个设计时存储过程将产生如 Figure 1 所示的 T-SQL 脚本。当这个 T-SQL 脚本运行时,它为 Order_Details 表创建一个新的 update 存储过程。所有的列被说明为新存储过程的参数,但是注意当其它非主键字段是 update 命令的 SET 语句的一部分时,主键列(OrderID 和 ProductID)是如何在 WHERE 字句中出现的。设计时存储过程检查 Order_Details 表存储在 SQL Server 系统表中的元数据(metadata),并用这个信息来创建适当的输出脚本,运行后将创建最后的运行时存储过程。
运行后仅仅产生输出,并且不产生新的运行时存储过程。然而,做个简单的修改,设计时存储过程实际上能运行作为最后输出结果的 T-SQL 脚本。为此,我们只运行设计时存储过程,将值1作为一个标志位,用第二个可选择参数传递,并且再次运行:
EXEC pr__SYS_MakeUpdateRecordProc ''Order_Details'', 1
这此不仅显示以前那样的输出结果,而且运行这个输出结果,由此创建运行时存储过程。
现在我们来看一下这个创建特定应用程序运行时存储过程的设计时存储过程的代码。
SQL Server 系统表和视图 为了创建设计时存储过程,我们必须知道如何从 SQL Server 的系统表和信息视图中获得表的定义。首先,我们必须找到这些列,并找出哪些是主键,每列支持哪些数据类型,以及列是否允许为空。
Figure 2 查看系统表
了解通过修改注册服务器的属性,SQL Server 企业管理器能使你查看系统表是非常有用的,如 Figure 2 所示。如果你在企业管理器中右键单击服务器名字,并选择“Edit SQL Server Registration properties”,将弹出一个对话框。在对话框的下面,你可以看到一个标有“Show system databases and system objects”的复选框。选中这个选项便打开了系统对象视图,也可以选择关闭来使表的视图看起来更简单和更易读。
解析表列 Syscolumns 表提供了许多必须的元数据信息,例如列名、ID、长度和是否允许空值。它还被用来连接 sysindexes 表来确定表的主键。同样可以通过 INFORMATION_SCHEMA.COLUMNS 视图获取列的默认值。
既然所有的存储过程都使用相同的元数据信息,那么出于模块化和可维护性考虑,将其封装在独立的代码块中是件非常好的事情。SQL Server 的早期版本没有UDF(用户定义函数),使得模块化看起来很困难。但是 SQL Server 2000 具备了 UDF 特性,我们决定进一步采用该代码并将四个设计时存储过程中的公共特性进行模块化。创建五个新的 UDFs 来处理系统表和信息大纲视图,封装所有取得的元数据。
毫无疑问,为了创建新的运行时存储过程,我们需要知道下面的关于表的元数据列信息:
列名
列的ID号
列的数据类型
列最大长度(包括字符和二进制数据)
列的精度,或者值的位数(decimal和numeric数据)
列的数值范围,或者小数点后的位数(decimal和numeric数据)
列是否允许为null
列是否是主键的一部分
列是否是 Identity 列
列的默认值
这些信息的大多数来自 syscolumns 表,只有两个除外。默认值实际上来自INFORMATION_SCHEMA.COLUMNS 视图。数据类型名(datatype name)从 systypes 表中吸取,并且通过一个更复杂的 syscolumns,sysindexes 和 sysindexkeys 表联合来确定一个列是否是主键的一部分。它是如此的复杂,以至于我们将该功能封装到其自己的 UDF 中。
让我们看一下 Figure 3 中的主要功能,它揭示了更多的元数据信息。这个 UDF 不是太复杂。正如你看到的,大多数元数据信息——除了一些简单的列重命名——被返回时未做任何修改,包括列名、列 ID、长度、精度、范围、是否允许为空和数据类型名字。接下来需要对这些信息做一点额外的工作。对于主键元数据,我们已创建另外一个 UDF 来确定表中一列是否是某个表主键的一部分。我们将马上检查这些额外的 UDFs 函数。
让我们看一下 alternate 类型和 identity 状态。Syscolumns 状态字段的第8位(128)指示该列是否是一个 identity 列。(这对于了解何时创建 Insert 和 Update 脚本非常重要)。我们的简单公式对这个值实施一个逻辑与 (&),并将该结果包装在 Sign 函数中。如果该位被设置,则意味该列是 identity 列。c.status & 128 将返回值 128 。否则,将返回值 0 。Sign 函数当为正值时返回1,负数时返回 -1,0 值时返回 0。因此,如果列被评估为 identity 列,将返回值 1,否则返回0。
alternate 类型被用来表示该数据类型在定义时是否要求额外的信息(长度、精度或范围)。我们将 character 和 binary 数据类型作为值是 1 的 alternate 类型,decimals 和 numerics 作为2,其它数据类型作为 0。这个值在存储过程用来确定长度、精度和范围是否需要加入到参数定义中。
查找主键列 正如你看到的,查找列的信息并不是很难。查找一个字段是否是主键的一部分稍微要费点力。有一个字段列表是能获取的。但要找到这些字段得在 syscolumns、sysindexes 和 sysindexkeys 表联合中并与我们请求的列进行比较(在 @sColumnName 参数中被传递到 UDF)。因此,查找主键的任务在单独的用户定义函数中较容易完成,因为我们可以将这个工作封装到某个单一的函数调用中。
让我们考察一下这个函数,看看所发生的真相:
CREATE FUNCTION dbo.fnIsColumnPrimaryKey
(@sTableName varchar(128), @sColumnName varchar(128))
RETURNS bit
AS
BEGIN
DECLARE @nTableID int,
@nIndexID int,
@i int
SET @nTableID = OBJECT_ID(@sTableName)
这个函数包含两个参数,表名和列名,如果指定列是表中的主键的一部分,函数将返回一个 bit 标志。我们接着声明一些存储过程中要用到的变量,并且赋予初始值。现在来到有趣的部分:查找主键信息。我们开始为表的主键索引找到索引ID,如下面的代码所示:
SELECT @nIndexID = indid
FROM sysindexes
WHERE id = @nTableID
AND indid BETWEEN 1 And 254
AND (status & 2048) = 2048
ORDER BY indid
IF (@nIndexID Is Null)
RETURN 0
现在将这个表的主键索引的索引 ID 赋予变量 @nIndexID。状态列的第12位(2048)指示是否是主键索引。如果不是主键,则没有记录被返回,并将 @nIndexID 设置为空值。如果退出函数时@nIndexID 包含一个空值,返回0值。换句话说,如果没有主键索引,那么列就不是主键的一部分。现在我们再次在主键索引列的列表中检查要求的列 (@sColumnName)。 IF @ColumnName IN
(SELECT sc.[name]
FROM sysindexkeys sik
INNER JOIN syscolumns sc ON sik.id = sc.id AND sik.colid =
sc.colid
WHERE sik.id = @nTableID
AND sik.indid = @nIndexID )
BEGIN