首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

7.8  CLR存储过程

SQL Server 2005允许使用任意 .NET语言开发CLR存储过程(以及其他程序一样)。上一章介绍了一些关于CLR程序的基础知识,给出了在什么情况下用CLR程序代替T-SQL的建议,并描述了如何开发CLR程序。 附录A包含了开发、生成、部署和测试 .NET代码的操作指南。在这里,我将演示两个CLR存储过程的示例,它提供了T-SQL代码所不具备的功能。

第一个示例是一个名为usp_GetEnvInfo 的CLR存储过程。该存储过程收集环境变量中的信息并把它们以表格形式返回。该存储过程返回的环境变量包括:机器名称、处理器、OS版本、CLR版本。

为了收集环境变量中的信息,程序集需要对操作系统资源进行外部访问。创建程序集时(使用CREATE ASSEMBLY命令),默认使用限制最高的PERMISSION_SET选项SAFE,这意味它们被限制为只能访问数据库资源。这是推荐的选项,这样可以获取最大的安全性和稳定性。权限集选项EXTERNAL_ACCESS 和UNSAFE(在CREATE ASSEMBLY 或 ALTER ASSEMBLY命令指定,或者在Visual Studio 中工程 | 属性对话框上的数据库选项卡指定)允许对文件、网络、环境变量或注册表这样的系统资源进

行外部访问。要允许EXTERNAL_ACCESS 和UNSAFE程序集运行,你需要把数据库选项TRUSTWORTHY设置为ON。允许EXTERNAL_ACCESS 或UNSAFE程序集运行会存在安全风险,应该避免这样做。稍后我将描述一个更为安全的解决方案,我先演示EXTERNAL_ACCESS选项。要把CLRUtilities数据库的TRUSTWORTHY选项设置为ON,并把CLRUtilities程序集的权限集改为EXTERNAL_ACCESS,运行下面的代码:

-- 使用EXTERNAL_ACCESS 权限集需要把数据库选项TRUSTWORTHY 设置为ON

ALTER DATABASE CLRUtilities SET TRUSTWORTHY ON;

GO

-- 修改程序集PERMISSION_SET = EXTERNAL_ACCESS

ALTER ASSEMBLY CLRUtilities

WITH PERMISSION_SET = EXTERNAL_ACCESS;

这时你就可以运行usp_GetEnvInfo存储过程了。UNSAFE程序集拥有任意权限并会危及SQL Server的健壮性和系统的安全。EXTERNAL_ACCESS程序集拥有和SAFE程序集一样的可靠性和稳定性,但在安全方面它们类似于UNSAFE程序集。

一个更为安全的解决方案是用强名称密钥文件(strong-named key file)为程序集签名或使用证书为授权码(Authenticode)签名。强名称(或证书)是SQL Server内部创建的非对称秘钥(asymmetric key)(或证书),对应一个拥有EXTERNAL ACCESS ASSEMBLY权限(对于external access程序集) 或UNSAFE ASSEMBLY权限(对于unsafe程序集)的登录。例如,假设CLRUtilities程序集的代码需要用EXTERNAL_ ACCESS权限集运行。你可以在Visual Studio中工程|属性对话框上的签名选项卡中,使用强名称密钥文件为程序集签名。然后运行下面的代码从可执行的 .dll文件中创建非对称密钥,并创建一个拥有EXTERNAL_ACCESS ASSEMBLY权限的登录。

-- 从已签名的程序创建非对称密钥

-- 注意:必须使用强名称密钥文件为程序集签名

USE master

GO

CREATE ASYMMETRIC KEY CLRUtilitiesKey

  FROM EXECUTABLE FILE =

    'C:\CLRUtilities\CLRUtilities\bin\Debug\CLRUtilities.dll'

-- 创建登录并授予其external access权限

CREATE LOGIN CLRUtilitiesLogin FROM ASYMMETRIC KEY CLRUtilitiesKey

GRANT EXTERNAL ACCESS ASSEMBLY TO CLRUtilitiesLogin

GO

关于程序集安全的更详细的信息,请参考联机丛书:http://msdn2.microsoft.com/en-us/ library/ms345106.aspx

代码清单7-10显示了使用C# 代码编写的usp_GetEnvInfo存储过程的定义。

