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

    二、配置 SQL Server CLR
    开启 CLR:
    CLR 使用 C# 自定义函数 - 图1
    —开启所有服务器配置
    sp_configure ‘show advanced options’, 1;
    RECONFIGURE WITH override
    GO
    —开启 CLR
    sp_configure ‘clr enabled’, 1;
    RECONFIGURE WITH override
    GO
    CLR 使用 C# 自定义函数 - 图2
    关闭 CLR:
    CLR 使用 C# 自定义函数 - 图3
    —关闭所有服务器配置
    sp_configure ‘show advanced options’, 0;
    RECONFIGURE WITH override
    GO
    —关闭 CLR
    sp_configure ‘clr enabled’, 0;
    RECONFIGURE WITH override
    GO
    CLR 使用 C# 自定义函数 - 图4
    在后面注册 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、标量函数
    CLR 使用 C# 自定义函数 - 图5
    public partial class UserDefinedFunctions
    {
    ///


    /// 10进制转16进制
    ///

    ///
    ///
    [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;
    }

    1. /// <summary><br /> /// 16进制转10进制<br /> /// </summary><br /> /// <param name="strNumber"></param><br /> /// <returns></returns><br /> [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read, IsDeterministic = true, Name = "ConvertToDecimal")]<br /> public static SqlString ConvertToDecimal(SqlString strNumber)<br /> {<br /> SqlString result = string.Empty;<br /> string str = strNumber.ToString();<br /> int number = 0;<br /> try<br /> {<br /> number = int.Parse(str, System.Globalization.NumberStyles.HexNumber);<br /> result = Convert.ToString(number, 10);<br /> }<br /> catch<br /> {<br /> }<br /> return result;<br /> }<br />}<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921150-3e7d4889-1e96-4a54-a0aa-1226241b8c53.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br /> <br />2、表值函数<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921177-eb19c095-ab97-470b-96d3-5a1b0a1ffc41.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />public partial class UserDefinedFunctions<br />{<br /> /// <summary><br /> /// SQL Server 字符串分割方法<br /> /// </summary><br /> /// <param name="separator"></param><br /> /// <param name="pendingString"></param><br /> /// <returns></returns><br /> [Microsoft.SqlServer.Server.SqlFunction(<br /> DataAccess = DataAccessKind.Read,<br /> IsDeterministic = true,<br /> Name = "SqlSplit",<br /> FillRowMethodName = "SqlSplit_FillRow",<br /> TableDefinition = "SerialNumber int,StringValue nvarchar(1024)")]<br /> public static IEnumerable SqlSplit(SqlString separator, SqlString pendingString)<br /> {<br /> string _separator = string.Empty;<br /> string _pendingString = string.Empty;<br /> if (separator.IsNull)<br /> {<br /> _separator = ",";<br /> }<br /> else<br /> {<br /> _separator = separator.ToString();<br /> if (string.IsNullOrEmpty(_separator))<br /> {<br /> _separator = ",";<br /> }<br /> }
    2. if (pendingString.IsNull)<br /> {<br /> return null;<br /> }<br /> else<br /> {<br /> _pendingString = pendingString.ToString();<br /> if (string.IsNullOrEmpty(_pendingString))<br /> {<br /> return null;<br /> }<br /> }
    3. string[] strs = _pendingString.Split(new string[] { _separator }, StringSplitOptions.RemoveEmptyEntries);<br /> if (strs.Length <= 0)<br /> {<br /> return null;<br /> }
    4. List<ResultData> resultDataList = new List<ResultData>();<br /> for (int i = 0; i < strs.Length; i++)<br /> {<br /> resultDataList.Add(new ResultData(i + 1, strs[i]));<br /> }<br /> return resultDataList;<br /> }
    5. /// <summary><br /> /// 填充数据方法<br /> /// </summary><br /> /// <param name="obj"></param><br /> /// <param name="serialNumber"></param><br /> /// <param name="stringValue"></param><br /> public static void SqlSplit_FillRow(Object obj, out SqlInt32 SerialNumber, out SqlString StringValue)<br /> {<br /> ResultData resultData = (ResultData)obj;<br /> SerialNumber = resultData.SerialNumber;<br /> StringValue = resultData.StringValue;<br /> }
    6. /// <summary><br /> /// 定义返回类型<br /> /// </summary><br /> public class ResultData<br /> {<br /> /// <summary><br /> /// 序号,即行号<br /> /// </summary><br /> public SqlInt32 SerialNumber { get; set; }
    7. /// <summary><br /> /// 分割后的每个子字符串<br /> /// </summary><br /> public SqlString StringValue { get; set; }
    8. public ResultData(SqlInt32 serialNumber, SqlString stringValue)<br /> {<br /> SerialNumber = serialNumber;<br /> StringValue = stringValue;<br /> }<br /> }<br />}<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921168-8ee8216d-45ba-41d9-a580-b4b3dba91287.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />SqlFunctionAttribute 的属性及介绍:<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921172-9d205222-d970-44dd-b476-9f8f8c5ae594.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />--属性 --说明<br />--DataAccess --指示该函数是否涉及访问存储在SQL Server的数据<br />--FillRowMethodName --在同一个类的方法的名称作为表值函数(TVF),这个参数在表值函数中才会用到,用于指定表值函数的数据填充方法<br />--IsDeterministic --指示用户定义的函数是否是确定性的<br />--IsPrecise --指示函数是否涉及不精确计算,如浮点运算<br />--Name --函数在SQL Server中注册时使用的函数的名称<br />--SystemDataAccess --指示该函数是否需要访问存储在系统目录或SQL Server虚拟系统表中的数据<br />--TableDefinition --如果方法作为表值函数(TVF),则为一个字符串,该字符串表示表结构的定义<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921173-79fc8558-4e4b-41dd-be97-ae58e1bffaa4.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />标量函数与表值函数可以写在同一个类文件里面,并且可以包含多个,但是聚合函数就不行了,现在需要添加一个新项,选择 SQL CLR C# 聚合。<br /> <br />3、聚合函数<br />我这里写的这个聚合函数的作用是把多个字符串拼为一个字符串,我之前还真有遇到这种情况需要的。<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921201-6c9dd0bf-e250-4207-a2d0-be81c9cc9eb3.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />[Serializable]<br />[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(<br /> Format.UserDefined, <br /> IsInvariantToDuplicates = false, <br /> IsInvariantToNulls = true, <br /> IsInvariantToOrder = false, <br /> MaxByteSize = 8000, <br /> Name = "SumString")]<br />public struct UserDefinedSqlAggregate : IBinarySerialize<br />{<br /> private StringBuilder stringBuilder;
    9. /// <summary><br /> /// 查询处理器使用此方法初始化聚合的计算<br /> /// </summary><br /> public void Init()<br /> {<br /> stringBuilder = new StringBuilder();<br /> }
    10. /// <summary><br /> /// 查询处理器使用此方法累计聚合值<br /> /// </summary><br /> /// <param name="Value"></param><br /> public void Accumulate(SqlString Value)<br /> {<br /> stringBuilder.Append(string.Format("{0},", Value));<br /> }
    11. /// <summary><br /> /// 查询处理器使用此方法合并聚合的多个部分计算的值<br /> /// </summary><br /> /// <param name="Group"></param><br /> public void Merge(UserDefinedSqlAggregate Group)<br /> {<br /> stringBuilder.Append(Group.stringBuilder);<br /> }
    12. /// <summary><br /> /// 此方法用于返回完成聚合计算的结果<br /> /// </summary><br /> /// <returns></returns><br /> public SqlString Terminate()<br /> {<br /> return new SqlString(stringBuilder.ToString());<br /> }
    13. #region Implement interface IBinarySerialize<br /> /// <summary><br /> /// 读<br /> /// </summary><br /> /// <param name="r"></param><br /> public void Read(System.IO.BinaryReader r)<br /> {<br /> stringBuilder = new StringBuilder(r.ReadString());<br /> }
    14. /// <summary><br /> /// 写<br /> /// </summary><br /> /// <param name="w"></param><br /> public void Write(System.IO.BinaryWriter w)<br /> {<br /> w.Write(stringBuilder.ToString());<br /> }<br /> #endregion<br />}<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921182-471653de-d1e1-49d7-b0eb-c5ca459e535c.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />SqlUserDefinedAggregateAttribute 的属性及介绍:<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921209-379b72da-1f7b-40b0-b1e9-5e6ae28fc93f.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />--属性 --说明<br />--Format --选择序列化的 Format 格式,默认选择 Native,表示使用本地序列化格式。如果选择 UserDefined,则聚合类需要实现 IBinarySerialize 接口<br />--IsInvariantToDuplicates --指示聚合是否与重复的值相计算保持不变<br />--IsInvariantToNulls --指示聚合是否与空值相计算保持不变<br />--IsInvariantToOrder --指示聚合最后计算的结果是否与顺序无关<br />--IsNullIfEmpty --指示在没有对任何值进行累计时,聚合返回值是否为 null <br />--MaxByteSize --聚合实例的最大大小(以字节为单位)<br />--Name --聚合函数的名称<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921201-9fa733f0-c7c8-45df-a2cb-080a2e17e7ea.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />然后生成项目,接下来注册程序集和注册函数就可以使用了。<br /> <br />4、注册 CLR 程序集<br />注册程序集的方式有以下两种:<br />第一种,这种方式注册程序集比较简单,但是缺点就是程序集不能移动或删除。<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921204-52a7bd9b-c2c9-4352-a2c9-c62ac6159535.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />--注册CLR程序集方式一,指定程序集DLL的路径<br />USE Test <br />GO<br />CREATE ASSEMBLY UserDefinedClrAssembly <br />--AUTHORIZATION sa --指定数据库所有者,默认为当前用户<br />FROM 'C:\Users\Administrator\Desktop\CLR Assembly\UserDefinedSqlClr.dll' --指定文件路径<br />WITH PERMISSION_SET = UNSAFE; --指定程序集的权限<br /> --SAFE:无法访问外部系统资源;<br /> --EXTERNAL_ACCESS:可以访问某些外部系统资源;<br /> --UNSAFE:可以不受限制的访问外部系统资源<br />GO<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921209-318f5e99-a0fd-4f70-8c8b-dca645661fdb.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />这里如果发生因为程序集拒绝访问的错误,那就把计算机用户 Everyone 的权限改为完全控制就可以了。<br />第二种,这种方式注册程序集稍微复杂一些,但是好处就是注册成功之后,可以移动甚至删除DLL文件,只要不是变更迁移数据库,都不用重新注册。<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921271-03d08b50-60de-4066-99a6-e4a472286c3a.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />--注册CLR程序集方式二,指定程序集DLL的16进制文件流<br />USE Test <br />GO<br />CREATE ASSEMBLY UserDefinedClrAssembly <br />--AUTHORIZATION sa --指定数据库所有者,默认为当前用户<br />FROM **0x4D5A90000300000004000000FFFF0000B8000000000000004000000000** --指定DLL的16进制文件流(当然没这么少,我删掉了)<br />WITH PERMISSION_SET = UNSAFE; --指定程序集的权限<br /> --SAFE:无法访问外部系统资源;<br /> --EXTERNAL_ACCESS:可以访问某些外部系统资源;<br /> --UNSAFE:可以不受限制的访问外部系统资源<br />GO<br />![](https://cdn.nlark.com/yuque/0/2020/gif/446847/1579164921257-722d5804-c74b-4624-bcdb-4deb7766a2ab.gif#align=left&display=inline&height=20&originHeight=20&originWidth=20&size=0&status=done&style=none&width=20)<br />获取DLL的16进制文件流,可以使用 UltraEdit 这个软件,具体操作方法这里就不多说了。<br />注册成功之后,可以使用下面的 SQL 语句查看程序集的信息,还包括查询自定义的函数、存储过程等的SQL语句,这个下面注册函数之后可以用到。<br />--查看程序集信息<br />SELECT * FROM sys.assemblies

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

    5、注册函数
    下面是三种函数的注册方式的 SQL 语句。
    CLR 使用 C# 自定义函数 - 图6
    USE Test
    GO

    —注册标量函数 ConvertToHexadecimal
    CREATE FUNCTION [dbo].ConvertToHexadecimal)
    RETURNS NVARCHAR(128)
    WITH EXECUTE AS CALLER —用于在用户在执行函数的时候对引用的对象进行权限检查
    AS
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedFunctions].[ConvertToHexadecimal] —EXTERNAL NAME 程序集名.类名.方法名
    GO

    —注册标量函数 ConvertToDecimal
    CREATE FUNCTION [dbo].ConvertToDecimal)
    RETURNS NVARCHAR(128)
    WITH EXECUTE AS CALLER —用于在用户在执行函数的时候对引用的对象进行权限检查
    AS
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedFunctions].[ConvertToDecimal] —EXTERNAL NAME 程序集名.类名.方法名
    GO

    —注册表值函数 SqlSplit
    CREATE FUNCTION [dbo].SqlSplit,@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)
    RETURNS NVARCHAR(MAX)
    EXTERNAL NAME [UserDefinedClrAssembly].[UserDefinedSqlAggregate] —EXTERNAL NAME 程序集名.类名
    GO
    CLR 使用 C# 自定义函数 - 图7
    注册函数成功之后,接下来测试一下。
    CLR 使用 C# 自定义函数 - 图8
    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
    CLR 使用 C# 自定义函数 - 图9
    结果如图。
    CLR 使用 C# 自定义函数 - 图10

    下面是删除函数和删除程序集的 SQL 语句,虽然可能用不到,但是还是贴出来吧。
    这里需要注意的是,删除程序集时要保证不存在函数、存储过程、触发器等对程序集的引用。
    CLR 使用 C# 自定义函数 - 图11
    —删除标量函数 ConvertToHexadecimal
    DROP FUNCTION dbo.ConvertToHexadecimal

    —删除标量函数 ConvertToDecimal
    DROP FUNCTION dbo.ConvertToDecimal

    —删除表值函数 SqlSplit
    DROP FUNCTION dbo.SqlSplit

    —删除聚合函数 SumString
    DROP FUNCTION dbo.SumString

    —删除程序集 UserDefinedClrAssembly
    DROP ASSEMBLY UserDefinedClrAssembly
    CLR 使用 C# 自定义函数 - 图12

    本想一篇写完的,还是算了,存储过程和触发器留待下一篇。
    其实存储过程和触发器也没什么了,只是 C# 代码不一样而已,其他注册之类的大同小异。

    这里推荐一篇博客,大家也可以去看这篇,写得还是挺完整的,有些地方都是借鉴于此。
    http://blog.csdn.net/tjvictor/article/details/4726933