Goodspeed

导航

SQL Server "Yukon" Beta 1 內的 .NET 程式設計功能概觀

Microsoft Corporation

2003 年 10 月

摘要:本文將探討 Microsoft® SQL Server™ 的 .NET Common Language Runtime 功能。還將介紹用來設計資料庫導向物件的基本方法與練習,以及如何在 SQL Server 內容當中使用 .NET Framework 物件。(列印共 44 頁)

適用於:
    SQL Server "Yukon" Beta 1

內容

簡介
CLR 整合
使用組件
建立 .NET 常式
結論

簡介

隨著 Microsoft SQL Server "Yukon" Beta 1 的推出,Microsoft Windows .NET Framework Common Language Runtime (CLR) 也已整合進資料庫內。開發人員現可使用任何 .NET 語言 (包括 Microsoft Visual Basic® .NET 與 C# 在內) 來撰寫預存程序、觸發程序及使用者定義的函式。此外,還可使用 Managed 程式碼來建立使用者定義的型別與彙總這兩種新物件。

在 SQL Server 與 .NET 之間的這種整合可提供一些主要優點給資料庫開發人員:

  • 增強的程式設計模型:.NET 語言在許多方面都比 Transact-SQL 更強,可提供先前所沒有的建構與能力給 SQL 開發人員。
  • 增強的可靠性與安全性:在 Common Language Runtime 下執行的 Managed 程式碼是由資料庫引擎所主控。如此一來,可讓 .NET 資料庫物件比原先存在於舊版 SQL Server 內的延伸預存程序更加安全且更具安全性。
  • 使用者可靠性型別與彙總:使用者可靠性型別與使用者可靠性彙總是兩項新的 Managed 資料庫物件,可用來擴充 SQL Server 的儲存與查詢能力。
  • 通用開發環境:資料庫開發將會整合到 Microsoft Visual Studio® .NET 未來版本的開發環境內。開發人員可使用與在撰寫中介層和用戶端層 .NET 元件和服務時所用的相同工具來開發和偵錯資料庫物件和指令碼。
  • 效能與延展性:在許多情況下,.NET 語言編譯和執行模型可提供超越 Transact-SQL 的改進效能。

CLR 整合

使用裝載在 Microsoft SQL Server "Yukon" Beta 1 內的 CLR,您便能用 Managed 程式碼來製作預存程序、觸發程序、使用者定義的函式,以及建立其他的資料庫物件。由於 Managed 程式碼會在執行之前先編譯成機器碼,因此在一些狀況下,您可獲得顯著的效能提升。Managed 程式碼可利用「程式碼存取安全性 (Code Access Security,CAS)」來防止組件進行某些作業。SQL Server "Yukon" 可使用 CAS 來協助保障 Managed 程式碼安全和避免危及到作業系統與資料庫伺服器。

定義的 Managed 程式碼

CLR 是 Microsoft .NET Framework 的核心,可為所有的 .NET 程式碼提供執行環境。因此在 CLR 內執行的程式碼也就是所謂的「Managed 程式碼」。CLR 可提供程式執行所需之各種不同的函式與服務,包括 Just-In-Time (JIT) 編譯、配置與管理記憶體、強制型別安全、例外處理、執行緒管理與安全性。CLR 會在 .NET 常式第一次叫用時由 SQL Server 載入。

Transact-SQL 的限制

Transact-SQL 是由「國際標準組織 (International Standards Organization,ISO)」與「美國國家標準局 (ANSI)」所定義之結構化查詢語言的擴充。資料庫開發人員只需使用 Transact-SQL 便能建立、修改及刪除資料庫與資料表,並能插入、擷取、修改及刪除儲存於資料庫內的資料。Transact-SQL 是專為提供直接資料存取與操作而設計的。雖然 Transact-SQL 擅長於資料的存取與管理,但它並不是像 Visual Basic .NET 與 C# 般成熟的程式設計語言。例如:Transact-SQL 並未支援陣列、集合、各種迴圈、位元移位或類別。雖然部份這些建構函式可用 Transact-SQL 來模擬,但 Managed 程式碼則具有對這些建構函式的最佳支援。這些功能可依實際狀況提供令人無法抗拒的理由以 Managed 程式碼來實作某項資料庫功能。

CLR 整合的優點

當資料庫程式設計人員在舊版 SQL Server 中撰寫伺服器端程式碼時,只受限於使用 Transact-SQL。而現在有了 CLR 整合之後,資料庫開發人員便能進行單獨使用 Transact-SQL 所不可能或很難完成的工作。Visual Basic .NET 與 C# 是新式的程式設計語言,可對陣列、結構化例外處理及集合提供完整的支援。開發人員可運用 CLR 整合來撰寫具有較複雜邏輯的程式碼,以及撰寫更適用於採用如 Visual Basic .NET 與 C# 等語言之運算工作的程式碼。

Visual Basic .NET 與 C# 可提供如封裝 (Encapsulation)、繼承 (Inheritance) 及多型 (Polymorphism) 等物件導向功能。相關的程式碼現已能輕易地組織成類別與命名空間。如此一來,當處理大量的伺服器程式碼時,便更方便您組織與維護您的程式碼資本。這項能將程式碼邏輯地和/或實際地組織成組件與命名空間內的能力是一大優勢,可讓您在大型的資料庫實作內更容易找到與連繫不同的程式碼片段。

Managed 程式碼比 Transact-SQL 更適合用來處理數字資訊和複雜的執行邏輯,並對多種複雜的工作 (包括字串處理和規則運算式) 具備廣泛的支援。資料庫開發人員現在只須使用 .NET Framework 基底類別程式庫 (Base Class Library,BCL) 內所提供的功能,便能存取數以千計預先建立的類別與常式,並可從預存程序、觸發程序或使用者定義的函式內輕鬆存取這些類別與常式。BCL 所具有的類別可提供用來改進字串功能、進階數學運算、檔案存取、密碼編譯及其他方面的功能。然而並不提供許多適合在 SQL CLR 程式碼內使用而不適合在伺服器端使用 (例如:windowing 類別) 的類別。

Managed 程式碼的其中一項優點為型別安全 (Type Safety)。當執行 Managed 程式碼時,CLR 便會確認該程式碼是否安全。這個過程便是所謂的「驗證 (Verification)」。在驗證期間,CLR 會進行一些檢查以確保程式碼能安全執行。例如:檢查程式碼以確保不會讀取到尚未被寫入的記憶體。CLR 還可防止發生緩衝區溢位。

依預設 Visual Basic .NET 與 C# 通常都會產生安全程式碼。然而 C# 程式設計人員可選擇使用 unsafe 關鍵字的選項來產生不安全的程式碼,例如:直接存取記憶體。

Transact-SQL 與 Managed 程式碼之間的抉擇

當撰寫預存程序、觸發程序及使用者定義的函式時,現在有一項程式設計人員所需作的決定,就是要使用傳統的 Transact-SQL 還是使用如 Visual Basic .NET 或 C# 之類的 .NET 語言。答案須視所遇到的特定情況而定。在某些情況下,您會想使用 Transact-SQL;而在其他情況下,您則會想使用 Managed 程式碼。

Transact-SQL 最適合用在大部份程式碼是以少量或無程式邏輯來進行資料存取的情況。Managed 程式碼則最適合用在具有複雜邏輯且耗用大量 CPU 的函式與程序上,或用在您想運用 .NET Framework 之基底類別程式庫的情況下。

程式碼放置也相當重要。Transact-SQL 與同處理序 (In-Process) 的 Managed 程式碼均可在伺服器上執行。這項功能可將程式碼與資料緊密放在一起,並可讓您善加利用伺服器的處理能力。另一方面,您可能會想避免將耗用大量處理器的工作放置於您的資料庫伺服器上。現今大部份用戶端機器的能力都相當強大,因此您可能會藉由將較多的程式碼盡可能地放置於用戶端上,以便能善加利用用戶端的處理能力。由於 Transact-SQL 程式碼無法在用戶端機器上執行,因此將 SQL Server 同處理序提供者設計成盡可能類似於用戶端 Managed ADO.NET,以增強程式碼在伺服器與用戶端之間的可攜性。同處理序提供者在本文稍後會詳細介紹。

使用組件

當撰寫 Managed 程式碼時,其中的部署單元被稱為「組件(Assembly)」。組件會被包裝成 DLL 或可執行檔 (EXE)。可執行檔是可自行執行的,而 DLL 則須由現有的應用程式來裝載。Managed DLL 組件可由 Microsoft SQL Server "Yukon" Beta 1 載入與裝載。但 SQL Server 卻無法裝載可執行檔。

載入與卸載組件

CREATE ASSEMBLY 陳述式是用來將組件登錄於伺服器上。舉例如下:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'

其中的 FROM 子句是用來指定載入組件的路徑名稱。這個路徑可以是 UNC 路徑或是在機器本機上的實際檔案路徑。

如需解除組件登錄,只需使用具有下列語法的 DROP ASSEMBLY 陳述式即可:

DROP ASSEMBLY MyDotNETAssembly

將組件載入 Microsoft SQL Server "Yukon" Beta 1 內是公開組件所提供之功能的第一步。

瞭解權限集合與載入組件

組件的安全性是由載入者的使用者識別而不是程式碼本身的識別所規範的。信任是依每一資料庫使用者識別或角色為基礎授予權限及厲行。除非明確地授權存取,否則由某一特定使用者或角色所擁有的資料與程式碼 (組件) 會與由其他使用者或使用者角色所擁有的資料與程式碼相隔離。

資料庫使用者或使用者角色會賦有在資料庫內建立 (且因而擁有) 組件的權限。組件擁有者可依序將參考組件的權限授權給其他的資料庫使用者或角色。只有當 (a) 被呼叫/被參考的組件是由同一使用者/角色,或是 (b) 擁有被呼叫/被參考之組件的使用者/角色已將權限授權給擁有呼叫/參考組件的使用者/角色,以及 (c) 被呼叫/被參考的組件是建立在同一資料庫內時,某個組件才能成功地參考/呼叫到其他的組件。

當將組件載入 SQL Server 內時,您可從下列三種不同的安全性層級中指定其中一種來執行您的程式碼:

SAFE
EXTERNAL_ACCESS
UNSAFE

SAFE 為預設的權限集合,且適用於大部份的情況。若要指定特定的安全性層級,您須將 CREATE ASSEMBLY 陳述式的語法修改如下:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'
WITH PERMISSION_SET = SAFE

只需將上述第三行程式碼省略,還可能登錄具有 SAFE 權限集合的組件:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'

當組件內的程式碼在 SAFE 權限集合之下執行時,可透過同處理序 Managed 提供者在伺服器內只進行運算與資料存取。

EXTERNAL_ACCESS 是個程式碼權限集合,可用來處理程式碼需要在存取伺服器以外的資源 (如檔案、網路、登錄及環境變數) 時的狀況。每當伺服器存取外部資源時,它都會模擬使用者呼叫 Managed 程式碼的安全性內容。

若要建立「外部存取(EXTERNAL ACCESS)」組件,該建立者須具有「外部存取」權限。若要指定「外部存取」權限,則組件須以 EXTERNAL_ACCESS 權限來執行,您只需在載入組件時變更其所指定的權限集合即可:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS

UNSAFE 程式碼權限是用於組件不想進行安全驗證或需要另外存取到受限資源 (如 Win32 API) 的情況下。

若要指定組件以 UNSAFE 權限載入,您可在將組件載入伺服器內時指定 UNSAFE 權限集合:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'
WITH PERMISSION_SET = UNSAFE

在特定權限層級所允許的一些事項範例詳述如下:

權限集合 允許的權限
SAFE 相當有限的權限。組件須進行型別安全驗證。無法存取如檔案、網路、環境變數及登錄等外部資源。
EXTERNAL_ACCESS 賦予 SAFE 具有額外讀取與寫入檔案與目錄、存取網路、事件記錄檔與登錄等能力的權限。
UNSAFE 組件無須進行型別安全驗證。組件可呼叫到 Unmanaged 程式碼內,且無須進行型別安全驗證。

在大多數情況下,都建議使用 SAFE 權限。

瞭解執行內容

Microsoft SQL Server "Yukon" Beta 1 將以使用者為基礎的 SQL Server 安全性模型與以程式碼存取為基礎的 CLR 整合在一起。SQL Server "Yukon" Beta 1 將這兩種模型內的有用功能全都融合在一起。為了確保系統的穩定性,Managed 程式碼應無法在未經授權的情況下存取資料庫內的任何資料或物件,或是存取 SQL Server 外部的系統資源 (如檔案系統或網路)。為了達到這項目的, SQL Server 須遵守執行內容 (Execution Context) 的原則。執行內容為包含目前正在執行之查詢內所指定之資料的資料結構。如果呼叫者與被呼叫者是由同一使用者所擁有,且是採用靜態 SQL 形式來呼叫,則無須檢查任何權限。然而,如果呼叫者不在一個包含物件內,則目前的執行內容便是目前的資料庫使用者。如果呼叫者是在如程序、函式或觸發程序之類的容器物件內部,則執行內容須視下列規則而定:如果容器物件被標記為 execution_context=<owner_name>,則目前的執行內容為容器物件的擁有者。如果未標記,則執行內容為容器物件之呼叫者的執行內容。

參考其他組件

組件可參考其他組件。意即您可載入包含可被一個或多個其他組件所使用之通用功能的組件。

建立 .NET 常式

在 Microsoft SQL Server "Yukon" Beta 1 內部執行的 Managed 程式碼指「.NET 常式」。.NET 常式的是共有四種:

  • 純量值使用者定義的函式 (數值類 UDF)
  • 資料表值使用者定義的函式 (TVF)
  • 使用者定義的程序 (UDP)
  • 使用者定義的觸發程序

稍後將對上述各項常式詳加說明。各種 .NET 常式都具有對等的 Transact-SQL,並可用在 SQL Server 內任何可使用對等 Transact-SQL 的地方。例如:數值類 UDF 可在任何的數值類運算式內使用。TVF 可在任何的 FROM 子句內使用。程序可在 EXEC 運算式內叫用或從用戶端應用程式叫用。

這四種常式都具有相同的 Managed 程式碼結構:它們都須是公用靜態的 (可在 Visual Basic .NET 內共用) 類別方法。各種 .NET 常式還具有其他的需求,稍後會加以說明。

瞭解同處理序 Managed 提供者

.NET 常式有機會可輕易存取儲存於其執行所在之 SQL Server 執行個體內的資料。常式所能存取的特定資料是由程式碼執行所在的使用者內容來決定。用來存取資料的機制為同處理序 Managed 提供者,也就是所謂的「Inproc 提供者」。

Inproc 提供者已經過最佳化可與 SQL Server 處理序內部的資料搭配使用。使用同處理序 Managed 提供者的類別與方法,您便能輕鬆地將查詢送出到資料庫、執行 DML 與 DDL 陳述式,以及將結果集與訊息傳回給用戶端應用程式。

System.Data.SqlServer 命名空間會將組成 Inproc 提供者的型別群組在一起。該名空間能與 ADO.NET 的 SqlClient 命名空間 (可供開發人員用來從 Managed 用戶端與中介層應用程式上存取 SQL Server 資料) 共用許多的相似點與介面。由於具有這項相似點,您便能輕鬆地將程式碼從用戶端應用程式移轉到伺服器程式庫,反向移轉亦然。

有三項專為 Inproc 提供者所提供的 ADO.NET 功能擴充如下:

  • SqlContext:這個類別會封裝其他的擴充功能。此外,它還能提供交易與資料庫連線,這是屬於常式執行所在環境的一部份。
  • SqlPipe:這個類別可讓常式將資料表值結果與訊息傳送到用戶端。這個 SqlPipe 類別與存在於 ASP.NET 內的 Response 類別在概念上相似。
  • SqlTriggerContext:這個類別可在觸發程序執行所在的內容上提供資訊,包括插入與刪除可從 T-SQL 觸發程序存取到的資料表。詳細資訊,請參閱稍後的<觸發程序>一節。

純量值函式 (數值類 UDF)

純量值函式會傳回一個如字串、整數或位元值之類的單一值。一個會傳回單一整數值的純量值函式範例如下:

using System.Data.SqlServer;
using System.Data.Sql;
public class T {
   [SqlFunc]
   [SqlFunction(DataAccess = DataAccessKind.Read)]
   public static int ReturnOrderCount()
   {
      SqlCommand sqlComm = SqlContext.GetCommand();
 
      sqlComm.CommandText = "SELECT COUNT(*) AS 'Order Count' FROM SalesOrderHeader";
 
      return (int) sqlComm.ExecuteScalar();
   }
}
 

讓我們更詳盡地來探究上述的程式碼範例:

程式碼的第一行 using System.Data.SqlServer是用來存取 Inproc 提供者內的型別所必備的。

接著,以 [SqlFunction] 自訂屬性所設定的函式,是存在於 System.Data.Sql 命名空間內。自訂屬性是用來指定 UDF 是否要使用 Inproc 提供者,以及是否該函式只能讀取資料或是還可更新、插入或刪除資料。SQL 可藉由將 DataAccessKind 設定為 DataAccessKind.None 來指定 UDF 不使用 Inproc 提供者,而讓 UDF 的執行能夠最佳化。在下一行中的目標方法為公用靜態 (可在 Visual Basic .NET 內共用) 的。

存在於 System.Data.SqlServer 命名空間內的 SqlContext 類別,可用連線到已設定之 SQL Server 執行個體的連線來存取 SqlCommand 物件。同時還可經由 SqlContext 類別來使用目前的交易內容,然而此處並未用到。

上述程式碼的最後兩行,對於曾撰寫過使用存在於 System.Data.SqlClient 命名空間內之型別的用戶端應用程式的開發人員而言,應該不陌生:

sqlComm.CommandText = "SELECT COUNT(*) AS 'Order Count' FROM SalesOrderHeader";
return (int) sqlComm.ExecuteScalar();

此處是藉由設定 SqlCommand 物件內的 CommandText 屬性來指定適當的命令文字,並可透過呼叫 SqlContext.GetCommand 來傳回命令列文字。接著呼叫 sqlComm 物件內的 ExecuteScalar() 方法,便會根據查詢結果傳回一個型別為 int 的數值。最後會將訂單計數傳回給呼叫者。

如果將上述程式碼儲存於一個名為 FirstUdf.cs 的檔案內,則可將之編譯成組件如下:

csc.exe /t:library /r:sqlaccess.dll /o:FirstUdf.dll FirstUdf.cs

請注意:

  • 任何使用到 Inproc 提供者的組件均須參考 sqlaccess.dll。Sqlaccess.dll (及其所伴隨的 clrcppmodule.dll) 是位在 SQL Server 安裝目錄所在的 BINN 子目錄下。使用 /r: sqlaccess.dll 來參考 sqlaccess.dll 的需求和它對 clrcppmodule.dll 的依存性可能會在未來的 SQL Server "Yukon" beta 版中移除。
  • /t:library指出應該產生程式庫,而非執行檔。執行檔無法在 SQL Server 中進行登錄。

可用 Transact-SQL 來登錄 SQL 內的組件與 UDF,叫用範例如下:

CREATE ASSEMBLY FirstUdf FROM A?A‘FirstUdf.dll'
CREATE FUNCTION CountSalesOrderHeader() RETURNS INT 
AS EXTERNAL NAME FirstUdf:T::ReturnOrderCount
SELECT dbo.CountSalesOrderHeader()

請注意:

  • 以 Transact-SQL 所公開的組件名稱須符合 Managed 組件的名稱。
  • 以 Transact-SQL 所公開的函式名稱無須符合目標公用靜態方法的名稱。

資料表值函式 (TVF)

資料表值函式 (TVF) 相當簡單,就只是個會傳回一個資料表的使用者定義的函式。Transact-SQL TVF 的首次出現是在 SQL Server 2000 內的功能。

SQL Server "Yukon" Beta 1 進而延伸讓您在任何的 Managed 語言內定義 TVF 來擴充 TVF 的功能性。資料可經由 ISqlReader 物件從 TVF 傳回。

用來定義 CLR TVF 的語法與 Transact-SQL TVF 相同,均須使用額外的 EXTERNAL NAME 子句:

CREATE FUNCTION GetEmpFirstLastNames
RETURNS @EmpTab TABLE (FirstName NVARCHAR(4000), LastName NVARCHAR(4000))
EXTERNAL NAME MyDotNETAssembly:[MyNamespace.CLRCode]:: GetEmpFirstLastNames

TVF 是以關聯形式來示資料,以便未來進行查詢處理,例如:

select * from function()
select * from tbl join function() onA?A…
select * from table t cross apply function(t.column)A?A…

TVF 用來傳回資料表的方式有兩種:

  • 從數值類輸入引數建立:例如,TVF 可接收以逗號分隔的數目字串,然後將它們整理成中樞資料表。
  • 從外部資料產生:例如,TVF 可讀取事件記錄檔,然後將之外顯成資料表。

請參閱<案例>一節,查看有關 TVF 的一些範例。

使用者定義的預存程序 (UDP)

預存程序為 .NET 常式,無法用於數值類運算式上。與函式不同,它們能夠:將資料表結果與訊息傳回給用戶端、可叫用 DDL 與 DML 陳述式,以及傳回輸出參數。

將資料表結果與訊息傳回給用戶端可透過 SqlPipe 物件來完成。若要取得管道,可使用 SqlContext 類別的 GetPipe() 方法。 SqlPipe 物件具有一個 Send() 方法。藉由呼叫 Send() 方法的各個多載,您便能經由管道將資料傳送給呼叫的應用程式。

Send() 方法的各種多載列出如下:

Send(ISqlDataReader)
Send(ISqlDataRecord)
Send(ISqlError)
Send(msg As String)

一個可經由 Sql.Pipe 傳送資料表結果與訊息的範例如下:

Using System.Data.Sql;
Using System.Data.SqlServer; 
Class T {
[SqlProcedure]
public static void HelloWorld()
{
  SqlPipe sp = SqlContext.GetPipe();

  sp.Send("Hello world! It's now " + System.DateTime.Now.ToString()+"\n");
  SqlCommand cmd = SqlContext.GetCommand()
  cmd.CommandText = "SELECT ProductNumber FROM ProductMaster"
  SqlDataReader rdr = cmd.ExecuteReader();
  Sp.Send(rdr);
}
}

請注意以自訂屬性 SqlProcedure 所標記的程序,也定義於 System.Data.Sql 內。  

第一個 sp.Send 會將一個訊息傳送到用戶端,而第二個則會使用 ISqlReader 來傳送資料表結果。

一個幾乎完全對等的 Transact-SQL 預存程序如下:

CREATE PROCEDURE HelloWorld() AS
BEGIN
PRINT(A?A‘Hello world!')
SELECT ProductNumber FROM ProductMaster
END

在用戶端應用程式上對訊息與結果集的存取方式有所不同。例如:SQL "Workbench" 結果集是出現在 [結果] 檢視內,而訊息則是出現在訊息窗格內。

如果將上述程式碼儲存於名為 MyFirstUdp.cs 的檔案內,並用下列命令來編譯

csc /t:library /r:sqlaccess.dll MyFirstUdp.cs /o: MyFirstUdp.dll

結果組件可進行登錄,並可使用下列的 DDL 來叫用進入點:

CREATE ASSEMBLY MyFirstUdp FROM A?A‘MyFirstUdp.dll'
CREATE PROCEDURE HelloWorld
AS EXTERNAL NAME MyFirstUdp:T::HelloWorld

Exec MyFirstUdp

.NET 常式:範例案例與效能祕訣

本節中所包含的範例可展現些許 .NET 常式所能提供的功能。

本節中還會提供有關數值類 UDF、TVF 及 UDP 的其他資訊,並包括針對各種型別的最佳使用時機與如何提升效能的建議。

請注意:通用路徑名稱乃作為展示目的之用。因此在編譯與執行案例範例程式碼之前,您須先變更適當的路徑名稱。
請注意:在下列所有的案例中均有個假設:除非在某個特定案例中有特別說明,否則名為 YukonCLR 的組件會在安全模式下被載入到伺服器的 AdventureWorks 資料庫內。用來載入組件的程式碼如下所示 (請確定已變更為適當的路徑):
CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'
WITH PERMISSION_SET = SAFE

案例 #1 – 規則運算式

.NET Framework 內附豐富的規則運算式功能性。請考量下列的方法:

Public Shared Function IsValidZipCode(ByVal zipCode As String) As Boolean
   Return Regex.IsMatch(zipCode, "^\s*(\d{5}|(\d{5}-\d{4}))\s*$") 
End Function

IsMatch() 方法會將字串與規則運算式相比較。規則運算式可能會相當複雜。這個運算式:^\s*(\d{5}|(\d{5}-\d{4}))\s*$ 可比對到 5 個數字或 9 個數字的郵遞區號。

若要將上述函式登錄為使用者定義的函式,可執行下列 Transact-SQL DDL:

CREATE FUNCTION IsValidZipCode(@ZipCode NVARCHAR(4000))
RETURNS BIT
EXTERNAL NAME YukonCLR:[MyNamespace.CLRCode]::IsValidZipCode

最後,若要從 Transact-SQL 呼叫上述函式,須執行下列的陳述式:

SELECT DBO.IsValidZipCode('97124')

使用規則運算式來驗證輸入資料庫內的電子郵件位地址相當容易:

Public Shared Function IsValidEMailAddress(ByVal email As String) _
   As Boolean

   Return Regex.IsMatch(email, _
     "^([\w-]+\.)*?[\w-]+@[\w-]+\.([\w-]+\.)*?[\w]+$") 
End Function

若要將上述函式登錄為使用者定義的函式,可執行下列 Transact-SQL DDL:

CREATE FUNCTION IsValidEMailAddress(@email NVARCHAR(4000))
RETURNS BIT
EXTERNAL NAME YukonCLR:[MyNamespace.CLRCode]:: IsValidEMailAddress

最後,若要從 Transact-SQL 呼叫上述函式,須執行下列的陳述式:

SELECT DBO.IsValidEMailAddress('someone@microsoft.com')

這些範例展示了原先使用 Transact-SQL 很難處理的一些事情,在改用了 CLR 之後就變得相當容易。

案例 #2 – Path 類別和數值類 UDF 與 TVF 的比較

.NET Framework 內附豐富的字串處理功能性。其中一個範例是在 System.IO 命名空間內的 Path 類別。這個類別可使檔案路徑的處理更加容易,其中包括一個名為 "GetFileNameWithoutExtension" 的方法,可傳回以不含副檔名之指定字串為名稱的檔案名稱。

若想提供內含完整檔案路徑名稱的相片資料表,有兩種使用 Path 類別的方式可取得不含副檔名的檔案名稱清單:

  • 撰寫一個可指定字串的數值類 UDF,並呼叫 Path.GetFileNameWithoutExtension;可在用來查詢 Photos 資料表的 Transact-SQL 查詢內使用這個函式。
  • 撰寫一個用來查詢 Photos 資料表的 TVF,並在每一資料列呼叫 GetFileNameWithoutExtension 及傳回檔案名稱清單。

第二種方法比第一種方法的效率差。在從資料表值函式傳回之前,所有正在使用的 SqlCommand 物件均須先關閉。如此一來才能迫使在第二種方法中所使用的 TVF 能建立所有資料列的副本到 ArrayList 內,然後重複其在 ISqlReader 內傳回的 ArrayList。此外,由於大部份的程式碼均為資料存取,因此許多時間是花在將資料列移進與移出 Managed 記憶體內,而不是花在資料運算上。

通常在操作儲存於 SQL Server 內部的資料時,

  • 當獨立運作於每一來源資料列時,可從 Transact-SQL 查詢中使用數值類函式。
  • 當須考量由多重來源資料列以產生一個單一輸出資料列時,可使用 TVF。

在處理儲存於 SQL Server 外部的資料時,

  • 當只傳回一個值時,可使用數值類函式。
  • 當傳回多重值時,可使用資料表值函式。請參閱稍後的案例 #5,查看有關這種使用方式的範例。

C# 語法的 GetFileNameWithoutExtension 數值類函式如下所示:

using System;
using System.IO;

public class Wrapper 
{
public static String ExtractFileNameWithoutExtension(String str)
   {
   return System.IO.Path.GetFileNameWithoutExtension(str);
   }
};

可使用下列的命令列來編譯:

csc /t:library /r:sqlaccess.dll scenario2.cs /o: scenario2.dll

在 SQL 內登錄組件與程序的 DDL 及其叫用方式如下所示:

use tempdb
go
drop table PHOToS
drop function GetFileName
drop assembly scenario2
go
CREATE TABLE PHOTOS
(
  ID INT NOT NULL,
  PATH NVARCHAR(400) NOT NULL
)
INSERT PHOTOS VALUES(1, 'C:\IMAGES\PRODUCTS\PHOTOS\COLOR\KETCHUP.JPG')
INSERT PHOTOS VALUES(2, 'C:\IMAGES\PRODUCTS\PHOTOS\COLOR\MUSTARD.JPG')
INSERT PHOTOS VALUES(3, 'C:\IMAGES\PRODUCTS\PHOTOS\COLOR\RELISH.JPG')
INSERT PHOTOS VALUES(4, 'C:\IMAGES\PRODUCTS\PHOTOS\COLOR\PICKLES.JPG')
go
CREATE ASSEMBLY scenario2 FROM 'c:\yourpathhere\scenario2.dll'
go
CREATE FUNCTION GetFileName(@FullFileName NVARCHAR(400))
RETURNS NVARCHAR(30)
EXTERNAL NAME scenario2:Wrapper::ExtractFileNameWithoutExtension
go
select dbo.GetFileName(path) from photos
go

案例 #3 – 拍賣出價與高效能預存程序

大部份只能用來操作資料庫內資料而無法存取外部資料的預存程序,可用下列這兩種方式來撰寫:

  • CLR UDP
  • Transact-SQL 程序,或許是呼叫數值類 CLR UDF 來進行運算

至於何種效能較佳,須視資料存取與運算的比率而定。運算越多,則 CLR UDP 相對於 Transact-SQL 程序的效能較佳。在程式碼只用來進行運算的極端情況下,.NET 常式要比 Transact-SQL 程序快上好幾倍。而在程式碼只用來進行資料存取相反的極端情況下,CLR 要比 Transact-SQL 程序更慢約 20%。

其他因素在決定如何撰寫較為容易撰寫與維護程式碼的預存程序方面,以及對中介層或用戶端層的相對可攜性方面也可能相當重要。運算邏輯通常用 CLR 語言要比用 Transact-SQL 來得較容易撰寫、維護及移植。資料存取則通常用 Transact-SQL 較容易撰寫與維護。

在本案例中所實作的預存程序使用來接受拍賣的出價。但即使使用最單純的出價語意,仍需有充分的運算才能撰寫出吸引人的 CLR UDP。

由於 SQL Server "Yukon" Beta 1 針對 ADO.NET 程式設計模型提供了增強功能,因此可從 .NET 常式中獲得最大的資料存取延展性。其中提供了兩個類別:SqlDefinitionSqlExecutionContext。這兩個類別都包含了部份的 SqlCommand 物件。SqlDefinition 是命令中不會隨每次執行而改變的部份,包括保持靜態的命令文字、參數型別及方向。SqlExecutionContext 是命令中會隨每次執行而改變的部份,即參數值。當每位使用者都各有特定的 SqlExecutionContext時,這種區隔便可讓許多同時與後續使用者參考到 SqlDefinition 物件。

使用 SqlDefinitionSqlExecutionContext 要比單一使用者的執行時間約慢 1%。然而卻可在多使用者之伺服器型的情況下,產生相當高的輸送量,在某些情況下約可高達 50%。Microsoft 的 SQL Server 產品部門建議最好在 CLR 程序內的所有資料存取均使用 SqlDefinitionSqlExecutionContext

下列案例展示了有關 SqlDefinitionSqlExecutionContext 的使用方式。

using System;
using System.Data;
using System.Data.Sql;
using System.Data.SqlTypes;
using System.Data.SqlServer;

public class T {
   static readonly SqlDefinition sm_InsertNewBid;
   static readonly SqlDefinition sm_GetBidOfUserItem; 
   static readonly SqlDefinition sm_UpdateBidOfUserItem;
   static readonly SqlDefinition sm_GetCurrentWinnerOfItem;
   static readonly SqlDefinition sm_GetTop5;

   static T() {
      SqlCommand cmd = new SqlCommand();
      SqlParameter param;

      cmd.CommandText = "insert bids values(@uid,@itemid,@price,@maxprice,@incr,getdate())";
      param = cmd.Parameters.Add("@uid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@itemid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@price",SqlDbType.Float);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@maxprice",SqlDbType.Float);param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@incr",SqlDbType.Float);   param.Direction = ParameterDirection.Input;
      sm_InsertNewBid = new SqlDefinition(cmd);

      cmd.Parameters.Clear();
      cmd.CommandText = "select @price=price,@maxprice=maxprice,@incr=incr 
from bids where (uid=@uid and itemid=@itemid)";
      param = cmd.Parameters.Add("@price",SqlDbType.Float);   param.Direction = ParameterDirection.Output;
      param = cmd.Parameters.Add("@maxprice",SqlDbType.Float);param.Direction = ParameterDirection.Output;
      param = cmd.Parameters.Add("@incr",SqlDbType.Float);   param.Direction = ParameterDirection.Output;
      param = cmd.Parameters.Add("@uid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@itemid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      sm_GetBidOfUserItem = new SqlDefinition(cmd);

      cmd.Parameters.Clear();
      cmd.CommandText = "update bids set price=@price,maxprice=@maxprice,incr=@incr,bid_time=getdate() 
      from bids where (uid=@uid and itemid=@itemid)";
      param = cmd.Parameters.Add("@price",SqlDbType.Float);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@maxprice",SqlDbType.Float);param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@incr",SqlDbType.Float);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@uid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      param = cmd.Parameters.Add("@itemid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      sm_UpdateBidOfUserItem = new SqlDefinition(cmd);

      cmd.Parameters.Clear();
      cmd.CommandText = "select @uid=uid,@price=price,@maxprice=maxprice,@incr=incr 
      from bids where price=(select max(price) from bids where itemid=@iid) and 
      itemid=@iid order by maxprice";
      param = cmd.Parameters.Add("@iid",SqlDbType.Int);   param.Direction = 
ParameterDirection.Input;
      param = cmd.Parameters.Add("@uid",SqlDbType.Int);   param.Direction = ParameterDirection.Output;
      param = cmd.Parameters.Add("@price",SqlDbType.Float);   param.Direction = ParameterDirection.Output;
      param = cmd.Parameters.Add("@maxprice",SqlDbType.Float);param.Direction = ParameterDirection.Output;
      param = cmd.Parameters.Add("@incr",SqlDbType.Float);   param.Direction = ParameterDirection.Output;
      sm_GetCurrentWinnerOfItem = new SqlDefinition(cmd);

      cmd.Parameters.Clear();
      cmd.CommandText = "select top 5 uid,price,maxprice,bid_time from bids where 
      itemid=@itemid order by price";
      param = cmd.Parameters.Add("@itemid",SqlDbType.Int);   param.Direction = ParameterDirection.Input;
      sm_GetTop5 = new SqlDefinition(cmd);
      }

   public static void Bid(Int32 new_uid,Int32 iid,Double new_price,Double new_maxprice,Double new_incr)
      {
      Int32 cur_uid=0;
      Double exist_price=0,cur_price=0, cur_maxprice=0, cur_incr=0;
      SqlPipe sp = SqlContext.GetPipe();

      // 驗證出價: 
      if (    (new_maxprice <= new_price) || 
         ((new_maxprice > new_price) && (new_incr < 0)) ||
         ((new_maxprice == new_price) && (new_incr != 0)) )
         {sp.Send("Invalid Bid"); return;}

      // 開始交易
      SqlConnection cn = SqlContext.GetConnection();
      SqlTransaction tran = cn.BeginTransaction();

      // 決定目前最高出價。 
      SqlExecutionContext reqGetCurrentWinnerOfItem = cn.CreateExecutionContext(sm_GetCurrentWinnerOfItem);
      reqGetCurrentWinnerOfItem.Transaction = tran;
      reqGetCurrentWinnerOfItem.SetInt32(0,iid);
      reqGetCurrentWinnerOfItem.ExecuteNonQuery();
      if (!reqGetCurrentWinnerOfItem.GetSqlInt32(1).IsNull)
         {   
         cur_uid = reqGetCurrentWinnerOfItem.GetInt32(1);
         cur_price = reqGetCurrentWinnerOfItem.GetDouble(2);
         cur_maxprice = reqGetCurrentWinnerOfItem.GetDouble(3);
         cur_incr = reqGetCurrentWinnerOfItem.GetDouble(4);
         }

      // 決定是否此使用者已出價  
      SqlExecutionContext reqGetBidOfUserItem = cn.CreateExecutionContext(sm_GetBidOfUserItem);
      reqGetBidOfUserItem.Transaction = tran;
      reqGetBidOfUserItem.SetInt32(3,new_uid);
      reqGetBidOfUserItem.SetInt32(4,iid);
      reqGetBidOfUserItem.ExecuteNonQuery();
      if (reqGetBidOfUserItem.GetSqlDouble(0).IsNull)
         {   //此使用者的出價不存在,插入新的出價
         SqlExecutionContext reqInsertNewBid = cn.CreateExecutionContext(sm_InsertNewBid);
         reqInsertNewBid.Transaction = tran;
         reqInsertNewBid.SetInt32(0,new_uid);         
         reqInsertNewBid.SetInt32(1,iid);         
         reqInsertNewBid.SetDouble(2,new_price);         
         reqInsertNewBid.SetDouble(3,new_maxprice);         
         reqInsertNewBid.SetDouble(4,new_incr);         
         reqInsertNewBid.ExecuteNonQuery();
         }         
      else
         {   // 使用新的出價取代現有的出價
         exist_price = reqGetBidOfUserItem.GetDouble(0);
         if (exist_price > new_price)   // 新的出價較低,錯誤
            {sp.Send("Invalid Bid"); tran.Rollback(); return;}
         SqlExecutionContext reqUpdateBidOfUserItem = cn.CreateExecutionContext(sm_UpdateBidOfUserItem);
         reqUpdateBidOfUserItem.Transaction = tran;
         reqUpdateBidOfUserItem.SetDouble(0,new_price);         
         reqUpdateBidOfUserItem.SetDouble(1,new_maxprice);         
         reqUpdateBidOfUserItem.SetDouble(2,new_incr);         
         reqUpdateBidOfUserItem.SetInt32(3,new_uid);         
         reqUpdateBidOfUserItem.SetInt32(4,iid);         
         reqUpdateBidOfUserItem.ExecuteNonQuery();
         }         

      // 在新的出價與目前贏家之間決定贏家;可視需要調整
      if ((cur_uid == 0) || cur_uid == new_uid) 
         {goto send_top5;} //first bid or raise of existing winning bid

      if ( (!(cur_maxprice <= new_price))   && (!(new_maxprice <= cur_price)) )
         {
         if ((cur_price <= new_price) && (new_price <= cur_maxprice) && (cur_maxprice <= new_maxprice))
            {
            cur_price = cur_maxprice; new_price = System.Math.Min(new_maxprice,cur_maxprice + new_incr);
            }
         else if ((cur_price <= new_price) && (new_maxprice <= cur_maxprice))
            {
            cur_price = System.Math.Min(cur_maxprice,new_maxprice + cur_incr); new_price = new_maxprice;
            }
         else if ((new_price <= cur_price) && (cur_maxprice <= new_maxprice))
            {
            cur_price = cur_maxprice; new_price = System.Math.Min(new_maxprice,cur_maxprice + new_incr);
            }         
         else if ((new_price <= cur_price) && (cur_price <= new_maxprice) && (new_maxprice <= cur_maxprice))
            {
            cur_price = System.Math.Min(cur_maxprice,new_maxprice + cur_incr); new_price = new_maxprice;
            }            
         SqlExecutionContext reqUpdateBidOfUserItem = cn.CreateExecutionContext(sm_UpdateBidOfUserItem);
         reqUpdateBidOfUserItem.Transaction = tran;
         // 更新 cur_bid
         reqUpdateBidOfUserItem.SetDouble(0,new_price);         
         reqUpdateBidOfUserItem.SetDouble(1,new_maxprice);         
         reqUpdateBidOfUserItem.SetDouble(2,new_incr);         
         reqUpdateBidOfUserItem.SetInt32(3,new_uid);         
         reqUpdateBidOfUserItem.SetInt32(4,iid);         
         reqUpdateBidOfUserItem.ExecuteNonQuery();
         
         // 更新 new_bid
         reqUpdateBidOfUserItem.SetDouble(0,cur_price);         
         reqUpdateBidOfUserItem.SetDouble(1,cur_maxprice);         
         reqUpdateBidOfUserItem.SetDouble(2,cur_incr);         
         reqUpdateBidOfUserItem.SetInt32(3,cur_uid);         
         reqUpdateBidOfUserItem.SetInt32(4,iid);         
         reqUpdateBidOfUserItem.ExecuteNonQuery();
         }
send_top5:
      tran.Commit();      
      sp.Send("Bid Accepted");      

      //傳送目前出價的 top5 結果
      SqlExecutionContext reqGetTop5 = cn.CreateExecutionContext(sm_GetTop5);
      reqGetTop5.SetInt32(0,iid);          
      sp.Execute(reqGetTop5);
      }
}

使用 Inproc 提供者來編譯組件需參考 sqlaccess.dll:

csc /t:library /r:sqlaccess.dll scenario3.cs /o: scenario3.dll

在 SQL 內登錄組件與程序的 DDL 及其叫用範例如下所示:

use tempdb
go
DROP PROCEDURE Bid
DROP ASSEMBLY scenario3
DROP TABLE bids
go
CREATE TABLE bids (uid int, itemid int, price float, maxprice float, incr float,bid_time datetime)
create assembly scenario3 from 'c:\yourpathhere\scenario3.dll'
go
CREATE PROCEDURE Bid (@uid int,@iid int,@price float,@maxprice float,@incr float)
AS EXTERNAL NAME scenario3:T::Bid
go

exec Bid 2,1,50,100,1
go
exec Bid 1,1,30,110,1
go
exec Bid 2,1,200,300,1
go

案例 #4 – 寫入事件記錄檔

舊版 SQL Server 的系統管理員可藉由使用 RAISERROR 來寫入事件記錄檔及傳送安全性、錯誤碼及訊息字串的適當組合。

然而 RAISERROR 並無法在將資料寫入記錄檔方面一直提供相當的彈性,特別是當您想運用各種不同的訊息類型與記錄檔目標時。

使用 CLR 整合,您便能撰寫可輕鬆寫入本機與遠端事件記錄檔的預存程序與函式。您還可精細地選擇您所想寫入的訊息格式。

使用 System.Diagnostics 來寫入事件記錄檔需具有 EventLogPermission。在 Yukon Beta 1 內卻無法使用 EXTERNAL_ACCESS 將這項權限授權給組件。這是 Yukon Beta 1 的錯誤,在未來的 Yukon beta 版本中,會將 EventLogPermission 包含在 EXTERNAL_ACCESS 內。因此在此範例中組件須用 UNSAFE 登錄:

CREATE ASSEMBLY YukonCLRFROM 'C:\MyDBApp\YukonCLR.dll'
WITH PERMISSION_SET = UNSAFE

下列的常式是使用 Visual Basic .NET 來寫入事件記錄檔:

Imports System
Imports System.Diagnostics

Public Shared Sub WriteToEventLog(ByVal Msg As String, ByVal EntryType As String, ByVal LogName As String)
   Dim entry As EventLogEntryType
   Select Case EntryType
       Case "Information"
           entry = EventLogEntryType.Information
       Case "Warning"
           entry = EventLogEntryType.Warning
       Case "Error"
           entry = EventLogEntryType.Error
   End Select
   Dim ev As New EventLog(LogName, Environment.MachineName,  & _
      "CLR    Integration")
   ev.WriteEntry(Msg, entry)
   ev.Dispose()
End Sub

醒目提示程式碼的第一行是用來建立一個 EventLog 類別的執行個體,以便將 LogName 參數傳送到建構函式內。LogName 可以是標準的應用程式或 系統記錄檔,或是個自訂的記錄檔。傳送到建構內的下一個參數是用來指定本機的機器名稱,以便藉由 Environment 類別的 MachineName 屬性來擷取。最後一個參數是用來將事件的來源登錄為「CLR 整合」。

醒目提示程式碼的下一行是使用已指定的安全性與訊息將訊息寫入記錄檔。

ev.WriteEntry(Msg, entry)

若要將此副程式包裝成一個預存程序,可使用下列的程式碼:

CREATE PROCEDURE WriteToEventLog(@MSG NVARCHAR(4000), 
@EntryType NVARCHAR(11), @LogName NVARCHAR(4000))
AS 
EXTERNAL NAME YukonCLR:[MyNamespace.CLRCode]::WriteToEventLog

案例 #5 – 讀取事件記錄檔與如何寫入資料表值函式

將事件記錄檔公開成資料表值函式,可方便進行儲存與分析事件記錄檔。資料表值函式會傳回一個 ISqlReader。然而這是個相當龐大的介面,幸好只須實作少數方法就能在 SQL Server 內使用:

  • FieldCount 成員,是用來標示在所傳回資料表內的資料行數目。
  • GetSqlMetaData 方法,可提供在所傳回資料表內的資料行序數,並會傳回型別。
  • Read 方法,會被呼叫以決定是否有其他的資料列要傳回。如果尚有資料列要傳回,則 Read 會傳回 true;如果資料表已全部傳回,則會傳回false
  • 針對所傳回資料表內的每個唯一資料行型別,都會以所傳回資料表內的資料行序數指定為引數來呼叫 GetSql<Typename> 方法。它會將目前資料列內的指定資料行值傳回。

下列的 C# 程式碼可用來讀取事件記錄檔,並將之以資料表形式傳回:

using System;
using System.Data;
using System.Data.Sql; 
using System.Data.SqlServer;
using System.Data.SqlTypes;
using System.Diagnostics;

public class T
{
   public static ISqlReader ReadEventLog(String logname)
   {
      return (ISqlReader) new MySqlReader(logname);
   }

   public class MySqlReader : ISqlReader
   {
      private EventLog m_ev; // 我們正讀取的 EventLog
      private   EventLogEntryCollection m_evc;
      private int m_iRow = 0; // 讀取的資料列數目

      //初始化 eventlog 讀取程式
      public MySqlReader (String logname)
      {
         m_ev = new EventLog(logname,Environment.MachineName);
         m_evc = m_ev.Entries;
      }

   // SECTION:相關的 Metadata:提供個數、名稱、資料行結果的型別
      public int FieldCount {get{return 4;}}
      public SqlMetaData GetSqlMetaData(int FieldNo)
      {
         if (FieldNo == 0)
            return new SqlMetaData("Time",SqlDbType.DateTime);
         if (FieldNo == 1)
            return new SqlMetaData("Message",SqlDbType.NVarChar,400);
         if (FieldNo == 2)
            return new SqlMetaData("Category",SqlDbType.NVarChar,50);
         return new SqlMetaData("EventId",SqlDbType.Int);
      }

   
// SECTION:資料列處理。呼叫 Read 直到它傳回 false 為止。  
   // 在每次呼叫 Read 之後,Get<TypeName> 對每個欄位進行呼叫。
      public bool Read()
      {
         if (m_iRow == m_evc.Count)
            return false;
         m_iRow++;
         return true;
      }
      public SqlChars GetSqlCharsRef(int i)
      {
      if (i==0) 
         return new SqlChars(m_evc[m_iRow].Message);
      return new SqlChars(m_evc[m_iRow].Category);
      }
      public SqlDateTime GetSqlDateTime(int i){return new SqlDateTime(m_evc[m_iRow].TimeWritten);}
      public SqlInt32 GetSqlInt32(int i) {return m_evc[m_iRow].EventID;}

   // SqlType 存取子 - 無這些型別的資料行,因此這些型別不會呼叫。
      public SqlBinary GetSqlBinary(int i) {throw new NotImplementedException();}
      public SqlBoolean GetSqlBoolean(int i) {throw new NotImplementedException();}
      public SqlByte GetSqlByte(int i) {throw new NotImplementedException();}
      public SqlInt16 GetSqlInt16(int i) {throw new NotImplementedException();}
      public SqlInt64 GetSqlInt64(int i){throw new NotImplementedException();}
      public SqlSingle GetSqlSingle(int i){throw new NotImplementedException();}
      public SqlDouble GetSqlDouble(int i){throw new 
NotImplementedException();}
      public SqlMoney GetSqlMoney(int i){throw new 
NotImplementedException();}
      public SqlDecimal GetSqlDecimal(int i){throw new NotImplementedException();}
      public SqlString GetSqlString(int i){throw new NotImplementedException();}
      public SqlChars GetSqlChars(int i){throw new NotImplementedException();}
      public SqlGuid GetSqlGuid(int i){throw new NotImplementedException();}
      public SqlBytes GetSqlBytes(int i){throw new NotImplementedException();}
      public SqlBytes GetSqlBytesRef(int i){throw new NotImplementedException();}
      public object GetSqlValue(int i){throw new NotImplementedException();}
      public int GetSqlValues(object[] values){throw new NotImplementedException();}
      public System.Data.SqlTypes.SqlUtcDateTime GetSqlUtcDateTime(int i)
      {throw new NotImplementedException();}
      public System.Data.SqlTypes.SqlDate GetSqlDate(int i){throw new NotImplementedException();}
      public System.Data.SqlTypes.SqlTime GetSqlTime(int i){throw new NotImplementedException();}
      public System.Data.SqlTypes.SqlXmlReader GetSqlXmlReader(Int32 i)
      {throw new NotImplementedException();}

   // SECTION:Non-SqlType 運算子 - 從未被 SQL 使用
      public Object GetValue(int FieldNo) {throw new NotImplementedException();}
      public Int32 GetInt32(int FieldNo) {   throw new NotImplementedException();}
      public bool GetBoolean(int i) {   throw new NotImplementedException();}
      public byte GetByte(int i){   throw new NotImplementedException();}
      public long GetBytes(int i, long fieldOffset, byte[] buffer, int 
bufferoffset, int length)
      {throw new NotImplementedException();}
      public String GetString(int i)    {throw new NotImplementedException();}
      public char GetChar(int i){throw new NotImplementedException();}
      public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length)
      {throw new NotImplementedException();}
      public Guid GetGuid(int i) {throw new NotImplementedException();}
      public Int16 GetInt16(int i) {throw new NotImplementedException();}
      public Int64 GetInt64(int FieldNo) {throw new NotImplementedException();}
      public float GetFloat(int FieldNo) {throw new NotImplementedException();}
      public double GetDouble(int i) {throw new NotImplementedException();}
      public Decimal GetDecimal(int i) {throw new NotImplementedException();}
      public DateTime GetDateTime(int i) {throw new NotImplementedException();}
      public IDataReader GetData(int i) {throw new NotImplementedException();}

   // SECTION:這些是屬於 ISqlReader 介面所需要的部份,但不會被 SQL 使用。      
      public bool IsClosed {get{ return false;}}
      public void Close(){}
      public bool IsDBNull(int i){return false;}
      public bool IsSetAsDefault(int i) {return false;}
      public object GetObjectRef(int i) {return null;}
      public bool HasRows{get   {throw new NotImplementedException();}}
      public void Dispose(){throw new NotImplementedException();}
      public int Depth {get{return 0;}}
      public DataTable GetSchemaTable(){return null;}
      public object this [ int i ]    { get{return null;}}
      public object this [ String name ] { get{return null;}}
      public String GetName (int FieldNo) {return null;}
      public String GetDataTypeName (int FieldNo) {return null;}
      public Type GetFieldType(int FieldNo) {return typeof(int);}

      public int RecordsAffected {get{return 0;}}
      public bool NextResult (){return false;}
      public int GetValues(object[] values){return 0;}
      public int GetOrdinal(string name){return 0;}
      public bool IsDefault(int i) {return false;}
   } // public class MySqlReader
} // class T

編譯含有可傳回 ISqlReader 之函數的組件需參考 sqlaccess.dll

csc /t:library /r:sqlaccess.dll scenario5.cs /o: scenario5.dll

若要在 SQL 內登錄這個組件及其函式,並用它來尋找最後 10 次登入或登出系統的人:

dbcc freeproccache
go
use tempdb
go
drop function ReadEventLog
drop assembly scenario5
go
create assembly scenario5 from 'c:\yourpathhere\scenario5.dll' with PERMISSION_SET = UNSAFE
go
CREATE FUNCTION ReadEventLog(@logname nvarchar(100))
RETURNS @ TABLE(Time datetime,Message nvarchar(400),Category nvarchar(50),EventId int)
AS EXTERNAL NAME   scenario5:T::ReadEventLog
go
SELECT TOP 10 T.Time 
FROM dbo.ReadEventLog(N'Security') as T
WHERE T.Category = N'Logon/Logoff'
go

案例 #6 – 簡單檔案 IO

.Net Framework 還可用來進行檔案與目錄的存取。例如:下列的副程式可將一個訊息寫入到文字檔案內。這是設計成一般用途的常式,每當您想記錄一些來自伺服器的訊息和偵錯資訊時便能使用:

Public Shared Sub WriteToTextFile(ByVal Message As String)
Const FILE As String = "C:\Windows\Temp\LogFile.txt"
   'Open the file that the message will be written to.
   Dim myFileStream As New System.IO.FileStream(FILE, _
      IO.FileMode.Append, IO.FileAccess.Write)
   
   'Open the stream writer to do the actual writing.
   Dim myStreamWriter As New System.IO.StreamWriter(myFileStream)

   'Write message to file.
   myStreamWriter.WriteLine(Message)

   'Close stream writer.
   myStreamWriter.Close()

   'Close file.
   myFileStream.Close()
End Sub

由於這個常式需要存取到檔案系統,因此它不能以預設的 SAFE 權限來執行。因此您須使用 EXTERNAL_ACCESS 權限集合:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS

若要建立程序,可使用下列的 DLL :

CREATE PROCEDURE WriteToTextFile
@Message NVARCHAR(4000)
AS 
EXTERNAL NAME YukonCLR:[YukonCLR.CLRCode]::WriteToTextFile

若要執行 .NET 常式,可使用 EXEC 關鍵字並傳入訊息中:

EXEC dbo.WriteToTextFile 'Message from SQL Server'

案例 #7 – 財務計算

除了已經介紹過的範例之外,您還可用如 Visual Basic .NET 或 C# 之類的特定語言來包裝現有的函式,讓它們就跟使用 Transact-SQL 函式一樣容易呼叫。

包裝現有的函式的範例如下:

Public Shared Function PaymentCalcMonthly _
   (ByVal InterestRate As Double, ByVal TotalPayments As Double, _
    ByVal TotalValue As Double) As Double
    Dim monthlyRate As Double = InterestRate / 12
    Return Pmt(monthlyRate, TotalPayments, TotalValue)
End Function

上述程式碼的關鍵行是呼叫存在於 Microsoft.VisualBasic.Financial 命名空間內的 Pmt 函式。藉由使用上述程式碼來包裝 Pmt 函式,現在您便能在 SQL Server 內輕鬆地使用使用者定義的函式來存取此功能。此函式可根據每月的利率來計算每月的利率、付款的總數及本金總額。

用來建立使用者定義的函式以包裝上述 .NET 函式之使用者自訂函式的 DDL 如下所示:

CREATE FUNCTION PaymentCalcMonthly(@InterestRate float, 
@TotalPayments FLOAT, @TotalValue float)
RETURNS FLOAT
EXTERNAL NAME YukonCLR:[YukonCLR.CLRCode]:: PaymentCalcMonthly

案例 #8 – 數學公式

Managed 程式碼也是用來進行複雜或冗長之數學運算的極佳選擇,特別是在進行迴圈時。例如:用來計算分期付款的應用程式。用來計算月付款的常式如下:

Public Shared Function Amortization(ByVal Principal As Double, _
    ByVal InterestRate As Double, _
    ByVal Years As Integer) As Double

    'Principal = initial amount of the loan 
    'InterestRate = annual interest rate
    'Years = length in years of the loan

    '計算每月利率
    Dim dblInterestPerMonth As Double
    dblInterestPerMonth = InterestRate / (12 * 100)

    '計算以月為單位的貸款長度
    Dim intMonths As Integer
    intMonths = Years * 12

    Return Principal * (dblInterestPerMonth / _
        (1 - (1 + dblInterestPerMonth) ^ -intMonths))
End Function

當以 Transact-SQL 來撰寫此函式時,試想一個資料庫應用程式在資料庫內進行上千筆或甚至上百萬筆資料列的這類計算會如何。在這類情況下,使用 Managed 程式碼會較為理想,因為可透過編譯過的程式碼來提升效能。

若要載入此組件,可執行下列的 Transact-SQL:

CREATE ASSEMBLY YukonCLR
FROM 'C:\MyDBApp\YukonCLR.dll'

若要建立函式,可執行下列的 Transact-SQL:

CREATE FUNCTION Amortization(@Principal FLOAT, 
   @InterestRate FLOAT, @Years INT)
RETURNS FLOAT(53)
EXTERNAL NAME MyDotNETAssembly:[MyNamespace.CLRCode]::Amortization

若要計算金額為 $247,000 且年利率為 5.25% 之 30 年貸款的月付款,可執行下列的 Transact-SQL:

SELECT dbo.Amortization(247000, 5.25, 30)

所傳回的月付款金額為 $1,363.94。

觸發程序

現在您對如何使用 Managed 程式碼來建置預存程序與使用者定義的函式應該有些了解,您也可運用這些知識來設計觸發程序。您也可將許多在開發使用者定義的函式與程序時所用到的類別、方法及屬性運用到觸發程序上。例如:考慮下面情況,您讓使用者選擇任何他們想要的 ID,但您又想知道特別以所輸入之電子郵件位址地為 ID 的使用者。下列的觸發程序可用來偵測這項資訊,並將資訊記錄到一個稽核資料表內:

Imports System
Imports System.Data
Imports System.Data.SqlServer
Imports System.Text.RegularExpressions

Public Class EmailTrigger

Public Shared Sub EmailAudit()
   Dim triggContext As SqlTriggerContext = _
      SqlContext.GetTriggerContext()
   Dim userName as String
   If triggContext.TriggerAction.Insert  Then
        Dim sqlComm As SqlCommand = SqlContext.GetCommand()
      Dim sqlP As SqlPipe = SqlContext.GetPipe()
      sqlComm.CommandText = "SELECT Username from " & _
         "INSERTED"
      Dim dataRecord as SqlDataRecord = sqlComm.ExecuteRow()
      userName = dataRecord(0)
      If IsValidEMailAddress(userName) Then
         sqlComm.CommandText = "INSERT Emails " & _ 
         VALUES('"userName & "')"
         sqlP.Send(sqlComm.CommandText)
         sqlP.Execute(sqlComm)
      End If
   End If
End Sub

Public Shared Function IsValidEMailAddress(ByVal email As String) _ 
   As Boolean
   Return Regex.IsMatch(email, _
     "^([\w-]+\.)*?[\w-]+@[\w-]+\.([\w-]+\.)*?[\w]+$") 
End Function

End Class.

在上述第一個步驟中的 EmailAudit 函式,是用來取得 TriggerContext。這項工作可用呼叫 SqlContext 類別內的 GetTriggerContext 方法來完成。

Dim triggContext As SqlTriggerContext = _
   SqlContext.GetTriggerContext()

接著可從下列的程式碼中得知此 Managed 觸發程序的核心。

If triggContext.TriggerAction.Insert  Then
     Dim sqlComm As SqlCommand = SqlContext.GetCommand()
    Dim sqlP As SqlPipe = SqlContext.GetPipe()
   sqlComm.CommandText = "SELECT Username from INSERTED"
   Dim dataRecord as SqlDataRecord = sqlComm.ExecuteRow()
   userName = dataRecord(0)
   If IsValidEMailAddress(userName) Then
      sqlComm.CommandText = "INSERT Emails VALUES('" & _
         userName & "')"
      sqlP.Send(sqlComm.CommandText)
      sqlP.Execute(sqlComm)
   End If
End If

在上述程式碼中首先可藉由檢查 TriggerAction () 的 INSERT 作業結果,來決定是否要執行觸發程序。然後使用構成同處理序 Managed 提供者的類別與方法,便可從使用者名稱資料表內擷取到最近插入的使用者名稱。該使用者名稱會被當作參數傳到 IsValidEmailAddress 函式內。如果使用者名稱為電子郵件地址,則會被寫入資料庫內的 Emails 資料表。

假設有兩個具有下列定義的資料表存在:

CREATE TABLE Users
(
   UserName   NVARCHAR(200)    NOT NULL,
   Pass      NVARCHAR(200)    NOT  NULL
)

CREATE TABLE Emails
(
   UserName    NVARCHAR(200)    NOT NULL
)

建立觸發程序的 DDL 如下所示:

CREATE TRIGGER EmailAudit
ON Users
FOR INSERT
AS
EXTERNAL NAME YukonCLR:EmailTrigger::EmailAudit

請注意 CREATE TRIGGER 陳述式的 EXTERNAL NAME 項目會採用與 CREATE FUNCTIONCREATE PROCEDURE 陳述式所使用的類似模式。

上述的觸發程序只是一個用來展現如何撰寫功能較強之觸發程序的範例,其中運用了存在於 .Net Framework 的型別與由 Visual Basic .NET 或 C# 所提供之功能較強的語言建構函式。

使用者定義的資料型別 (UDT)

Yukon 新增了對 UDT 的支援,這是一種可讓您擴充資料庫之數值類型別系統的新機制。UDT 提供兩項從應用程式架構角度來看的主要優點:可在內部狀態與外部行為之間提供堅固的封裝 (同時存在於用戶端與伺服器內),以及可提供與其他相關伺服器功能更深入的整合。一旦您定義了自己的 UDT 之後,便能在所有的內容內使用該 UDT,讓您在 Yukon 內使用系統型別,包括資料行定義、變數、參數、函數結果、資料指標、觸發程序及複寫。

擴充的型別系統的過程可藉由完成下列步驟來達成:

  • 依照 UDT 的建立規則用 Managed 程式碼來建立類別。
  • 使用 CREATE ASSEMBLY 陳述式將含有您 UDT 的組件載入伺服器上的資料庫。
  • 使用公開 Managed 程式碼 UDT 的 CREATE TYPE 陳述式在資料庫內建立型別。

此時,您便能使用在資料表定義內的型別。

當您在 Managed 程式碼內所建立您的 UDT 定義時,型別須滿足下列需求。

  • 須標記為 Serializable
  • 須裝飾有 SqlUserDefinedTypeAttribute
  • 藉由實作 INullable 介面,讓型別須能識別 NULL 值。
  • 藉由實作下列方法,讓型別須能支援轉換成字串與從字串轉換。
    • Public String ToString()
    • Public Shared <type> Parse (SqlString s)
  • 型別須具有無須任何引數的建構函式。

實作具有下列特色的類別,應不會對已在撰寫 Managed 程式碼應用程式的開發人員造成問題。SqlUserDefinedTypeAttribute 是一項新的屬性,具有下列屬性。

名稱 描述
MaxByteSize 型別執行個體的最大大小。
IsFixedLength 用來描述型別的所有執行個體是否均為固定長度的布林值。
IsByteOrdered 用來描述是否要用型別的二進位表示法來進行比較的布林值。
Format UDT 的儲存格式;可以是 Native、UserDefined 或 SerializedDataWithMetadata 中的一種。

下列是一個在 Visual Basic .NET 內的 UDT 範例。

Imports System
Imports System.Data.Sql
Imports System.Data.SqlTypes
Imports System.Runtime.Serialization

<Serializable(), SqlUserDefinedTypeAttribute(Format.Native)> _
Public Structure Point

    Implements INullable
    Private is_Null As Boolean 
    Private m_x As Double
    Private m_y As Double

    Public ReadOnly Property IsNull() As Boolean _
       Implements INullable.IsNull
        Get
            Return (is_Null)
        End Get
    End Property

    Public Overrides Function ToString() As String
        If Me.IsNull Then
            Return "NULL"
        Else
            Return Me.m_x & ":" & Me.m_y
        End If
    End Function

    Public Shared Function Parse(ByVal s As SqlString) _
    As Point
        If s.IsNull Then
            Return Nothing
        Else
 
       'Parse input string here to separate out points
       Dim pt as new Point()
       Dim str as String = Convert.ToString(s)
       Dim xy() as String = str.Split(":")
       pt.x = xy(0)
       pt.y = xy(1)
            Return (pt)
        End If
    End Function

    Public Shared ReadOnly Property Null() As Point
        Get
            Dim pt As New Point
            pt.is_Null = True
            Return (pt)
        End Get
    End Property

    Public Property X() As Double
        Get
            Return (Me.m_x)
        End Get
        Set(ByVal Value As Double)
            m_x = Value
        End Set
    End Property

 
    Public Property Y() As Double
        Get
            Return (Me.m_y)
        End Get
        Set(ByVal Value As Double)
                m_y = Value
        End Set
    End Property
End Structure

在上述範例中,第一個有趣的第一個程式碼行是類別宣告。此類別宣告裝飾有 Serializable SqlUserDefinedTypeAttribute 屬性。SqlUserDefinedTypeAttribute 的 Format 屬性是用來決定 UDT 的儲存格式。

Yukon 支援三種序列化格式。每種序列化格式均各有特定的一般使用者狀況。下表列出了在各狀況之間的優劣:

狀況 最佳格式 優點 缺點
簡單的 Blittable 型別 (與結構類似) Native 精簡、快速 不支援參考型別
簡單的參考型別 (例如:包含一個字串欄位) UserDefined 序列化格式的完整控制、快速、宣告式程式設計模型 相當複雜 (運作您自己的序列化程式)
複雜的參考型別 (包含陣列、字串等) SerializedDataWithMetadata 支援複雜的型別定義、宣告式程式設計模型 耗用遠比前兩種格式更為龐大的空間與效能。

MaxByteSize 屬性會控制 UDT 的最大大小。藉由為您的 UDT 大小指定合理的最大值,便能確定 Microsoft SQL Server "Yukon" 能對您的 UDT 儲存體做出適當的判斷。在 SQL Server "Yukon" Beta 1 內之 UDT 的最大大小限制為 8K。

實作於範例類別內的 INullable 介面,讓型別能識別 NULL 值。型別還藉由實作 ParseToString() 方法來提供字串轉換。最後,型別另外還提供了兩個內性程序,以取得與設定標示此類別座標所在的 X 與 Y 值。

一旦您已將此類別編譯成組件之後,下一步則是在 Microsoft SQL Server "Yukon" Beta 1 內登錄它:

create assembly point from 'c:\yourpathhere\point.dll'
go

一旦載入組件之後,您可使用 CREATE TYPE 陳述式將型別新增到資料庫的可用型別清單內。

CREATE Type Point
external name [point]:Point

接著,在型別登錄成為資料庫的可用型別之一後,您便可開始在物件 (如資料表) 建立時使用該型別。

CREATE TABLE Points
(
   PointID int not null,
   Pnt Point not null
)

如前所示,在資料表定義內使用 UDT 無須撰寫任何特殊的程式碼。您可使用如其中一種內建資料型別 (如 nvarcharnchar) 的相同方式來定義資料表。若要使用 Transact-SQL 填入包含資料的資料表,可執行下列的指令碼。

DECLARE @startPoint Point
DECLARE @endPoint Point
SET @startPoint = CONVERT(Point, '5:5')
SET @endPoint = CONVERT(Point, '10:10')
-- SET @startPoint::X = 1
-- SET @startPoint::Y = 2

INSERT Points
VALUES(1, @startPoint)

INSERT Points
VALUES(2, @endPoint)

上述的許多程式碼對曾有使用過舊版 SQL Server 之 Transact-SQL 變數、DML 陳述式及 SELECT 陳述式經驗的人而言應不陌生。然而,在上述 Transact-SQL 程式碼內還有一些項目是值得注意。在兩個 Point 型別變數建立之後,會使用 CONVERT 陳述式來設定這兩個變數。當您以這種方式來使用 CONVERT 陳述式,便會呼叫 UDT 上的 Parse方法,以資料填入 UDT 執行個體。您還可明確地將數值傳送到 UDT 上的屬性,如加註的程式碼所示。然後可使用標準的 INSERT 陳述式,將 UDT 型別插入 Points 資料表的工作來完成。從儲存於資料表內的 UDT 中選取適當的值,是個藉由使用下列語法 ColumnName::Property 或方法名稱來指定您所想呼叫之內容屬性或方法的過程。

SELECT Pnt::X, Pnt::Y FROM Points

當使用 Managed 用戶端提供者 (ADO.NET) 來擷取 UDT 值時,會傳回型別的執行個體,亦即在狀態與行為之間的封裝會被保存在用戶端。一旦您擷取物件之後,同組的行為便可適用於在用戶端的行為消費者。

使用者定義的彙總

建立超越「現成」函式之額外彙總函式的能力,是使用舊版 SQL Server 的開發人員所沒有的。使用 Microsoft SQL Server "Yukon" Beta 1 版本,開發人員便能以 Managed 程式碼來建立自訂的彙總函式,並供 T-SQL 或其他的 Managed 程式碼存取這些函式。若要建立使用者定義的彙總,首先您須建立一個實作指定方法,並裝飾有 Serializable 與 SqlUserDefinedAggregate 屬性的類別。彙總的內容如下所示:

Imports System
Imports System.Data
Imports System.Data.SqlServer
Imports System.Data.SqlTypes
Imports System.Runtime.Serialization

<Serializable()> _
<SqlUserDefinedAggregateAttribute _
   Format.Native)> _
