1、什么是SQLCLR
SQL CLR (SQL Common Language Runtime) 是自 SQL Server 2005 才出现的新功能,它将.NET Framework中的CLR服务注入到 SQL Server 中,使得.NET代码可在SQL Server服务器进程中执行。
通过在 Microsoft SQL Server 中托管 CLR(称为 CLR 集成),开发人员可以在托管代码中编写存储过程、触发器、用户定义函数、用户定义类型和用户定义聚合函数, 改变了以前只能通过T-SQL语言来实现这些功能的局面。因为托管代码在执行之前会编译为本机代码,所以,在有些方案中可以大大提高性能。
2、在项目中遇到一些业务逻辑比较复杂的操作,例如对数据进行清洗,Load 100M的数据,要对这些数据每个进行多种合法性规则验证、多种筛选条件的筛选,计算派生列等等操作。如果在数据库中实现比较困难,所以采用了存储过程加CLR结合的方法。在存储过程中可以调用CLR方法,执行一些存储过程难以实现的操作,利用它们各自的优势提高性能。
3、以下是一个简化的例子,旨在说明实现步骤。
3.1 新建类库项目CLRBridge(特别注意,框架要选择3.5及以下版本,不然部署时会报版本不支持的错误)
CLR桥接器,只定义了两个存储过程
a、Hello 仅作为测试用
b、InvokeDataProcessEngine 实际用到的存储过程,输入参数提供必要的一些参数,输出参数返回执行的结果以及提示信息。
仅定义这么一个执行的接口,传入实际工作的类名称,实例化,执行,这样可以简化部署,因为可能工作类会增加删除,不用每次都部署CLRBridge Assembly
3.2 新建控制台项目DataProcess
实际执行DataProcess的项目
a、在CLR.Bridge.MainEntry中会启动进程DataProcess.exe
b、StoryBoard是一个基类,提供Create、Start等虚方法,子类可对其重写,满足自己需求。
Create主要是根据输入参数实例化StoryBoard,而真正的逻辑操作在Start里面。
c、Program的main函数接受输入参数,实例化相应的StoryBoard,并调用Start,输出结果。
3.3 新建控制台项目testCLR
testCLR:因在sql查询分析器里面不方便调试,这个项目主要是对CLRBridge进行测试的
项目结构如下图
4、代码
4.1 CLRBridge\MainEntry.cs
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; namespace CLRBridge { public class Utility { //连接字符串, //#if DEBUG //使用testCLR.exe测试时 //public static string ConnectionString = @"Database=TestClr;Server=.;User ID=sa;Password=FAS@dev321;Connection Timeout=90;Asynchronous Processing = True;"; //#else //使用数据库的查询分析器、或者在数据库存储过程中测试时 public static string ConnectionString = @"Context Connection=true"; //#endif } public class DataProcessEngineConfigurationInfo { //在数据库的Info表中存储exe的路径及名称,不写死,后续如有变动方便修改 public string DataProcessEngineExeName { get; private set; } public string DataProcessEngineBaseLocation { get; private set; } public string DataProcessEngineExeFullPath { get { return Path.Combine(DataProcessEngineBaseLocation, DataProcessEngineExeName); } } public DataProcessEngineConfigurationInfo() { Get(); } private void Get() { using (SqlConnection conn = new SqlConnection(Utility.ConnectionString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand("select * from info", conn)) { cmd.CommandType = System.Data.CommandType.Text; using (SqlDataReader sqlReader = cmd.ExecuteReader()) { // ApplicationSettings Info while (sqlReader.Read()) { if (((string)sqlReader["Tag"]).Trim() == "DataProcessEngineExeName") DataProcessEngineExeName = ((string)sqlReader["Value"]).Trim(); else if (((string)sqlReader["Tag"]).Trim() == "DataProcessEngineBaseLocation") DataProcessEngineBaseLocation = ((string)sqlReader["Value"]).Trim(); } } } } } } public class MainEntry { [Microsoft.SqlServer.Server.SqlProcedure] public static int Hello() { return 10; } [Microsoft.SqlServer.Server.SqlProcedure] public static int InvokeDataProcessEngine(string storyboardName, short parameterCategory, short parameterDataType, string parameterValue, string outputParameterName, out string outputParameterValue, out string outputMessage) { int resultCode = -1; ProcessStartInfo start = new ProcessStartInfo(); //start.FileName = @"C:\zxc\CLRBridge\DataProcess\bin\Debug\DataProcess.exe"; DataProcessEngineConfigurationInfo config = new DataProcessEngineConfigurationInfo(); start.FileName = config.DataProcessEngineExeFullPath; // start.Arguments = string.Format("\"{0}\" \"{1}\" \"{2}\" \"{3}\"", storyboardName, parameterCategory, parameterDataType, parameterValue); start.UseShellExecute = false; start.RedirectStandardOutput = true; //启动外部进程,外部进程根据输入的参数实例化相应的类,执行相关费时且逻辑复杂的操作 using (Process p = new Process()) { p.StartInfo = start; p.Start(); p.WaitForExit(); using (StreamReader reader = p.StandardOutput) { string result = reader.ReadToEnd(); Match m = Regex.Match(result, "Result code:(\\d+),"); Match m2 = Regex.Match(result, string.Format("OutputParameter {0}:(\\d+),", outputParameterName)); int.TryParse(m.Groups[1].Value, out resultCode); outputParameterValue = m2.Groups[1].Value.Trim(); Match m3 = Regex.Match(result, "OutputMessage: (.+),"); outputMessage = m3.Groups[1].Value.Trim(); } } return resultCode; } } }
4.2 DataProcess
4.2.1 DataProcess\Storyboard.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DataProcess { public abstract class Storyboard { public string ParameterName { get; set; } public object ParameterId { get; set; } private string _name; public virtual string OutputParameterName { get; protected set; } public virtual string OutputParameterValue { get; protected set; } public abstract void Start(); public virtual void PreStart() { // Nothing to do. } public virtual void PostStart() { // Nothing to do. } public virtual void OnStoryboardCompleted() { // Nothing to do. } public virtual void OnStoryboardSuccessful() { // Nothing to do. } public virtual void OnStoryboardError() { // Nothing to do. } public Storyboard() { string fullTypename = this.GetType().FullName; int lastIndexOfDot = fullTypename.LastIndexOf('.'); _name = fullTypename.Substring(lastIndexOfDot + 1, fullTypename.Length - lastIndexOfDot - 1); } public virtual string Name { get { return _name; } set { _name = value; } } public string CurrentOperand { get; set; } public static Storyboard Create(string storyboardName, ParameterResolver paramResolver) { //使用配置文件配置需要实例化的类名称及命名空间,运行时依赖注入, //1、这样避免写很多if else,或者select case (如下注释掉的部分) //2、解耦,在增加storybord类型时,只需要在配置文件中添加即可 //StoryboradConfigNode sbConfig = sbNameOperandMap[storyboardName.ToLowerInvariant()]; //Type t = Type.GetType(string.Format("{0}.{1}", sbConfig.Namespace, sbConfig.Name)); Type t = Type.GetType(string.Format("{0}.{1}", "DataProcess", "LVNStoryBoard")); object parameterId = paramResolver.Resolve(); Storyboard sb = (Storyboard)Activator.CreateInstance(t, parameterId); sb.CurrentOperand = storyboardName; //sb.StoryboardConfig = sbConfig; //sb.BindStoryboardConfig(); //if (storyboardName.ToLowerInvariant() == "loadandvalidate") //{ // Guid fileDefinitionId = (Guid)paramResolver.Resolve(); // sb = new LNVStoryboard(fileDefinitionId); // IStoryboardConfig storyboardConfig = (IStoryboardConfig)EngineConfigurationManager.Instance.GetConfig(sb.Name); // sb.StoryboardConfig = storyboardConfig; // MessageLoggerManager.GetLogger().DebugVerbose("Leaving Storyboard.Create..."); //} //else if (storyboardName.ToLowerInvariant() == "viewandupdatefilter") //{ // Guid viewAndUpdateId = (Guid)paramResolver.Resolve(); // sb = new ViewAndUpdateStoryboard(viewAndUpdateId); // IStoryboardConfig storyboardConfig = (IStoryboardConfig)EngineConfigurationManager.Instance.GetConfig(sb.Name); // sb.StoryboardConfig = storyboardConfig; // MessageLoggerManager.GetLogger().DebugVerbose("Leaving Storyboard.Create..."); //} if (sb != null) { return sb; } else throw new Exception(string.Format("Storyboard {0} is not supported...", storyboardName)); } } public class ParameterResolver { public short ParameterCategory { get; set; } public short ParameterDataType { get; set; } public string ParameterValue { get; set; } public virtual object Resolve() { throw new NotImplementedException(); } protected ParameterResolver() { } public static ParameterResolver CreateParameterResolver(short parameterCategory, short parameterDataType, string parameterValue) { if (parameterCategory == 0) { SimpleParameterResolver paramResolver = new SimpleParameterResolver(); paramResolver.ParameterCategory = parameterCategory; paramResolver.ParameterDataType = parameterDataType; paramResolver.ParameterValue = parameterValue; return paramResolver; } else throw new Exception(string.Format("ParameterResolver {0} not support yet...", parameterCategory)); } } public class SimpleParameterResolver : ParameterResolver { public override object Resolve() { switch (this.ParameterDataType) { case 0: return ParameterValue; case 1: return decimal.Parse(ParameterValue); case 2: return int.Parse(ParameterValue); case 3: return short.Parse(ParameterValue); case 4: return new Guid(ParameterValue); case 5: return char.Parse(ParameterValue); default: return null; } } } }
4.2.2 DataProcess\LVNStoryBoard.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DataProcess { class LVNStoryBoard:Storyboard { //fileDefinitionId为输入参数,可能在SolveData的时候需要,但是有时候可能并不需要输入参数 public LVNStoryBoard(int fileDefinitionId) { this.ParameterId = fileDefinitionId; this.ParameterName = "FileDefinitionId"; } public override string OutputParameterName { get { return "DataJobId"; } } public override void Start() { //会有很多个串行或者并行的操作在这里执行 string result= SolveData(); this.OutputParameterValue = result; } public string SolveData() { //例如对数据库中的数据进行运算 //例如LoadDataAndValid,包括以下Action, //FetchCommonPropertiesAction、CreateDataJobAction、FetchDataJobPropertiesAction、GetFilterInfoAction、FetchImportStatusAction等等 //每个Action也有各自的业务逻辑,这些业务逻辑在存储过程中实现比较困难 //数据操作后最终的结果会存入数据库中 return "1000"; } } }
4.2.3 DataProcess\Program.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DataProcess { class Program { static void Main(string[] args) { string storyboardName = string.Empty; short parameterCategory = -1; short parameterDataType = -1; string parameterValue = string.Empty; int errorCode = -900000; string outputMessage = ""; bool IsDebug = false; //也可以写在配置文件里面 if (IsDebug) { //也可以写在配置文件里面 storyboardName = "LVNStoryBoard"; parameterCategory = 0; parameterDataType = 2; //int parameterValue = "1"; } else { storyboardName = args[0].Trim(); parameterCategory = short.Parse(args[1].Trim()); parameterDataType = short.Parse(args[2].Trim()); parameterValue = args[3].Trim(); } ParameterResolver paramResolver = ParameterResolver.CreateParameterResolver(parameterCategory, parameterDataType, parameterValue); Storyboard sb = null; try { sb = Storyboard.Create(storyboardName, paramResolver); sb.PreStart(); sb.Start(); sb.PostStart(); errorCode = 0; outputMessage = "Successful"; sb.OnStoryboardSuccessful(); } catch (Exception ex) { outputMessage = "Error: " + ex.Message; if (sb != null) sb.OnStoryboardError(); } string outputParameterName = string.Empty; string outputParameterValue = string.Empty; if (sb != null) { outputParameterName = sb.OutputParameterName; outputParameterValue = sb.OutputParameterValue; } if (errorCode != 0) { Console.WriteLine(string.Format("Result code:{0}, (DataProcessEngine failed)...", errorCode)); Console.WriteLine(string.Format("OutputParameter {0}:{1}, ...", outputParameterName, outputParameterValue)); Console.WriteLine(string.Format("OutputMessage: {0},", PostProcessOutputMessage(outputMessage))); } else { Console.WriteLine(string.Format("Result code:{0}, (DataProcessEngine successfully)...", errorCode)); Console.WriteLine(string.Format("OutputParameter {0}:{1}, ...", outputParameterName, outputParameterValue)); Console.WriteLine(string.Format("OutputMessage: {0},", PostProcessOutputMessage(outputMessage))); } if (sb != null) sb.OnStoryboardCompleted(); } private static string PostProcessOutputMessage(string outputMessage) { return outputMessage.Replace(Environment.NewLine, " "); } } }
4.3 testCLR\Program.cs
using CLRBridge; using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using System.Text; using System.Threading.Tasks; namespace testCLR { class Program { static void Main(string[] args) { //InvokeDataProcessEngine(string storyboardName, short parameterCategory, short parameterDataType, //string parameterValue, string outputParameterName, out string outputParameterValue, //out string outputMessage) string s; string msg; MainEntry.InvokeDataProcessEngine("LVNStoryBoard", 0, 2, "1", "DataJobId", out s, out msg); } } }
5、新建数据库TestCLR,并新建表
USE [TestCLR] GO /****** Object: Table [dbo].[Info] Script Date: 12/30/2015 10:13:24 AM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Info]( [Tag] [nvarchar](50) NULL, [Value] [nvarchar](50) NULL ) ON [PRIMARY] GO DELETE Info
--项目DataProcess.exe所在的实际路径及名称 INSERT [Info] ([Tag],[Value]) VALUES ( 'DataProcessEngineBaseLocation','C:\CLRBridge\DataProcess\bin\Debug\') INSERT [Info] ([Tag],[Value]) VALUES ( 'DataProcessEngineExeName','DataProcess.exe')
6、部署CLR
--show clr state sp_configure 'clr enabled' --enable clr --1,enable clr\0,disable clr exec sp_configure 'clr enabled',1 --TRUSTWORTHY 数据库属性用于指明 SQL Server 实例是否信任该数据库以及其中的内容 ALTER DATABASE TESTCLR SET TRUSTWORTHY ON; --create a Assmbies create ASSEMBLY [CLRBridge] FROM 'C:\zxc\CLRBridge\CLRBridge\bin\Debug\CLRBridge.dll' WITH PERMISSION_SET = UNSAFE GO CREATE PROCEDURE [dbo].[usp_InvokeDataProcessEngine] @storyboardName [nvarchar](100), @parameterCategory [smallint], @parameterDataType [smallint], @parameterValue [nvarchar](4000), @outputParameterName [nvarchar](100), @outputParameterValue [nvarchar](4000) OUTPUT, @outputMessage [nvarchar](4000) OUTPUT WITH EXECUTE AS CALLER AS EXTERNAL NAME [CLRBridge].[CLRBridge.MainEntry].[InvokeDataProcessEngine] GO CREATE PROCEDURE [dbo].[Hello] -- @name [nvarchar](4000) OUTPUT WITH EXECUTE AS CALLER AS EXTERNAL NAME [CLRBridge].[CLRBridge.MainEntry].[Hello] GO
部署后在sql中会有两个存储过程Hello,usp_InvokeDataProcessEngine及新建的Assembly CLRBridge
7、在sql中进行测试
DECLARE @return_value int DECLARE @outputval BIGINT, @message NVARCHAR(4000) EXEC @return_value = usp_InvokeDataProcessEngine 'LVNStoryBoard', 0, 2, '810', 'DataJobId', @outputval OUTPUT, @message OUTPUT PRINT @return_value PRINT @outputval PRINT @message DECLARE @return_value1 int EXEC @return_value1=hello PRINT @return_value1
8、在部署过程中可能出现的问题
create ASSEMBLY [CLRBridge]
FROM 'C:\CLRBridge\CLRBridge\bin\Debug\CLRBridge.dll'
WITH PERMISSION_SET = UNSAFE
报错 1
Could not obtain information about Windows NT group/user
解决
ALTER DATABASE TESTCLR SET TRUSTWORTHY ON;
原因:TRUSTWORTHY 数据库属性用于指明 SQL Server 实例是否信任该数据库以及其中的内容
http://www.cnblogs.com/chendaoyin/archive/2013/12/23/3487182.html
http://blog.csdn.net/chen_xizhang/article/details/5952608
报错2 :System.Security.SecurityException
解决:
create ASSEMBLY [CLRBridge]
FROM 'C:\zxc\CLRBridge\CLRBridge\bin\Debug\CLRBridge.dll'
WITH PERMISSION_SET = UNSAFE
报错3:在数据库中执行
EXEC @return_value = usp_InvokeDataProcessEngine 'LVNStoryBoard', 0, 2, '810', 'DataJobId', @outputval OUTPUT, @message OUTPUT
总是报 Press Acess Deny的错误system.ComponentModel.Win32Expcetion: Access is denied
后来把DataProcess.exe的路径换了地方就好了,开始放在VS2013默认project路径下。这个路径下的文件好像是受保护的,我有时候手动都不能删除。
9、源码下载
参考:
SQL Server CLR全功略之一---CLR介绍和配置