• SQL Server CLR 使用 C# 自定义函数


    一、简介

    Microsoft SQL Server 2005之后,实现了对 Microsoft .NET Framework 的公共语言运行时(CLR)的集成。
    CLR 集成使得现在可以使用 .NET Framework 语言编写代码,从而能够在 SQL Server 上运行,现在就可以通过 C# 来编写 SQL Server 自定义函数、存储过程、触发器等。
    我最初的目的是因为在 SQL Server 数据库中遇到数字的十进制与十六进制的互相转换问题,也看过一些方法吧,但是最后我却选择了用 CLR 来做,毕竟在 C# 中两三行代码就能搞定的问题。。。

    二、配置 SQL Server CLR

    开启 CLR:

    复制代码
    --开启所有服务器配置
    sp_configure 'show advanced options', 1; 
    RECONFIGURE WITH override 
    GO 
    --开启 CLR
    sp_configure 'clr enabled', 1; 
    RECONFIGURE WITH override 
    GO
    复制代码

    关闭 CLR:

    复制代码
    --关闭所有服务器配置
    sp_configure 'show advanced options', 0; 
    RECONFIGURE WITH override 
    GO 
    --关闭 CLR
    sp_configure 'clr enabled', 0; 
    RECONFIGURE WITH override 
    GO
    复制代码

    在后面注册 CLR 程序集时,发生因操作权限问题而导致的失败时,可以尝试执行下面的 SQL 语句,这里我把 SQL 一并贴出来。

    --权限不够时,设置目标数据库为可信赖的,例如:Test
    ALTER DATABASE [Test] SET TRUSTWORTHY ON 
    
    --修改数据库所有者为当前登录的用户,也可以为其他用户,例如:sa
    EXEC sp_changedbowner 'sa'

    三、CLR Function

    打开 Visual Studio 新建一个 SQL Server 数据库项目,这里需要注意 .NET Framework 的版本。
    因为我的目标数据库为 SQL Server 2008,所以这里我选择的是 .NET Framework 3.5 的版本。
    然后添加新建项,选择 SQL CLR C# 用户自定义函数,先从标量函数开始。

    1、标量函数

    复制代码
    public partial class UserDefinedFunctions
    {
        /// <summary>
        /// 10进制转16进制
        /// </summary>
        /// <param name="strNumber"></param>
        /// <returns></returns>
        [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read, IsDeterministic = true, Name = "ConvertToHexadecimal")]
        public static SqlString ConvertToHexadecimal(SqlString strNumber)
        {
            SqlString result = string.Empty;
            string str = strNumber.ToString();
            int number = 0;
            if (int.TryParse(str, out number))
            {
                result = number.ToString("X");
            }
            return result;
        }
    
        /// <summary>
        /// 16进制转10进制
        /// </summary>
        /// <param name="strNumber"></param>
        /// <returns></returns>
        [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read, IsDeterministic = true, Name = "ConvertToDecimal")]
        public static SqlString ConvertToDecimal(SqlString strNumber)
        {
            SqlString result = string.Empty;
            string str = strNumber.ToString();
            int number = 0;
            try
            {
                number = int.Parse(str, System.Globalization.NumberStyles.HexNumber);
                result = Convert.ToString(number, 10);
            }
            catch
            {
            }
            return result;
        }
    }
    复制代码

    2、表值函数

    复制代码
    public partial class UserDefinedFunctions
    {
        /// <summary>
        /// SQL Server 字符串分割方法
        /// </summary>
        /// <param name="separator"></param>
        /// <param name="pendingString"></param>
        /// <returns></returns>
        [Microsoft.SqlServer.Server.SqlFunction(
            DataAccess = DataAccessKind.Read,
            IsDeterministic = true,
            Name = "SqlSplit",
            FillRowMethodName = "SqlSplit_FillRow",
            TableDefinition = "SerialNumber int,StringValue nvarchar(1024)")]
        public static IEnumerable SqlSplit(SqlString separator, SqlString pendingString)
        {
            string _separator = string.Empty;
            string _pendingString = string.Empty;
            if (separator.IsNull)
            {
                _separator = ",";
            }
            else
            {
                _separator = separator.ToString();
                if (string.IsNullOrEmpty(_separator))
                {
                    _separator = ",";
                }
            }
    
            if (pendingString.IsNull)
            {
                return null;
            }
            else
            {
                _pendingString = pendingString.ToString();
                if (string.IsNullOrEmpty(_pendingString))
                {
                    return null;
                }
            }
    
            string[] strs = _pendingString.Split(new string[] { _separator }, StringSplitOptions.RemoveEmptyEntries);
            if (strs.Length <= 0)
            {
                return null;
            }
    
            List<ResultData> resultDataList = new List<ResultData>();
            for (int i = 0; i < strs.Length; i++)
            {
                resultDataList.Add(new ResultData(i + 1, strs[i]));
            }
            return resultDataList;
        }
    
        /// <summary>
        /// 填充数据方法
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="serialNumber"></param>
        /// <param name="stringValue"></param>
        public static void SqlSplit_FillRow(Object obj, out SqlInt32 SerialNumber, out SqlString StringValue)
        {
            ResultData resultData = (ResultData)obj;
            SerialNumber = resultData.SerialNumber;
            StringValue = resultData.StringValue;
        }
    
        /// <summary>
        /// 定义返回类型
        /// </summary>
        public class ResultData
        {
            /// <summary>
            /// 序号,即行号
            /// </summary>
            public SqlInt32 SerialNumber { get; set; }
    
            /// <summary>
            /// 分割后的每个子字符串
            /// </summary>
            public SqlString StringValue { get; set; }
    
            public ResultData(SqlInt32 serialNumber, SqlString stringValue)
            {
                SerialNumber = serialNumber;
                StringValue = stringValue;
            }
        }
    }
    复制代码

    SqlFunctionAttribute 的属性及介绍:

    复制代码
    --属性                    --说明
    --DataAccess            --指示该函数是否涉及访问存储在SQL Server的数据
    --FillRowMethodName        --在同一个类的方法的名称作为表值函数(TVF),这个参数在表值函数中才会用到,用于指定表值函数的数据填充方法
    --IsDeterministic        --指示用户定义的函数是否是确定性的
    --IsPrecise                --指示函数是否涉及不精确计算,如浮点运算
    --Name                    --函数在SQL Server中注册时使用的函数的名称
    --SystemDataAccess        --指示该函数是否需要访问存储在系统目录或SQL Server虚拟系统表中的数据
    --TableDefinition        --如果方法作为表值函数(TVF),则为一个字符串,该字符串表示表结构的定义
    复制代码

    标量函数与表值函数可以写在同一个类文件里面,并且可以包含多个,但是聚合函数就不行了,现在需要添加一个新项,选择 SQL CLR C# 聚合。

    3、聚合函数

    我这里写的这个聚合函数的作用是把多个字符串拼为一个字符串,我之前还真有遇到这种情况需要的。

    复制代码
    [Serializable]
    [Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
        Format.UserDefined, 
        IsInvariantToDuplicates = false, 
        IsInvariantToNulls = true, 
        IsInvariantToOrder = false, 
        MaxByteSize = 8000, 
        Name = "SumString")]
    public struct UserDefinedSqlAggregate : IBinarySerialize
    {
        private StringBuilder stringBuilder;
    
        /// <summary>
        /// 查询处理器使用此方法初始化聚合的计算
        /// </summary>
        public void Init()
        {
            stringBuilder = new StringBuilder();
        }
    
        /// <summary>
        /// 查询处理器使用此方法累计聚合值
        /// </summary>
        /// <param name="Value"></param>
        public void Accumulate(SqlString Value)
        {
            stringBuilder.Append(string.Format("{0},", Value));
        }
    
        /// <summary>
        /// 查询处理器使用此方法合并聚合的多个部分计算的值
        /// </summary>
        /// <param name="Group"></param>
        public void Merge(UserDefinedSqlAggregate Group)
        {
            stringBuilder.Append(Group.stringBuilder);
        }
    
        /// <summary>
        /// 此方法用于返回完成聚合计算的结果
        /// </summary>
        /// <returns></returns>
        public SqlString Terminate()
        {
            return new SqlString(stringBuilder.ToString());
        }
    
        #region Implement interface IBinarySerialize
        /// <summary>
        /// 读
        /// </summary>
        /// <param name="r"></param>
        public void Read(System.IO.BinaryReader r)
        {
            stringBuilder = new StringBuilder(r.ReadString());
        }
    
        /// <summary>
        /// 写
        /// </summary>
        /// <param name="w"></param>
        public void Write(System.IO.BinaryWriter w)
        {
            w.Write(stringBuilder.ToString());
        }
        #endregion
    }
    复制代码

    SqlUserDefinedAggregateAttribute 的属性及介绍:

    复制代码
    --属性                        --说明
    --Format                    --选择序列化的 Format 格式,默认选择 Native,表示使用本地序列化格式。如果选择 UserDefined,则聚合类需要实现 IBinarySerialize 接口
    --IsInvariantToDuplicates    --指示聚合是否与重复的值相计算保持不变
    --IsInvariantToNulls        --指示聚合是否与空值相计算保持不变
    --IsInvariantToOrder        --指示聚合最后计算的结果是否与顺序无关
    --IsNullIfEmpty                --指示在没有对任何值进行累计时,聚合返回值是否为 null 
    --MaxByteSize                 --聚合实例的最大大小(以字节为单位)
    --Name                        --聚合函数的名称
    复制代码

    然后生成项目,接下来注册程序集和注册函数就可以使用了。

    4、注册 CLR 程序集

    注册程序集的方式有以下两种:

    第一种,这种方式注册程序集比较简单,但是缺点就是程序集不能移动或删除。

    复制代码
    --注册CLR程序集方式一,指定程序集DLL的路径
    USE Test 
    GO 
    CREATE ASSEMBLY UserDefinedClrAssembly 
    --AUTHORIZATION sa        --指定数据库所有者,默认为当前用户
    FROM 'C:UsersAdministratorDesktopCLR AssemblyUserDefinedSqlClr.dll'        --指定文件路径
    WITH PERMISSION_SET = UNSAFE;        --指定程序集的权限
                                    --SAFE:无法访问外部系统资源;
                                    --EXTERNAL_ACCESS:可以访问某些外部系统资源;
                                    --UNSAFE:可以不受限制的访问外部系统资源
    GO 
    复制代码

    这里如果发生因为程序集拒绝访问的错误,那就把计算机用户 Everyone 的权限改为完全控制就可以了。

    第二种,这种方式注册程序集稍微复杂一些,但是好处就是注册成功之后,可以移动甚至删除DLL文件,只要不是变更迁移数据库,都不用重新注册。

    复制代码
    --注册CLR程序集方式二,指定程序集DLL的16进制文件流
    USE Test 
    GO 
    CREATE ASSEMBLY UserDefinedClrAssembly 
    --AUTHORIZATION sa        --指定数据库所有者,默认为当前用户
    FROM 0x4D5A90000300000004000000FFFF0000B8000000000000004000000000    --指定DLL的16进制文件流(当然没这么少,我删掉了)
    WITH PERMISSION_SET = UNSAFE;        --指定程序集的权限
                                    --SAFE:无法访问外部系统资源;
                                    --EXTERNAL_ACCESS:可以访问某些外部系统资源;
                                    --UNSAFE:可以不受限制的访问外部系统资源
    GO 
    复制代码

    获取DLL的16进制文件流,可以使用 UltraEdit 这个软件,具体操作方法这里就不多说了。

    注册成功之后,可以使用下面的 SQL 语句查看程序集的信息,还包括查询自定义的函数、存储过程等的SQL语句,这个下面注册函数之后可以用到。

    --查看程序集信息
    SELECT * FROM sys.assemblies 
    
    --查看模块信息,即自定义函数、视图、存储过程、触发器等等
    SELECT * FROM sys.sql_modules
    GO 

    5、注册函数

    下面是三种函数的注册方式的 SQL 语句。

    复制代码
    USE Test 
    GO 
    
    --注册标量函数 ConvertToHexadecimal 
    CREATE FUNCTION [dbo].[ConvertToHexadecimal](@strNumber NVARCHAR(128))
    RETURNS NVARCHAR(128) 
    WITH EXECUTE AS CALLER        --用于在用户在执行函数的时候对引用的对象进行权限检查
    AS 
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedFunctions].[ConvertToHexadecimal]    --EXTERNAL NAME 程序集名.类名.方法名
    GO 
    
    --注册标量函数 ConvertToDecimal 
    CREATE FUNCTION [dbo].[ConvertToDecimal](@strNumber NVARCHAR(128))
    RETURNS NVARCHAR(128) 
    WITH EXECUTE AS CALLER        --用于在用户在执行函数的时候对引用的对象进行权限检查
    AS 
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedFunctions].[ConvertToDecimal]    --EXTERNAL NAME 程序集名.类名.方法名
    GO 
    
    --注册表值函数 SqlSplit 
    CREATE FUNCTION [dbo].[SqlSplit](@separator NVARCHAR(32),@string NVARCHAR(MAX))
    RETURNS TABLE 
    (
        SerialNumber INT,
        StringValue NVARCHAR(1024)
    )
    WITH EXECUTE AS CALLER        --用于在用户在执行函数的时候对引用的对象进行权限检查
    AS 
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedFunctions].[SqlSplit]    --EXTERNAL NAME 程序集名.类名.方法名
    GO 
    
    --注册聚合函数 SumString 
    CREATE AGGREGATE [dbo].[SumString](@params NVARCHAR(128))
    RETURNS NVARCHAR(MAX) 
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedSqlAggregate]    --EXTERNAL NAME 程序集名.类名
    GO 
    复制代码

    注册函数成功之后,接下来测试一下。

    复制代码
    DECLARE @TempTable TABLE
    (
        Id INT NOT NULL,
        Name NVARCHAR(32) NOT NULL 
    )
    INSERT INTO @TempTable (
        Id,
        [Name]
    )
    SELECT '1','小张' UNION ALL 
    SELECT '2','小明' UNION ALL 
    SELECT '2','小丽' UNION ALL 
    SELECT '2','小李' UNION ALL 
    SELECT '3','小王' UNION ALL 
    SELECT '3','小舞' 
    
    SELECT dbo.ConvertToHexadecimal('15')
    
    SELECT dbo.ConvertToDecimal('FC')
    
    SELECT * FROM SqlSplit(',',',123,456,789,')
    
    SELECT Id,dbo.SumString([Name]) Names 
    FROM @TempTable 
    GROUP BY Id 
    复制代码

    结果如图。

    下面是删除函数和删除程序集的 SQL 语句,虽然可能用不到,但是还是贴出来吧。

    这里需要注意的是,删除程序集时要保证不存在函数、存储过程、触发器等对程序集的引用。

    复制代码
    --删除标量函数 ConvertToHexadecimal 
    DROP FUNCTION dbo.ConvertToHexadecimal
    
    --删除标量函数 ConvertToDecimal 
    DROP FUNCTION dbo.ConvertToDecimal
    
    --删除表值函数 SqlSplit 
    DROP FUNCTION dbo.SqlSplit
    
    --删除聚合函数 SumString 
    DROP FUNCTION dbo.SumString
    
    --删除程序集 UserDefinedClrAssembly 
    DROP ASSEMBLY UserDefinedClrAssembly
    复制代码

    本想一篇写完的,还是算了,存储过程和触发器留待下一篇。

    其实存储过程和触发器也没什么了,只是 C# 代码不一样而已,其他注册之类的大同小异。

    这里推荐一篇博客,大家也可以去看这篇,写得还是挺完整的,有些地方都是借鉴于此。

    http://blog.csdn.net/tjvictor/article/details/4726933

  • 相关阅读:
    如何看linux是32位还是64位
    Linux下,命令 wget 的使用
    express框架目录结构
    怎么在centos中查看某个目录的树结构?
    CentOS minimal版安装图形界面的步骤(自动获取IP)
    微信小程序项目,实现图书搜索组件完善
    微信小程序项目,实现图书搜索高阶组件:
    微信小程序中使用音频组件以及wx:if和hidden的区别
    微信小程序绑定数据以及自定义指令
    微信小程序定义一个组件
  • 原文地址:https://www.cnblogs.com/asdyzh/p/9818642.html
Copyright © 2020-2023  润新知