一、简介
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
{
///
/// 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;
}
/// <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 /> }
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 /> }
string[] strs = _pendingString.Split(new string[] { _separator }, StringSplitOptions.RemoveEmptyEntries);<br /> if (strs.Length <= 0)<br /> {<br /> return null;<br /> }
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 /> }
/// <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 /> }
/// <summary><br /> /// 定义返回类型<br /> /// </summary><br /> public class ResultData<br /> {<br /> /// <summary><br /> /// 序号,即行号<br /> /// </summary><br /> public SqlInt32 SerialNumber { get; set; }
/// <summary><br /> /// 分割后的每个子字符串<br /> /// </summary><br /> public SqlString StringValue { get; set; }
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;
/// <summary><br /> /// 查询处理器使用此方法初始化聚合的计算<br /> /// </summary><br /> public void Init()<br /> {<br /> stringBuilder = new StringBuilder();<br /> }
/// <summary><br /> /// 查询处理器使用此方法累计聚合值<br /> /// </summary><br /> /// <param name="Value"></param><br /> public void Accumulate(SqlString Value)<br /> {<br /> stringBuilder.Append(string.Format("{0},", Value));<br /> }
/// <summary><br /> /// 查询处理器使用此方法合并聚合的多个部分计算的值<br /> /// </summary><br /> /// <param name="Group"></param><br /> public void Merge(UserDefinedSqlAggregate Group)<br /> {<br /> stringBuilder.Append(Group.stringBuilder);<br /> }
/// <summary><br /> /// 此方法用于返回完成聚合计算的结果<br /> /// </summary><br /> /// <returns></returns><br /> public SqlString Terminate()<br /> {<br /> return new SqlString(stringBuilder.ToString());<br /> }
#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 /> }
/// <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 语句。
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
注册函数成功之后,接下来测试一下。
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