代码清单7-10  CLR usp_GetEnvInfo 存储过程,C#版

   // 以表格形式返回环境信息的存储过程

    [SqlProcedure]

    public static void usp_GetEnvInfo()

    {

        // 创建一个record- 表示行的对象

        // 包括SQL表的元数据

        SqlDataRecord record = new SqlDataRecord(

            new SqlMetaData("EnvProperty", SqlDbType.NVarChar, 20),

            new SqlMetaData("Value", SqlDbType.NVarChar, 256));

        // 标记要发送回客户端的结果集的开始

        // record参数用于构造结果集的元数据

        //

        SqlContext.Pipe.SendResultsStart(record);

        // 填充一些行并通过管道发送它们

        record.SetSqlString(0, @"Machine Name");

        record.SetSqlString(1, Environment.MachineName);

        SqlContext.Pipe.SendResultsRow(record);

        record.SetSqlString(0, @"Processors");

        record.SetSqlString(1, Environment.ProcessorCount.ToString());

        SqlContext.Pipe.SendResultsRow(record);

        record.SetSqlString(0, @"OS Version");

        record.SetSqlString(1, Environment.OSVersion.ToString());

        SqlContext.Pipe.SendResultsRow(record);

        record.SetSqlString(0, @"CLR Version");

        record.SetSqlString(1, Environment.Version.ToString());

        SqlContext.Pipe.SendResultsRow(record);

        //标记结果集的结尾

        SqlContext.Pipe.SendResultsEnd();

    }

在这个存储过程,你可以看到在SQL Server CLR程序中使用了一些特殊的ADO.NET扩展。它们定义在 .NET2.0的Microsoft.SqlServer.Server命名空间下。

当你从SQL Server调用存储过程时,已经建立了连接。不必再打开一个新的连接;你需要从服务器上运行的代码中访问调用者的上下文。调用者的上下文是由SqlContext对象提供的。在使用SqlContext对象之前,应该先使用IsAvailable属性测试它是否可用。

该存储过程从操作系统中获取一些运行环境的数据。这些数据可以通过Environment对象的属性获取,该对象位于System命名空间。但是你获取的数据是文本格式的。在这个CLR存储过程中,你会看到如何生成任意格式的行集。程序的代码把数据保存在SqlDataRecord对象中,它表示的一个数据行,并通过SqlMetaData对象为该行定义架构。

T-SQL存储过程使用SELECT语句把结果发送到调用者的“管道(pipe)”。这是把结果发送给调用者的最有效的方式。在SQL Server中运行的CLR程序也公开了同样的方法。SqlPipe对象的send方法可以把结果发送给连接的管道。使用SqlContext对象的Pipe属性可以得到实例化的SqlPipe对象。

代码清单7-11显示了使用Visual Basic代码编写的usp_GetEnvInfo存储过程的定义。

代码清单7-11  CLR usp_GetEnvInfo存储过程,Visual Basic版

   '以表格形式返回环境信息的存储过程

    <SqlProcedure()> _

    Public Shared Sub usp_GetEnvInfo()

        '创建一个record- 表示行的对象

        '包括SQL表的元数据

        Dim record As New SqlDataRecord(_

            New SqlMetaData("EnvProperty", SqlDbType.NVarChar, 20), _

            New SqlMetaData("Value", SqlDbType.NVarChar, 256))

        '标记要发送回客户端的结果集的开始

        ' record参数用于构造结果集的元数据

        '

        SqlContext.Pipe.SendResultsStart(record)

        ''填充一些行并通过管道发送它们

        record.SetSqlString(0, "Machine Name")

        record.SetSqlString(1, Environment.MachineName)

        SqlContext.Pipe.SendResultsRow(record)

        record.SetSqlString(0, "Processors")

        record.SetSqlString(1, Environment.ProcessorCount.ToString())

        SqlContext.Pipe.SendResultsRow(record)

        record.SetSqlString(0, "OS Version")

        record.SetSqlString(1, Environment.OSVersion.ToString())

        SqlContext.Pipe.SendResultsRow(record)

        record.SetSqlString(0, "CLR Version")

        record.SetSqlString(1, Environment.Version.ToString())

        SqlContext.Pipe.SendResultsRow(record)

        '标记结果集的结尾

        SqlContext.Pipe.SendResultsEnd()

    End Sub

运行下面的代码在CLRUtilities数据库中注册C# 版的usp_GetEnvInfo存储过程。

USE CLRUtilities;

GO

IF OBJECT_ID('dbo.usp_GetEnvInfo') IS NOT NULL

  DROP PROC usp_GetEnvInfo;

GO

CREATE PROCEDURE dbo.usp_GetEnvInfo

AS EXTERNAL NAME CLRUtilities.CLRUtilities.usp_GetEnvInfo;

如果你是用Visual Basic开发的,用下面的代码注册该存储过程。

CREATE PROCEDURE dbo.usp_GetEnvInfo

AS EXTERNAL NAME

  CLRUtilities.[CLRUtilities.CLRUtilities].usp_GetEnvInfo;

运行下面的代码测试usp_GetEnvInfo存储过程,产生的输出如表7-15所示。

EXEC dbo.usp_GetEnvInfo;

表7-15  usp_GetEnvInfo存储过程的输出

EnvProperty

Value

