数据库中存储过程的自动化生成

原文出处: MSDN Magazine April 2003(Advanced T-SQL)

原代码下载: StoredProcedures.exe (108KB)

本文假定你熟悉 T-SQL。
 
摘要

  设计时的自动化加快了编码,并保证所有过程都用相同的命名规范和结构来生成。为了在大型 SQL 项目中尽量提高编码的效率,作者编写了一系列的设计时 (design-time)存储过程,用它们来生成运行时(run-time)存储过程,并一直在工程中使用。最近,作者更新了其存储过程以便使用 SQL Server 2000 的特性, 其中包括用户定义函数。本文所及内容涵盖创建并执行这些动态 T-SQL 脚本以使普通的数据库存储过程编码自动化。
 


  以前,我们开发了一个相当大的n层的客户/服务器项目。在最初计划期间,我们决定用一系列存取数据库中大量表的方法。有四个基本的存储过程用来实现每 个表的 select、insert、uodate 和delete 操作。虽然所需的存储过程在设计上类似,每张表的唯一索引列的结构控制每个独立过程的重要细节会不同,这 无疑会导致存储过程的编写非常单调烦琐。很显然存储过程的编写过程必须实现自动化。
   在任何给定的项目中,从实验着手,建立一套核心的设计时存储过程,用它编写数据库表基本的运行时存储过程集。通过创建并使用这些设计时过程,我们不仅节省了大量时间,同时还为我们的客户节省了金钱,此外还留下了一些我们至今仍在使用的好代码。
  这些设计时存储过程已经被更新,以便利用 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
RETURN 1
END

RETURN 0
END

  用前面获得的 IndexID,我们从 syscolumns 和 sysindexkeys 的联合中获得列名。这些表通过列 ID 和对象 ID 进行联合。通过 WHERE 从句设置检索条件,因此我们可以只选择要求的表的索引列(sik.id = @nTableID),并且仅仅选择主键索引(sik.indid = @nIndexID)。如果 @sColumnName 在返回的列的列表中,返回值1,否则,返回值0,标识匹配没有找到。 

列的默认值 

  当某条记录被插入到表中,如果不给某一指定列提供值,同时该列有一默认值的话,则该默认值将作为该列的值。由于新产生的表插入存储过程有一个参数是用于所有可能被插入 的列,同时变量必须包括一个值,即使是一个空值,该表的默认值不会被使用。本质上,通过明确为每一列提供值(即使是 NULL),我们将改写列的默认值。为了中和我们所创建的存储过程这个特性,我们 必须在插入数据时提供默认值。在本文后面我们将看到如何在自动化存储过程中使用默认值。但是现在,让我们首先来考察如何获得那些默认值。
  我们将使用的 UDF 只是简单地引用了 INFORMATION_SCHEMA.COLUMNS 视图,它提供某列的默认值。与使用 sysconstraints 系统表 相比,用这个视图来获取默认值更容易。下一个 UDF 通过将默认值的查找逻辑包装到一个简单的函数调用中来简化这个过程。

CREATE FUNCTION dbo.fnColumnDefault(@sTableName varchar(128), 
@sColumnName varchar(128))
RETURNS varchar(4000)
AS
BEGIN
DECLARE @sDefaultValue varchar(4000)

SELECT @sDefaultValue = dbo.fnCleanDefaultValue(COLUMN_DEFAULT)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @sTableName
AND COLUMN_NAME = @sColumnName

RETURN @sDefaultValue

END

  列的默认值存储在一对圆括号中,但我们不需要。所以如你所看到的,我们将 COLUMN_DEFAULT  字段传递给另外一个函数 fnCleanDefaultValue,它将园括号 剥离掉,然后返回实际的默认值。
  例如,如果一个叫 nQty 的列有一个默认值 1,COLUMN_DEFAULT 值将当然包括(1)。如果默认值是“Enter Text Here”,我们就得到(“Enter Text Here”)。这里是这个 UDF 的源代码:

CREATE FUNCTION dbo.fnCleanDefaultValue(@sDefaultValue varchar(4000))
RETURNS varchar(4000)
AS
BEGIN
RETURN SubString(@sDefaultValue, 2, DataLength(@sDefaultValue)-2)
END

  现在我们得到了所有创建自动化存储过程所需的元数据信息。

动态执行T-SQL

  动态 T-SQL 的执行是我们的存储过程的精华所在,它允许你写一个通用的 T-SQL 脚本,按次序生成 T-SQL 脚本。正是 T-SQL 的 EXECUTE 命令允许通用的 T-SQL 脚本具体执行专门的输出,并创建将被应用程序使用的运行时存储过程。
  EXECUTE 或者 EXEC 实际上有两种功能:它可以执行一个存在的存储过程和动态执行一个保存在一个字符串中的 SQL 命令。正是后一个功能,我们将使用取得的元数据的联合来自动创建这些存储过程。一个 该过程的简化视图将用需要创建该存储过程的存储过程代码(使用元数据)填充一个大的 varchar 变量,然后一次性动态执行这个 varchar 变量的内容,创建新的存储过程。
