摘要:
文檔闡述使用C#和SQLCLR為SQL Server編寫用戶定義函數,並演示用戶定義函數在T-SQL中的應用。文檔中實現的 Base64 編碼解碼函數和正則表達式函數屬於標量值函數,字符串分割函數屬於表值函數,而平方平均數函數屬於聚合函數。
環境 要求:SQL Server 2005/2008,Visual Studio 2005/2008,C# 2.0。
本頁內容
微軟在推出SQL Server 2005后,實現了對.NET CLR的集成,使得.NET代碼可在SQL Server服務器進程中執行。開發人員通過C#和SQLCLR可輕松創建存儲過程、用戶定義函數、觸發器和用戶定義類型等功能,改變了以前只能通過T- SQL語言來實現這些功能的局面。作為SQLCLR的典型應用,本文將通過C#編寫Base64編碼解碼函數、正則表達式函數、字符串分割函數以及 平方平均數函數 來演示如何為SQL Server編寫 標量值函數、表值函數和聚合函數。
在講解具體函數之前,我們先來了解一下如何啟用SQLCLR,並通過Visual Studio 2005/2008創建數據庫項目,最后部署.NET程序集到SQL Server的過程。
默認情況下,SQL Server的SQLCLR是禁用的,要使用SQLCLR需要通過sp_configure系統存儲過程設置服務器配置選項來啟用,下面顯示了默認情況下的設置。
sp_configure 'clr enabled' ;
name minimum maximum config_value run_value
----------------------------------- ----------- ----------- ------------ -----------
clr enabled 0 1 0 0
|
下面用T-SQL來啟用SQLCLR,並查看修改后的設置信息。
sp_configure 'clr enabled' , 1;
GO
RECONFIGURE ;
GO
sp_configure 'clr enabled' ;
name minimum maximum config_value run_value
----------------------------------- ----------- ----------- ------------ -----------
clr enabled 0 1 1 1
|
Visual Studio 提供了一個項目模板來建立用於數據庫開發的項目,在新建項目中選擇Visual C#,然后選擇數據庫項目,在對話框中設置項目名稱為SqlServer.SqlClr.Functions,為項目添加一個C#類文件並命名類名稱為 UserDefinedFunctions,並添加一個靜態公共的HelloSqlClr函數,補充代碼結果如下所示。
using System;
using System.Data;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_HelloSqlClr" )]
public static SqlString HelloSqlClr(SqlString input)
{
return input;
}
}
|
UserDefinedFunctions 類引用了System.Data.SqlTypes命名空間。 System.Data.SqlTypes 命名空間包含了SQL Server 中本地數據類型對應的類型,比如上面的SqlString類型對應於SQL Server的char、nchar、text、ntext、nvarchar、varchar數據類型。這些類提供一種比.NET Framework公共語言運行庫(CLR)提供的數據類型更快更安全的替代方案。使用此命名空間中的類有助於防止類型轉換錯誤時出現精度損失的情況。
UserDefinedFunctions 類還引用了Microsoft.SqlServer.Server命名空間,該命名空間包 含將 Microsoft .NET Framework公共語言運行庫(CLR)集成到Microsoft SQL Server和SQL Server 數據庫引擎進程執行環境時所要用到的類、接口和枚舉。上面的代碼我們為 HelloSqlClr 指定了SqlFunction特性,並設置Name屬性,這使得通過Visual Studio輕松實現托管用戶定義函數在SQL Server中的部署。
如果采用非自動化方式部署需要通過C#編譯器編譯源代碼,並通過T-SQL語言進行程序集安裝,最后進行函數注冊等步驟。
下面通過CSC進行C#代碼的編譯,輸出 SqlServer.SqlClr.Functions.dll 程序集。
csc /target:library /out:SqlServer.SqlClr.Functions.dll UserDefinedFunctions.cs
|
通過SQL Server Management Studio執行T-SQL加載SqlServer.SqlClr.Functions.dll程序集到SQL Server中。
CREATE ASSEMBLY [SqlServer.SqlClr.Functions]
FROM 'D:/SqlServer SqlClr Solution/SqlServer.SqlClr.Functions/SqlServer.SqlClr.Functions.dll'
WITH PERMISSION_SET = SAFE
|
當程序集安裝到SQL Server中后,可以通過以下T-SQL來查看程序集。
SELECT * FROM sys . assemblies
|
下面的T-SQL注冊程序集中包含的 HelloSqlClr 函數。
CREATE FUNCTION [dbo] . [Clr_HelloSqlClr] ( @input [nvarchar] ( 128))
RETURNS [nvarchar] ( 128) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [SqlServer.SqlClr.Functions] . [UserDefinedFunctions] . [HelloSqlClr]
|
以上工作完成后,就可以通過T-SQL來調用用戶定義函數了。
SELECT dbo . Clr_HelloSqlClr ( 'Hello sqlclr' );
---------------------------
Hello sqlclr
|
本節簡單介紹了通過C#實現用戶定義函數並部署應用的一般流程,接下去的章節來具體實現幾個常用的功能函數,其中 Base64 編碼解碼函數和正則表達式函數屬於標量值函數,字符串分割函數屬於表值函數,而平方平均數函數屬於聚合函數。
SQL Server 雖然能夠通過FOR XML語法間接實現對數據的Base64編碼,但到目前為止實際上並沒有提供可直接調用Base64編碼解碼的函數,這不免讓人感到意外,不過通過SQLCLR可輕松解決這一問題。
下面通過C#為UserDefinedFunctions類增加了Base64編碼和解碼函數。Convert類的ToBase64String和FromBase64String方法很好地發揮了作用,輕松實現了Base64編碼和解碼操作。
Base64 編碼函數。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_Base64Encode" )]
public static SqlString Base64Encode(SqlString input)
{
if (input.IsNull)
{
return new SqlString (null );
}
byte [] array = System.Text.Encoding .UTF8.GetBytes(input.Value);
string result = System.Convert .ToBase64String(array);
return new SqlString (result);
}
|
Base64 解碼函數。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_Base64Decode" )]
public static SqlString Base64Decode(SqlString input)
{
if (input.IsNull)
{
return new SqlString (null );
}
byte [] array = System.Convert .FromBase64String(input.Value);
string result = Encoding .UTF8.GetString(array);
return new SqlString (result);
}
|
對SqlServer.SqlClr.Functions項目進行編譯,並部署 SqlServer.SqlClr.Functions.dll到SQL Server,在SQL Server對象資源管理器對應數據庫的標量值函數目錄下可以發現增加了Clr_Base64Encode和Clr_Base64Decode函數,通過 右鍵修改菜單查看Clr_Base64Encode函數對應的T-SQL代碼。
ALTER FUNCTION [dbo] . [Clr_Base64Encode] ( @input [nvarchar] ( 4000))
RETURNS [nvarchar] ( 4000) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [SqlServer.SqlClr.Functions] . [UserDefinedFunctions] . [Base64Encode]
|
使用T-SQL進行應用測試。
SELECT dbo . Clr_Base64Encode ( 'StarCraft|WarCraft|Diablo' ) AS ITEM ;
SELECT dbo . Clr_Base64Decode ( 'U3RhckNyYWZ0fFdhckNyYWZ0fERpYWJsbw==' ) AS ITEM ;
ITEM
-------------------------------------------------------
U3RhckNyYWZ0fFdhckNyYWZ0fERpYWJsbw==
ITEM
-------------------------------------------------------
StarCraft|WarCraft|Diablo
|
正則表達式為字符串處理提供了強大的功能,可惜的是目前SQL Server還沒有提供對正則表達式的支持。雖然Oracle早期版本中也缺乏對SQL正則表達式支持,不過在Oracle 10g中內建了符合POSIX 標准的正則表達式,增加了REGEXP_LIKE、REGEXP_INSTR、REGEXP_SUBSTR和EGEXP_REPLACE四個新函數。本節 我們將通過C#和SQLCLR來實現類似的正則表達式函數。
在.NET中使用正則表達式需要用到 System.Text.RegularExpressions命名空間中的類型,核心類為Regex,那么我們繼續來完善 UserDefinedFunctions類,添加正則表達式匹配函數、正則表達式匹配索引函數、正則表達式匹配項函數和正則表達式替換函數這四個函數。
正則表達式匹配函數。RegexLike函數功能為對字符串進行模式匹配查詢,如果匹配成功返回true,失敗返回false。函數接收三個參數,分別對應輸入字符串,匹配模式,和正則表達式選項,在函數內部通過Regex.IsMatch方法返回匹配結果。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_RegexLike" )]
public static SqlBoolean RegexLike(SqlString input, SqlString pattern, SqlInt32 options)
{
if (input.IsNull || pattern.IsNull)
{
return new SqlBoolean (false );
}
bool result = Regex .IsMatch(input.Value, pattern.Value, (RegexOptions )options.Value);
return new SqlBoolean (result);
}
|
正則表達式匹配索引函數。RegexMatchIndex函數功能為對字符串進行模式匹配查找,如果存在匹配,返回第一個匹配項的第一個字符的索引,如果不存在返回-1。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_RegexMatchIndex" )]
public static SqlInt32 RegexMatchIndex(SqlString input, SqlString pattern, SqlInt32 options)
{
if (input.IsNull || pattern.IsNull)
{
return new SqlInt32 (-1);
}
Match match = Regex .Match(input.Value, pattern.Value, (RegexOptions )options.Value);
if (match.Success)
{
return new SqlInt32 (match.Captures[0].Index);
}
return new SqlInt32 (-1);
}
|
正則表達式匹配項函數。RegexMatchValue函數功能為對字符串進行模式匹配查找,如果存在匹配,返回第一個匹配項的內容,如果不存在,返回空字符串。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_RegexMatchValue" )]
public static SqlString RegexMatchValue(SqlString input, SqlString pattern, SqlInt32 options)
{
if (input.IsNull || pattern.IsNull)
{
return SqlString .Null;
}
Match match = Regex .Match(input.Value, pattern.Value, (RegexOptions )options.Value);
if (match.Success)
{
return new SqlString (match.Captures[0].Value);
}
return SqlString .Null;
}
|
正則表達式替換函數。RegexReplace函數功能為對字符串進行模式查找替換,相比前面的函數,增加了一個替換內容參數,內部通過Regex.Replace方法對查找到的匹配使用新的字符串進行替換。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_RegexReplace" )]
public static SqlString RegexReplace(SqlString input, SqlString pattern, SqlString replacement, SqlInt32 options)
{
if (input.IsNull || pattern.IsNull || replacement.IsNull)
{
return input;
}
string s = Regex .Replace(input.Value, pattern.Value, replacement.Value, (RegexOptions )options.Value);
return new SqlString (s);
}
|
四個正則表達式函數都提供了一個SqlInt32類型的options參數,該 參數功能對應.NET中的RegexOptions枚舉,RegexOptions帶有FlagAttribute特性,也就是說多個不同的枚舉值可以組 合在一起,因此如果要正確設置Options,需要對RegexOptions進行一些了解,下面是通過NUnit確認的RegexOptions枚舉項 對應的值,並對枚舉組合進行單元測試。
[Test ]
public void RegexOptionsTest()
{
Assert .AreEqual((int )RegexOptions .None,0);
Assert .AreEqual((int )RegexOptions .IgnoreCase,1);
Assert .AreEqual((int )RegexOptions .Multiline,2);
Assert .AreEqual((int )RegexOptions .ExplicitCapture,4);
Assert .AreEqual((int )RegexOptions .Compiled,8);
Assert .AreEqual((int )RegexOptions .Singleline,16);
Assert .AreEqual((int )RegexOptions .IgnorePatternWhitespace,32);
Assert .AreEqual((int )RegexOptions .RightToLeft,64);
Assert .AreEqual((int )RegexOptions .ECMAScript, 256);
Assert .AreEqual((int )RegexOptions .CultureInvariant,512);
Assert .AreEqual((int )(RegexOptions .IgnoreCase | RegexOptions .Multiline), 3);
}
|
再次對SqlServer.SqlClr.Functions項目進行編譯和部 署,在SQL Server對象資源管理器對應數據庫的標量值函數目錄下可以發現增加了Clr_RegexLike、Clr_RegexMatchIndex、 Clr_RegexMatchValue和Clr_RegexReplace四個函數。
為測試正則表達式函數,准備一張GameInfo表並寫入一些測試數據。
CREATE TABLE [dbo] . [GameInfo] (
[Id] [int] NOT NULL,
[Name] [nvarchar] ( 32) NOT NULL
) ON [PRIMARY]
GO
INSERT INTO [dbo] . [GameInfo] ( Id , Name ) VALUES ( 1, 'StarCraft' );
INSERT INTO [dbo] . [GameInfo] ( Id , Name ) VALUES ( 2, 'WarCraft' );
INSERT INTO [dbo] . [GameInfo] ( Id , Name ) VALUES ( 1, 'Diablo' );
|
下面的T-SQL對四個正則表達式函數進行了測試。
SELECT * FROM dbo . GameInfo WHERE dbo . Clr_RegexLike ( Name , 'Craft$' , 0) = 1;
Id Name
----------- --------------------------------
1 StarCraft
2 WarCraft
SELECT Name , dbo . Clr_RegexMatchIndex ( Name , 'Craft$' , 0) AS MIndex FROM dbo . GameInfo ;
Name MIndex
-------------------------------- -----------
StarCraft 4
WarCraft 3
Diablo -1
SELECT Name , dbo . Clr_RegexMatchValue ( Name , 'craft$' , 1) AS MValue FROM dbo . GameInfo ;
Name MValue
-------------------------------- ------------------------------
StarCraft Craft
WarCraft Craft
Diablo NULL
SELECT Name , dbo . Clr_RegexReplace ( Name , '^StarCraft$' , 'StarCraftII' , 0) AS Name2 FROM dbo . GameInfo ;
Name Name2
-------------------------------- -------------------------------
StarCraft StarCraftII
WarCraft WarCraft
Diablo Diablo
|
字符串分割是字符串處理的一項基本功能,在.NET中通過 String.Split方法可以輕松實現按特定字符組或字符串組進行的分割,並返回分割后的子字符串數組,當然Regex.Split方法提供了更為強 大的分割功能,支持由正則表達式匹配項定義的分割字符串將輸入的字符串拆分為一個子字符串數組。相對於.NET提供對字符串處理的強大支持,SQL Server就顯得相對乏力,下面就來實現SQLCLR版本的Split函數。
由於字符串分割后返回的是數組,在SQL Server中體現為多條記錄,因而不同於前面的Base64編碼解碼和正則表達式等標量值函數,Split函數屬於表值函數(TVF)。要實現表值函數 除了為SqlFuctionAttribute特性設置Name屬性外,還需要指定FillRowMethodName和TableDefinition 兩個屬性,FillRowMethodName屬性指定行填充方法的名稱,而TableDefinition屬性定義返回的記錄集的表結構,下面顯示了完 整實現代碼。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_Split" , FillRowMethodName = "SplitFillRow" , TableDefinition = "item nvarchar(256)" )]
public static IEnumerable Split(SqlString input, SqlString separators)
{
string [] array;
if (input.IsNull)
{
array = new string [] { null };
}
else if (separators.IsNull)
{
array = new string [] { input.Value };
}
else
{
string [] separatorsArray = separators.Value.Split(new char [] { ',' }, StringSplitOptions .RemoveEmptyEntries);
array = input.Value.Split(separatorsArray, StringSplitOptions .None);
}
return array;
}
private static void SplitFillRow(Object obj, ref SqlString item)
{
if (obj != null )
{
item = (string )obj;
}
}
|
不難發現表值函數需要返回IEnumerable接口,行填充函數可以指定任意名稱,上面的代碼還指定數據表只包含一個item列。
通過Visual Studio對SqlServer.SqlClr.Functions項目進行編譯和部署,在SQL Server對象資源管理器對應數據庫的表值函數目錄下發現增加了Clr_Split函數,通過修改右鍵菜單查看部署生成的T-SQL代碼。
ALTER FUNCTION [dbo] . [Clr_Split] ( @input [nvarchar] ( 4000), @separators [nvarchar] ( 4000))
RETURNS TABLE (
[item] [nvarchar] ( 256) NULL
) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [SqlServer.SqlClr.Functions] . [UserDefinedFunctions] . [Split]
|
部署工具對於item列生成nvarchar(4000)數據類型,可以在T-SQL中修改數據長度大小。下面通過兩條T-SQL語句對Clr_Split函數進行測試,輸出同一結果。
SELECT * FROM dbo . Clr_Split ( 'StarCraft|WarCraft|Diablo' , '|' );
SELECT * FROM dbo . Clr_Split ( 'StarCraft|WarCraft//Diablo' , '|,//' );
item
-----------------------------
StarCraft
WarCraft
Diablo
|
下面的C#代碼實現了Split函數的另外一個版本,返回的記錄集包含兩列,除了分割值還增加了分割序號。
[Microsoft.SqlServer.Server.SqlFunction (Name = "Clr_SplitWithOrder" , FillRowMethodName = "SplitWithOrderFillRow" , TableDefinition = "orderId int, item nvarchar(4000)" )]
public static IEnumerable SplitWithOrder(SqlString input, SqlString separators)
{
Dictionary <int , string > dictionary = new Dictionary <int , string >();
if (input.IsNull)
{
dictionary.Add(1, null );
}
else if (separators.IsNull)
{
dictionary.Add(1, input.Value);
}
else
{
string [] separatorsArray = separators.Value.Split(new char [] { ',' }, StringSplitOptions .RemoveEmptyEntries);
string [] array = input.Value.Split(separatorsArray, StringSplitOptions .None);
for (int i = 0; i < array.Length; i++)
{
dictionary.Add(i + 1, array[i]);
}
}
return dictionary;
}
private static void SplitWithOrderFillRow(Object obj, ref SqlInt32 orderid, ref SqlString item)
{
if (obj != null )
{
KeyValuePair <int , string > kvp = (KeyValuePair <int , string >)obj;
orderid = kvp.Key;
item = kvp.Value;
}
}
|
通過T-SQL對SplitWithOrder函數進行測試。
SELECT * FROM dbo . Clr_SplitWithOrder ( 'StarCraft|WarCraft//Diablo' , '|,//' ) ORDER BY orderId DESC ;
orderId item
----------- ----------------------------------------
3 Diablo
2 WarCraft
1 StarCraft
|
聚合函數的功能是對一組值進行計算並返回單個值。在SQL Server 2005之前,數據庫引擎只支持內置聚合函數,例如常見的SUM、MAX、AVG和COUNT等函數,這些聚合函數對一組輸入標量值執行操作,並且從該組 值生成單個聚合值。在SQL Server 2005版本推出后,SQL Server同.NET CLR集成,使得開發人員能夠通過.NET托管代碼創建自定義聚合函數,並且使這些函數可應用於T-SQL編程。
本節,我們通過實現平方平均數函數來講解如何使用.NET實現聚合函數。在 Visual Studio中通過解決方案管理器右鍵點擊SqlServer.SqlClr.Functions項目打開“添加”子菜單選擇“聚合”菜單項,項目中會新 增加包含聚合函數模板的Aggregate1.cs文件,接下去只需要在模板里面添加代碼實現功能邏輯即可。
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
[Serializable ]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate (Format .Native)]
public struct Aggregate1
{
public void Init()
{
}
public void Accumulate(SqlString Value)
{
}
public void Merge(Aggregate1 Group)
{
}
public SqlString Terminate()
{
return new SqlString ("" );
}
private int var1;
}
|
聚合函數模板包含一個Aggregate1結構,該結構除了聲明可序列化外還指 定了SqlUserDefinedAggregate特性來指示結構類型應如何注冊為用戶定義的聚合。SqlUserDefinedAggregate的 Format屬性通過Microsoft.SqlServer.Server.Format枚舉來指定聚合的序列化格式,如果在聚合函數中只使用值類型成 員,那么可以設置Format屬性為Format.Native,如果設置為Format.UserDefined,開發人員需要自己實現 Microsoft.SqlServer.Server.IBinarySerialize接口以支持序列化。另外 SqlUserDefinedAggregate特性類還包含 IsInvariantToNulls 和IsInvariantToDuplicates等其他屬性,關於這些屬性功能具體可以參考MSDN。
Aggregate1 包含4個方法,這是查詢處理器計算聚合所用的方法,如果編程人員對BizTalk組件開發比較熟悉,你會發現聚合函數編程類似於BizTalk中的自定義Functoid組件編程。MSDN文檔對4個方法進行了具體解釋。
Init 方法。
public void Init();
|
查詢處理器使用此方法初始化聚合的計算。對於查詢處理器正在聚合的每個組調用此方法一次。查詢處理器可以選擇重用聚合類的同一實例來計算多個組的聚合。Init 方法應在上一次使用此實例后根據需要執行清除,並允許重新啟動新的聚合計算。
Accumulate 方法。
public void Accumulate(input_type Value);
|
查詢處理器使用此方法累計聚合值。對於正在聚合的組中的每個值調用此方法一次。 查詢處理器僅在為聚合類的指定實例調用Init方法之后才調用此方法。此方法的實現應更新實例的狀態以反映正在傳遞的參數值的累計。input_type 參數是托管的 SQL Server 數據類型,該數據類型與CREATE AGGREGATE 語句中input_sqltype 所指定的本機SQL Server數據類型等效。
Merge 方法。
public void Merge(Aggregate1 Group);
|
此方法可以將此聚合的另一實例與當前實例合並。查詢處理器使用此方法合並聚合的多個部分計算。
Terminate 方法。
public return_type Terminate();
|
此方法完成聚合計算並返回聚合的結果。return_type返回值類型應是托管的SQL Server數據類型,該數據類型是CREATE AGGREGATE語句中指定的return_sqltype 的托管等效類型。
通過以上的介紹,實現平方平均數聚合函數就變得比較簡單,修改結構名稱為QuadraticMean,並設置兩個內部變量, totalValue 變量用來存儲輸入數據的平方和,而count變量用來存儲輸入數據的個數,最后通過System.Math.Sqrt方法計算返回平方平均數,下面是實現代碼。
[Serializable ]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate (Format .Native,IsInvariantToNulls = true , IsInvariantToDuplicates = false )]
public struct QuadraticMean
{
private double totalValue;
private int count;
public void Init()
{
totalValue = 0.0;
count = 0;
}
public void Accumulate(SqlDouble input)
{
if (input.IsNull)
{
return ;
}
totalValue += input.Value * input.Value;
count++;
}
public void Merge(QuadraticMean Group)
{
totalValue += Group.totalValue;
count = Group.count;
}
public SqlDouble Terminate()
{
double result = Math .Sqrt(totalValue / count);
return new SqlDouble (result);
}
}
|
通過Visaul Studio編譯和部署項目,在SQL Server對象資源管理器對應數據庫的聚合函數目錄下發現增加了QuadraticMean函數,查看生成的T-SQL代碼。
CREATE AGGREGATE [dbo] . [QuadraticMean]
( @input [float] )
RETURNS [float]
EXTERNAL NAME [SqlServer.SqlClr.Functions] . [QuadraticMean]
|
結合AVG函數對QuadraticMean函數進行測試。
SELECT avg ( Id ), dbo . QuadraticMean ( Id ) FROM dbo . GameInfo ;
----------- ----------------------
2 2.16024689946929
|
SQL Server 同.NET CLR的緊密集成使得.NET開發人員能夠快速實現SQL Server擴展功能編程,一般使用SQLCLR來實現專門執行計算的操作功能,使用T-SQL來實現基於集合的操作功能。該文檔全面講解了如何通過C# 和SQLCLR為SQL Server實現標量值、表值和聚合3類用戶定義函數,並演示用戶定義函數在T-SQL中的實際應用。 Base64 編碼解碼函數、正則表達式函數、字符串分割函數和平方平均數函數均屬於基礎函數,可被方便應用到實際業務系統中。