Machine Name

DOJO

Processors

1

OS Version

Microsoft Windows

NT 5.1.2600 Service Pack 2

CLR Version

2.0.50727.42

CLR存储过程的第二个示例是usp_GetAssemblyInfo存储过程,它返回与输入程序集相关的信息。

代码清单7-12显示了C# 版的usp_GetAssemblyInfo存储过程的定义。

代码清单7-12  CLR usp_GetAssemblyInfo存储过程,C#版

   // 返回程序集信息的存储过程

    // 使用反射

    [SqlProcedure]

    public static void usp_GetAssemblyInfo(SqlString asmName)

    {

        // 获取程序集的clr 名称

        String clrName = null;

        // 获取上下文

        using (SqlConnection connection =

                 new SqlConnection("Context connection = true"))

        {

            connection.Open();

            using (SqlCommand command = new SqlCommand())

            {

                //得到程序集并加载它

                command.Connection = connection;

                command.CommandText =

                  "SELECT clr_name FROM sys.assemblies WHERE name = @asmName";

                command.Parameters.Add("@asmName", SqlDbType.NVarChar);

                command.Parameters[0].Value = asmName;

                clrName = (String)command.ExecuteScalar();

                if (clrName == null)

                {

                    throw new ArgumentException("Invalid assembly name!");

                }

                Assembly myAsm = Assembly.Load(clrName);

                // 创建一个 record – 表示行的对象

                // 包含SQL表的元数据

                SqlDataRecord record = new SqlDataRecord(

                    new SqlMetaData("Type", SqlDbType.NVarChar, 50),

                    new SqlMetaData("Name", SqlDbType.NVarChar, 256));                 //标记要发送回客户端的结果集的开始

               //record参数用于构造结果集的元数据

                SqlContext.Pipe.SendResultsStart(record);

                // 得到程序集中的所有类型

                Type[] typesArr = myAsm.GetTypes();

                foreach (Type t in typesArr)

                {

                    // SQL数据库中的类型应该是一个类或一个结构

                    if (t.IsClass == true)

                    {

                        record.SetSqlString(0, @"Class");

                    }

                    else

                    {

                        record.SetSqlString(0, @"Structure");

                    }

                    record.SetSqlString(1, t.FullName);

                    SqlContext.Pipe.SendResultsRow(record);

                    // 找出所有公共的静态方法

                    MethodInfo[] miArr = t.GetMethods();

                    foreach (MethodInfo mi in miArr)

                    {

                        if (mi.IsPublic && mi.IsStatic)

                        {

                            record.SetSqlString(0, @" Method");

                            record.SetSqlString(1, mi.Name);

                            SqlContext.Pipe.SendResultsRow(record);

                        }

                    }

                }

                // 标记结果集的结尾

                SqlContext.Pipe.SendResultsEnd();

            }

        }

    }

DBA可能会遇到一个问题,即如何精确的找出一个 .NET程序集中的哪些部分被加载到了数据库。幸运的是,这个问题很容易解决。所有的 .NET程序集都包含元数据,用于描述在程序集中定义的所有类型(类和结构),包括所有public方法和属性。.NET中的System.Reflection命名空间中的一些类和接口提供已加载类型的管理视图(managed view)。

要了解存储在文件系统中的 .NET程序集的详细描述,你可以使用Reflector for .NET这个非常出色的工具,它由Lutz Roeder开发。这个工具可以从他的网站免费下载:http://www.aisto.com/roeder/dotnet/,它在 .NET开发人员中非常受欢迎。Miles Trochesset在它的博客(http://blogs.msdn.com/sqlclr/archive/2005/11/21/495438.aspx)中提供了一个SQL Server CLR DDL触发器,这个触发器在执行CREATE ASSEMBLY语句时触发。它会自动地注

册程序集中的所有CLR对象,包括UDT、UDA、UDF、SP和触发器。我想它在数据库开发人员中一定会非常受欢迎。我以这两个工具为切入点创建简化版的SQL Server CLR存储过程。我认为DBA可能更愿意从存储过程中读取程序集元数据,而不是利用外部工具,就像Lutz Roeder的Reflector for .NET,而且DBA可能只是先读取元数据,不会像Miles Trochesset的触发器一样,马上注册程序集中的所有CLR对象。

usp_GetAssemblyInfo存储过程必须从sys.assemblies目录视图中加载一个程序集。为此,它必须执行SqlCommandSqlCommand需要一个连接。在usp_GetEnvInfo存储过程的代码中你已经看到了SqlContext类的用法;现在你需要一个显示的SqlConnection对象。通过使用新的连接字符串选项“Context connection = true”,你可以得到调用者连接的上下文。