Public Class MyAgg
   
   Public Sub Init()

   End Sub

   Public Sub Accumulate(i as Integer)

   End Sub

   Public Sub Merge(otherAggregate as MyAgg)
   
   End Sub

   Public Function Terminate() as Integer
      Return([type])
   End Function
End Class

您可使用標準的 CREATE ASSEMBLY 陳述式來載入組件,然後建立彙總如下:

CREATE AGGEGATE MyAgg(@p1 INT)
RETURNS INT
EXTERNAL NAME Agg:MyAgg

SqlUserDefinedAggregate 屬性在某些方面與 SqlUserDefinedType 屬性類似。它還需要型別序列化的格式並指定最大大小。它另外還具有一些其他屬性,摘錄於下表內。

IsInvariantToDuplicates 選擇性屬性。只有當彙總對複本是不變時,才會傳回 true。例如:MAX 與 MIN 函式符合這項條件,而 SUM 卻不符合。
IsInvariantToNulls 選擇性屬性。指定彙總是否對 NULL 是不變的。例如:MAX 與 MIN 函式符合這項條件,而 COUNT 卻不符合。
IsInvariantToOrder 選擇性屬性。指定彙總是否對順序是不變的。如果它對順序是不變的,則查詢最佳化工具便具有較高彈性來選擇具有彙總之查詢的執行計畫。這個內容屬性的預設值為 false。