让我们着手测试一个动态 T-SQL 的简单例子:

CREATE PROC prGetAuthor
@au_id char(11)
AS
DECLARE @sExec varchar(8000)
SET @sExec = ''SELECT * FROM authors WHERE au_id = '''''' + @au_id + ''''''''

EXEC (@sExec)

  在这个例子中,我们传入 author 的 ID 并将它连接到一个从 author 表获取某个 author 的 SELECT 语句。 

我们象下面这样调用这个存储过程:

  EXEC prGetAuthor ''123-45-6789''

prGetAuthor 存储过程将创建一个如下的 SQL 语句:

  SELECT * FROM authors WHERE au_id = ''123-45-6789''

  这个语句将在 EXEC 中执行并返回 ID 为 123-45-6789 的 author。正如你看到的,设计时存储过程将在很高的水平上使用这个特征。 
  但是我们应注意到这不是动态 T-SQL 推荐的用法。任何时候动态 T-SQL 代码对外部世界是可获得的,这样就存在 SQL 攻击的可能性。我们仅仅使用动态 T-SQ L来实现管理和 任务目的,决不会在除系统人员和管理人员之外的任何人可存取的任何存储过程中暴露这个功能。

创建存储过程 

  创建这些设计时存储过程的第一步是相当的标准。 定义存储过程,声明变量,变量初始化。在建立其它存储过程之前,快速浏览这些代码是否有可疑之处。我们创建两个特别的字符串变量,一个保存 TAB 字符,另一个保存回车换行符。这些 都可用 UDFs 来建立,但是我们决定不这样做,以便作为练习留给读者来完成。它们被用来辅助代码输出的格式化。让我们看一下这个过程的开始,如 Figure 4 所示。
  再次,这里并没有什么 T-SQL 新发现。我们首先检查表是否有主键。这将防止我们的代码创建具有潜在危险的运行时存储过程。接着设置一些变量和默认值。存储过程首先 为新过程建立 DROP 语句,避免存储过程已经存在,再创建一些注释,创建实际的存储过程定义(见 Figure 1 的前面几行)。你可以修改这些代码来创建还不存在的运行时存储过程(如果存在则什么都不做)。这个新特性将由第三个选项参数 @bIfExistsDoNothing 设置。我们将作为一个简单的练习留给读者。 
  下一个代码片段开始创建动态 T-SQL 的过程。为了新存储过程的定义(见 Figure 5 ), 添加删掉某个已存在的存储过程及定义新的存储过程的代码。注意如何使用第二个参数(可选) @bExecute 来确定我们是否要实际运行代码。在我们的自动化存储过程的定义中,这个参数是可选的,默认值为0,意味着并不实际执行代码。 
  下一步我们将使用一个有趣的特性。我们使用 fnTableColumnInfo 用户定义函数作为游标的元数据,fnTableColumnInfo 是一个返回 table 值的函数。这个函数用来取代复杂的 T-SQL,在四个自动化存储过程中,仅仅在游标的声明中引用这个 UDF。声明游标后,我们接着打开它并获取第一条记录,放在一个包含元数据信息的变量中,我们就可以使用它来创建我们的新的存储过程(见 Figure 6 )。 
  当然,我们使用 WHILE 语句来建立一个循环,一直到取得所有的值(@@FETCH_STATUS = 0)。现在我们准备来解析列的信息,并为新存储过程创建关键语句段。 
  在下一个代码例子中,我们使用游标进行循环,并使用列的元数据信息创建代码。你将注意到有三个变量被修改:@sKeyFields, @sSetClause 和 @sWhereClause。第一个用来为存储过程创建参数列表(包括在 Figure 1 中的 CREATE PRDC 段)。第二个用来设置 Figure 1 中 UPDATE 命令的 SET 语句。最后一个变量用来设置 Figure 1 最后的 WHERE 子句。现在我们来验证代码的第一部分(见 Figure 7 )。 
  Figure 7 包括了为新的存储过程创建参数列表的代码。第一个IF语句检查是否准备好了将数据加入到变量中。如果已准备好,我们加入一个逗号和一个回车/换行。我们必 须正确地结束参数列表中的每一个参数。如果没有检查,我们将会以一个或更多逗号结束。在下一列前加入逗号,就可以防止了这个错误。 
  下一步,我们加入一个字符和元数据信息的串联,包括一个TAB字符,一个@字符,列的名字,一个空格和列类型的名字。接着我们看是否需要数据类型的其它信息,检查是否需要精度、范围、长度等信息。如果需要,我们另外加入这些在 圆括号中的值(如 T-SQL 语法的需要)。 
  最后,如果该列不是一个 identity 列,并且该列允许空值或是一个时间戳(不允许被更新因为它是被直接自动更新的),接着我们在参数定义中加入"= NULL"。例如,数据库 pubs 中 discounts 表的列是这样的:

  discounttype varchar(40),
  stor_id char(4) = NULL,
  lowqty smallint = NULL,
  highqty smallint = NULL,
  discount decimal(4, 2)

  注意 discounts 表没有主键,将不允许自动生成代码。这些存储过程依赖于主键来确定数据如何被更新。如果没有主键,这个自动化存储过程应当修改,在新的存储过程的 WHERE 语句中使用所有的列,或者查找一个唯一索引列 给 WHERE 子句使用。换句话说,如果可能,所有表应当有主键,这是数据库设计的基本原则。 
  下一步,看一下为新存储过程的 UPDATE 命令创建的 SET 语句的代码(见 Figure 8)。注意我们如何处理不是主键的列。再次,如果你想更新所有的列,包括主键中的列,你可以简单地删除IF语句。注意这个IF可以是选项特性,由另外一个参数设置。在最后部分,如果需要 的话,我们为变量加入一个逗号。在这个例子中,如果没有数据(意味着我们还没有加入任何列),我们就在变量中设置SET语句来关闭。
  下一步,我们加入一个TAB字符,需要数据更新的列名,和一个等号(=)。在 Order_Details 表中,应由如下代码结束:

   SET UnitPrice = @UnitPrice,
      Quantity = @Quantity,
      Discount = @Discount

  下一步,我们建立为新存储过程创建的 WHERE 语句。你将注意到代码段有一个 ELSE 语句。这是主键检查的例外状态,表示这列是主键的一部分,仅仅运行了这段代码(见 Figure 9)。 
  再次,或者由 WHERE 子句开始这个变量,或者加入一个 AND 子句,这决定于它是否是 WHERE 字句的第一项。接着,我们加入一个TAB字符,列名,字符串"=@"和列名。Order_Details 例子的结果如下面所示:

  WHERE OrderID = @OrderID
     AND ProductID = @ProductID

  在结束 WHILE 循环前,我们需要从游标中获取下一行,并再次将元数据值放在变量中。一旦循环结束,我们关闭和回收游标。现在我们就可以输出任意创建的新运行时存储过程的信息(见 Figure 10
  最后,设计时存储过程将输出生成新运行时存储过程的 T-SQL,首先加入一个回车换行符给SET语句(纯属于格式输出目的)。下一步,我们加入关键字段(存储过程参数)和关键字AS(存储过程定义要求)。 接着,UPDATE 和将被更新的表的名字被加入。最后,我们加入SET语句变量和 WHERE 语句变量,结束存储过程的定义。注意,@sProcText 变量包括为新的运行时存储过程的 T-SQL,可以选择执行。如果被执行,运行时存储过程将被加入到数据库中。

结论

  上述仅仅是我们开发的四个自动化存储过程的一个。当然,每个存储过程将根据需要变化。例如,创建运行时 delete 存储过程的设计时存储过程仅仅使用了每个表的主键。所有用户定义函数和存储过程可以通过本文前面的链接进行下载。
  这些存储过程可以加入许多其它的特性,一些是我们已经提到的,如使用相同的对象名字,对象存在的验证,稽核索引的创建,以及存储过程存在时改变声明 (维护存储过程和混合 XML 则更有效)。你也可以在一个表中创建另外的设置来对代码生成提供帮助。换句话说,这些存储过程可以作为其 它自动代码生成任务的起点。在其它的例子中,这些代码将帮助你节省大量的时间和精力,甚至能帮助你探索其它感兴趣的 T-SQL 技术。

相关文章 

  • .NET Reflection: Dynamically Bind Your Data Layer to Stored Procedures and SQL Commands Using .NET Metadata and Reflection 
  • SQL and XML: Use XML to Invoke and Return Stored Procedures Over the Web 
    Serving the Web: Stored Procedure Wizard in Visual Basic Boosts Productivity 
  • 背景资料
            
    《Inside Microsoft SQL Server 2000》,作者: Kalen Delaney (Microsoft Press, 2000) 
    《The Guru''s Guide to SQL Server Stored Procedures, XML, and HTML》,作者:Ken Henderson (Addison-Wesley Professional, 2001)

    作者介绍

      Peter W. DeBetta:是 一位 Wintellect 培训人员。他使用 Visual Basic、ASP、.NET Framework 和 SQL Server 解决和开发企业级软件。他还和别人合著了一些书籍,包括《including SQL Server 7.0 Programming Unleashed 》(SAMS, 1999)。

      J. Byer Hill:有自己的公司 Advanced Software Logic。Byer 有11年使用微软技术如 Visual       Basic、Visual C++、COM+、ADO、ASP、DHTML 和 SQL Serve 编写和设计数据库的经验。他主要的工作重心是应用系统的构造、设计和高效代码重用。

    译者简介
            

    肖进:南京中萃食品有限公司资讯部,软件工程师。 
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值