usp_GetEnvInfo存储过程一样,你希望得到表格形式的结果。还是使用SqlDataRecordSqlMetaData对象构造返回的记录。SqlPipe对象提供了最好的性能把行返回给调用方。

读取程序集的元数据之前,你必须加载它。之后的工作就非常容易了。已加载程序集的GetTypes方法用于获取该程序集中定义的所有类型的集合。代码把该集合返回到一个数组。然后遍历该数组,使用GetMethods方法获取每个类型的所有的public方法,并保存到MethodInfo对象的数组中。这个存储过程只获取类型和方法名称。其实利用反射还可以获取其他的元数据信息。例如,输入参数的名称和类型。代码清单7-13显示了使用Visual Basic代码编写的usp_GetAssemblyInfo存储过程。

代码清单7-13  CLR usp_GetAssemblyInfo存储过程,Visual Basic版

    '返回程序集信息的存储过程

    '使用反射

    <SqlProcedure()> _

    Public Shared Sub usp_GetAssemblyInfo(ByVal asmName As SqlString)

        '获取程序集的clr 名称

        Dim clrName As String = Nothing

        '获取上下文

        Using connection As New SqlConnection("Context connection = true")

            connection.Open()

            Using command As New SqlCommand

                '获得程序集并加载它

                command.Connection = connection

                command.CommandText = _

                  "SELECT clr_name FROM sys.assemblies WHERE name = @asmName"

                command.Parameters.Add("@asmName", SqlDbType.NVarChar)

                command.Parameters(0).Value = asmName

                clrName = CStr(command.ExecuteScalar())

                If (clrName = Nothing) Then

                    Throw New ArgumentException("Invalid assembly name!")

                End If

                Dim myAsm As Assembly = Assembly.Load(clrName)

                '创建一个 record – 表示行的对象

                '包含SQL表的元数据                 Dim record As New SqlDataRecord( _

                    New SqlMetaData("Type", SqlDbType.NVarChar, 50), _

                    New SqlMetaData("Name", SqlDbType.NVarChar, 256))

                '标记要发送回客户端的结果集的开始

                '

                ' record参数用于构造结果集的元数据

                '

                SqlContext.Pipe.SendResultsStart(record)

                '得到程序集中的所有类型

                Dim typesArr() As Type = myAsm.GetTypes()

                For Each t As Type In typesArr

                    ' SQL数据库中的类型应该是一个类或一个结构

                    If (t.IsClass = True) Then

                        record.SetSqlString(0, "Class")

                    Else

                        record.SetSqlString(0, "Structure")

                    End If

                    record.SetSqlString(1, t.FullName)

                    SqlContext.Pipe.SendResultsRow(record)

                    '找出所有公共的静态方法

                    Dim miArr() As MethodInfo = t.GetMethods

                    For Each mi As MethodInfo In miArr

                        If (mi.IsPublic And mi.IsStatic) Then

                            record.SetSqlString(0, " Method")

                            record.SetSqlString(1, mi.Name)

                            SqlContext.Pipe.SendResultsRow(record)

                        End If

                    Next

                Next

                '标识结果集的结尾

                SqlContext.Pipe.SendResultsEnd()

            End Using

        End Using

    End Sub

运行下面的代码在CLRUtilities数据库中注册C# 版的usp_GetAssemblyInfo存储过程。

IF OBJECT_ID('dbo.usp_GetAssemblyInfo') IS NOT NULL

  DROP PROC usp_GetAssemblyInfo;

GO

CREATE PROCEDURE usp_GetAssemblyInfo

  @asmName AS sysname

AS EXTERNAL NAME CLRUtilities.CLRUtilities.usp_GetAssemblyInfo;

如果你使用Visual Basic开发该存储过程, 使用下面的代码注册:

CREATE PROCEDURE usp_GetAssemblyInfo

  @asmName AS sysname

AS EXTERNAL NAME

  CLRUtilities.[CLRUtilities.CLRUtilities].

usp_GetAssemblyInfo;

运行下面的代码测试usp_GetAssemblyInfo存储过程,用CLRUtilities程序集作为输入。

EXEC usp_GetAssemblyInfo N'CLRUtilities';

你将得到的输出如表7-16所示,其中包含了程序集名称和其中定义的所有方法(程序)的名称。除了trg_GenericDMLAudit这个CLR触发器,你应该能识别出其他的程序名称,我将在下一章介绍trg_GenericDMLAudit触发器。

表7-16  usp_GetAssemblyInfo存储过程的输出

Type

Name

Class

CLRUtilities

Method

fn_RegExMatch

Method

fn_SQLSigCLR

Method

fn_ImpCast

Method

fn_ExpCast

Method

fn_SplitCLR

Method

ArrSplitFillRow

Method

usp_GetEnvInfo

Method

usp_GetAssemblyInfo

Method

trg_GenericDMLAudit

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论