服务器 频道

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

  列的默认值
  
  当某条记录被插入到表中,如果不给某一指定列提供值,同时该列有一默认值的话,则该默认值将作为该列的值。由于新产生的表插入存储过程有一个参数是用于所有可能被插入的列,同时变量必须包括一个值,即使是一个空值,该表的默认值不会被使用。本质上,通过明确为每一列提供值(即使是 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 则更有效)。你也可以在一个表中创建另外的设置来对代码生成提供帮助。换句话说,这些存储过程可以作为其它自动代码生成任务的起点。
0
相关文章