以 Managed 程式碼所實作的彙總還具有四個強制方法。Init 方法是用來將彙總初始化成為起始狀態。這個方法會在使用過後將彙總清除,以便彙總能再重新用來計算。Accumulate 方法須採用與在 CREATE AGGREGATE 陳述式內使用之輸入參數的型別相同的單一引數。這個方法會更新執行個體的狀態,並進行適用於此彙總的運算 (例如:COUNT 會累加內部計數器)。Merge 方法可讓查詢處理器用來合併此彙總類別的另一個執行個體成為部分運算的一份子。Terminate 方法會在運算處理工作結束時被呼叫。這個方法會傳回一個與 CREATE AGGEGATE 陳述式內RETURNS 元素所使用之型別相同的型別。

下列的彙總範例會將從資料表之資料行內所取得的字串值集合串連在一起。

Imports System
Imports System.Data
Imports System.Data.SqlServer
Imports System.Data.SqlTypes
Imports System.Runtime.Serialization
Imports System.Text

<Serializable()> _
<SqlUserDefinedAggregate( _
   Format.SerializedDataWithMetadata, MaxByteSize:=8000)> _
Public Class MyAgg
   Private sb as StringBuilder
   Private firstConcat as Boolean = True

   Public Sub Init()
      sb = new StringBuilder()
   End Sub

   Public Sub Accumulate(s as String)
      If firstConcat Then
         sb.Append(s)
         firstConcat = false
      Else
         sb.Append(", ")
         sb.Append(s)
      End If
   End Sub

   Public Sub Merge(otherAggregate as MyAgg)
      Accumulate(otherAggregate)      
   End Sub

   Public Function Terminate() as String
      Return(sb.ToString())
   End Function
End Class

如同多數的彙總般,實際工作均在 Accumulate 方法內完成。此處以參數方式傳送到 Accumulate 方法內的字串,會附加到已在 Init 方法內初始化的 StringBuilder 物件。假設這不是第一次呼叫 Accumulate ,則在附加傳送到 StringBuilder 的字串之前,還須先附加一個逗號。在結束運算工作時所呼叫的 Terminate 方法會將 StringBuilder 傳回成字串。

假設有個資料表具有下列的結構描述...

CREATE TABLE .BookAuthors
(
   BookID   int       NOT NULL,
   AuthorName    nvarchar(200) NOT NULL
)

會插入 A?A...及下列的資料列:

INSERT BookAuthors VALUES(1, 'Johnson')
INSERT BookAuthors VALUES(2, 'Taylor')
INSERT BookAuthors VALUES(3, 'Steven')
INSERT BookAuthors VALUES(2, 'Mayler')
INSERT BookAuthors VALUES(3, 'Roberts')
INSERT BookAuthors VALUES(3, 'Michaels')

A?A...及下列的資料列下列查詢所產生的結果會直接出現於其下。

SELECT BookID, dbo.MyAgg(AuthorName)
FROM BookAuthors
GROUP BY BookID
BookID Author Names
1 Johnson
2 Taylor, Mayler
3 Roberts, Michaels, Steven

結論

隨著 SQL Server "Yukon" Beta 1 的推出,資料庫程式設計人員現在能善加利用 .NET 基底類別庫與 Common Language Runtime (CLR) 的豐富功能。使用 CLR 整合,資料庫開發人員便能使用他們所選的 .NET 語言 (如 Visual Basic .NET 與 C#) 來建立他們的預存程序、函式、觸發程序及使用者定義的型別,而這提供了物件導向的建構函式、結構化例外處理、陣列、命名空間及類別。此外,.NET Framework 還提供了包含方便運用於伺服器端之廣泛內建能力的數千個類別與方法。許多棘手或很難以 Transact-SQL 來進行的工作,現在使用 Managed 程式碼便能夠更容易完成。同時還提供了兩種新的資料庫物件型別,彙總與使用者定義的型別。現在開發人員在撰寫同處理序程式碼時,可運用他們已具備的知識與技術。SQL Server "Yukon" Beta 1 可讓開發人員將資料戶伺服器擴充到更便於進行適當的運算與作業

posted on 2004-04-24 19:07  Goodspeed  阅读(1354)  评论(0)    收藏  举报