代码位置:数据库与中间层
通过在数据库中提供丰富的编程模型,CLR 集成提供了将逻辑从其他层移动到数据库层的选择。然而,这显然并不意味着所有或大部分逻辑应该移到数据库中。
将逻辑移到数据库层可以减少网络中的数据流量,但是增加了服务器上宝贵的 CPU 资源的负荷。因此,在应用程序中做出代码放置的决定之前,要慎重权衡。以下注意事项适用于将数据库层作为首选的代码位置:
| • |
数据验证:在数据层进行数据验证的逻辑可以更好地将数据和逻辑封装在一起。这样避免了在不同数据接触点(如:后端处理、批量上载和来自中间层的数据更新等)中重复验证逻辑。
|
| • |
减少网络流量:对于需要处理大量的数据而产生很少的数据的数据处理任务(如数据分析应用程序中的需求预测、基于需求预测的生产安排等)来说,将逻辑放在数据库层中是合适的。
|
注即使在引入 CLR 支持之前,上面的注意事项也是有效的。数据库层中的 CLR 支持意味着编程语言的选择没有妨碍代码位置的正确选择。
生产安排是制造企业的常见任务。在高层次上,它包括制订何时生产多少单位数量的产品的计划,以便能够满足需求、最大程度的降低库存成本,同时将生产成本降到最低。有几个算法将需求预测、库存成本和生产线安装成本作为输入,而将制造策略作为输出。
假定将来的需求预测存储在 SQL Server 表中,则此类算法的实现有以下特征:
在中间层实现这样的算法是可行的,但是在数据库外移动输入集方面有性能损失。在 T-SQL 中将其实现为存储过程也是可行的,但是因为需要复杂的计算,性能损失就显现出来了。性能特征将随着实际的数据量和算法的复杂性的不同而不同。
为了验证 CLR 集成是否适合于这样的情况,我们举一个特定的生产安排算法的示例 - Wagner-Whitin 算法的动态编程实现。正如所预料的,CLR 集成优于 T-SQL。对于这种情况,使用托管代码还有其他好处。这种算法的实现需要使用大量的一维和多维数组、数据结构,而这些在 T-SQL 中是不可用的。总之,CLR 集成的性能要优于 T-SQL 实现几个数量级。
|
PID
|
int
|
非空
|
产品主键 ID
|
|
Pname
|
nvarchar(256)
|
空
|
产品名称
|
|
InventoryCost
|
int
|
非空
|
存储该产品的每时段成本
|
|
StartupCost
|
int
|
非空
|
建立生产线来生产该产品的成本
|
|
PID
|
Int
|
非空
|
产品 ID
|
|
WeekDate
|
smalldatetime
|
非空
|
需求预测周
|
|
DemandQty
|
int
|
非空
|
特定产品和特定周的需求预测
|
给定一组产品,它们的库存和启动成本以及未来需求预测,我们创建了接受如下输入参数的存储过程:1)制订生产进度表的日期,2)按进度表生产所需要的周数。
|
Product
|
nvarchar(256)
|
产品名称
|
|
Period
|
datetime
|
进度周
|
|
Quantity
|
int
|
在指定周内制造的产品的数量
|
将 C# 版本的代码复制到下面的代码中,以说明这种可以从 CLR 集成中大大获益的情况:
using System;
using System.Data;
using System.Data.Sql;
using System.Data.SqlServer;
using System.Data.SqlTypes;
public class ProductionSchedule
{
//4-year limit on scheduling
public const int MAXPRODUCTS = 101;
public const int MAXWEEKS = 210;
public const int MAXNAME = 256;
public ProductionSchedule()
{
}
public static int Schedule(SqlDateTime startDate, int numWeeks)
{
SqlDateTime[] week = new SqlDateTime[MAXWEEKS];
int[] quantity;
int[][] Cij;
int[] Fk;
int[] minK = new int[MAXWEEKS];
int product_id, current_product, product_count = 0;
int startPeriod;
// We'll use arrays to keep state about products and forecasts
in memory. This is only viable given that we know we have a small number
of products and weeks.
// For larger data sets, we would have to consider cursors or
temporary tables.
// stored as CLR types since we know they can't be null
int[] h = new int[MAXPRODUCTS];
int[] K = new int[MAXPRODUCTS];
// stored as nullable SqlChars since the table schema allows for null names
SqlChars[] productNames = new SqlChars[MAXPRODUCTS];
bool moreProducts = true;
int optimal_j;
int period;
int sum;
SqlPipe pipe = SqlContext.GetPipe();
SqlDataRecord record;
object[] values = new object[3];
SqlMetaData[] metadata = new SqlMetaData[3];
//Initialize algorithm arrays
Cij = new int[MAXWEEKS][];
for( int l=0;l<MAXWEEKS;l++)
Cij[l] = new int[MAXWEEKS];
Fk = new int[MAXWEEKS];
//Look up K and h for all products
SqlCommand cmd = SqlContext.GetCommand();
cmd.CommandText = @"SELECT pname, InventoryCost, StartupCost from dbo.t_Products ORDER BY PID";
SqlDataReader reader = cmd.ExecuteReader();
while(reader.Read())
{
productNames[product_count] = reader.GetSqlChars(0); //product name
h[product_count] = reader.GetInt32(1); //holding cost
K[product_count] = reader.GetInt32(2); //startup cost
product_count++;
// if we exceeded number of expected products then bail out with an exception
if (product_count >= MAXPRODUCTS)
{
throw new Exception("Too many products");
}
}
reader.Close();
product_count = 0;
//Get the list of product ids;
cmd = SqlContext.GetCommand();
cmd.CommandText = @"select PID, weekdate, DemandQty from dbo.t_SalesForecast ORDER BY PID, WeekDate";
reader = cmd.ExecuteReader();
moreProducts=reader.Read();
//Set up the record for returning results
metadata[0] = new SqlMetaData( "Product",
SqlDbType.NVarChar,MAXNAME );
metadata[1] = new SqlMetaData( "Period", SqlDbType.DateTime );
metadata[2] = new SqlMetaData( "Quantity", SqlDbType.Int );
record = new SqlDataRecord( metadata );
while( moreProducts )
{
product_id = current_product = reader.GetInt32(0);
int index = 1;
quantity = new int[MAXWEEKS];
while( current_product == product_id )
{
week[index] = reader.GetSqlDateTime(1);
quantity[index] = reader.GetInt32(2);
index++;
moreProducts = reader.Read();
if( !moreProducts )
break;
current_product = reader.GetInt32(0);
}
//Determine the ordinal start week
startPeriod = 1;
//For each product ID calculate Cij
for( int i = startPeriod; i < (startPeriod + numWeeks); i++ )
{
for( int j = i+1; j <= (startPeriod + numWeeks+1); j++ )
{
Cij[i][j] = GetCij(quantity,i,j,K [product_count],h[product_count]);
}
}
//Calculate Fk
for( int k = startPeriod + numWeeks + 1; k >= startPeriod; k--)
{
minK[k] = GetFk_SO(k,startPeriod + numWeeks,Cij,Fk);
}
//Send the results
record.SetSqlChars(0,productNames[product_count]);
pipe.SendResultsStart(record,false);
for( int k = startPeriod; k < startPeriod + numWeeks; )
{
period = k;
optimal_j = minK[k];
sum = 0;
while( k < optimal_j )
{
sum = sum + quantity[k++];
}
values[1] = week[period];
record.SetValue(1,values[1]);
values[2] = sum;
record.SetValue(2,values[2]);
pipe.SendResultsRow(record);
}
pipe.SendResultsEnd();
product_count++;
}
reader.Close();
return 0;
}
private static int GetCij(int[] quantities, int i, int j, int K, int h)
{
if( j == i+1 )
return K;
else
return (j-1-i) * h * quantities[j-1] + GetCij(quantities, i, j-1,K,h);
}
private static int GetFk_SO(int k,int n,int[][] Cij, int[] Fk)
{
int j,min;
j = k+1;
min = j;
if ( k == n+1 )
{
Fk[k] = 0;
return j;
}
Fk[k] = Cij[k][j] + Fk[j];
for(; k <= n ;k++)
{
j = k + 1;
while( j <= n+1 )
{
if( Cij[k][j] + Fk[j] < Fk[k] )
{
min = j;
Fk[k] = Cij[k][j] + Fk[j];
}
j++;
}
}
return min;
}
}
处理常见数据库编程任务和问题
前一节在高层次上对基于 CLR 的编程与 T-SQL、中间层和扩展存储过程 (XP) 进行了比较。在这一节中,我们将考虑数据库应用程序开发人员经常遇到的一些编程任务和模型,并且讨论如何使用 CLR(以及在一些情况下如何不使用)进行处理。
SQL Server 2005 中的 CLR 集成允许用户利用 .NET Framework 类库提供的丰富功能来解决其数据库编程问题。
常规表达式的使用可以很好地说明 CLR 集成如何增强了验证和过滤功能。在处理数据库中存储的文本数据方面,常规表达式提供的模式匹配功能比通过 T-SQL 查询语言中的 LIKE 运算符可用的模式匹配功能多。考虑以下 C# 代码,它只是 System.Text.RegularExpressions 命名空间中的 RegEx 类的一个简单包装:
using System;
using System.Data.Sql;
using System.Data.SqlTypes;
using System.Text.RegularExpressions;
public partial class StringFunctions
{
[SqlFunction(IsDeterministic = true, IsPrecise = true)]
public static bool RegExMatch(string pattern, string matchString)
{
Regex r1 = new Regex(pattern.TrimEnd(null));
return r1.Match(matchString.TrimEnd(null)).Success;
}
[SqlFunction(IsDeterministic = true, IsPrecise = true)]
public static SqlString ExtractAreaCode(string matchString)
{
Regex r1 = new Regex("\\((?<ac>[1-9][0-9][0-9])\\)");
Match m = r1.Match(matchString);
if (m.Success)
return m.Value.Substring(1, 3);
else return SqlString.Null;
}
};
假设 StringFunctions.RegExMatch 和 StringFunctions.ExtractAreaCode 方法已经被注册为带有 RETURNS NULL ON NULL INPUT 选项的数据库中的用户定义函数(这允许该函数在任何输入都为 NULL 时返回 NULL,这样在该函数内就没有特殊的 NULL 处理代码):
现在,可以在使用上述代码的表的列中定义约束,以验证电子邮件地址和电话号码,如下所示:
create table Contacts
(
FirstName nvarchar(30),
LastName nvarchar(30),
EmailAddress nvarchar(30) CHECK
(dbo.RegExMatch('[a-zA-Z0-9_\-]+@([a-zA-Z0-9_\-]+\.)+(com|org|edu)',
EmailAddress) = 1),
USPhoneNo nvarchar(30) CHECK
(dbo.RegExMatch('\([1-9][0-9][0-9]\) [0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9]',
UsPhoneNo)=1),
AreaCode AS dbo.ExtractAreaCode(UsPhoneNo) PERSISTED
)
另外,请注意 AreaCode 列是使用 dbo.ExtractAreaCode 函数从 USPhoneNo 列中取出地区代码而得到的列。然后,可以对 AreaCode 列建立索引,这样便于在表格中根据特定地区代码查找联系人的查询。
更一般地讲,此示例演示了如何利用 .NET Framework 库来增强带有有用函数的 T-SQL 内置函数库,这些有用函数很难用 T-SQL 表达。
需要从运行在服务器内的数据库对象(如存储过程或视图)中产生结果集可能是最常见的数据库编程任务之一。如果可以使用单个查询(SELECT 语句)来构建结果集,则这只需使用视图或在线表值函数即可实现。然而,如果需要多个语句(过程逻辑)来构建结果集,则有两个选择:存储过程和表值函数。虽然 SQL Server 2005 有表值函数,但是它们只能用 T-SQL 进行编写。在 SQL Server 2005 中,通过 CLR 集成,还可以使用托管语言来编写这样的函数。在这一节中,我们将讨论如何决定使用存储过程还是使用表值函数,以及使用 T-SQL 还是使用 CLR。
从 T -SQL 过程可以将相关的结果作为表值函数的返回值返回,或者通过存储过程内曾经隐式存在的“调用者管道”返回。从存储过程的任何位置(不管执行的嵌套程度如何)执行 SELECT 语句都会把结果返回给调用者。更严格地讲,实际上 SELECT 语句并没有进行变量赋值。而且,FETCH、READTEXT、PRINT 和 RAISERROR 语句也隐式地将结果返回给调用者。
请注意,“调用者”一直没有正确地定义,它实际上取决于存储过程的调用上下文。
如果从任何客户端数据访问 API(如 ODBC、OLEDB 和 SQLClient)中调用存储过程,则调用者是实际的 API,并且它提供的任何一种抽象都可以表示结果(如 hstmt、IRowset 或 SqlDataReaderand)。这意味着,通常,从存储过程中产生的结果将始终返回到调用 API 中,而跳过堆栈中所有的 T-SQL 框架,如以下示例中所示:
create proc proc1 as
select col1 from dbo.table1;
create proc proc2 as
exec proc1;
在执行过程 proc2 时,proc1 产生的结果将转到 proc2 的调用者。proc2 中只有一种方法可以捕获产生的结果,即通过使用 INSERT/EXEC 将其存储到永久表、临时表或表变量中,从而将结果流式处理到磁盘。
create proc proc2 as
declare @t table(col1 int);
insert @t (col1) exec proc1;
-- do something with results
在使用 INSERT/EXEC的情况下,“调用者”是 INSERT 语句的目标表/视图。
SQL Server 2005 CLR 存储过程引入了新的“调用者”类型。当通过托管框架中的 in-proc 提供程序执行查询时,就可以通过 SqlDataReader 对象使结果可用,并且可以在存储过程中使用结果。
...
SqlCommand cmd=SqlContext.GetCommand();
cmd.CommandText= "select col1 from dbo.table1";
SqlDataReader sdr=cmd.ExecuteReader();
while (sdr.Read())
{
// do something with current row
}
...
下面的问题是托管存储过程如何将结果返回给它的调用者而不是通过 SqlDataReader 来使用它。这可以通过称为 SqlPipe 的新类来实现。通过 SqlContext 类的静态方法可以使此类的实例对托管存储过程可用。SqlPipe 有几种方法可以将结果返回给存储过程的调用者。这两个类都是在 Sqlaccess.dll 中定义的。
在 SqlPipe 类中可以使用的方法中,最容易理解的就是 Execute 方法,它将命令对象作为参数接受。这个方法主要执行命令,并且没有使执行的结果可用于托管框架,而是将结果发送给存储过程的调用者。发送结果的这种形式在语义上与将语句嵌入 T-SQL 存储过程内是一样的。在本文前面描述的性能方面,SqlPipe.Execute 与 T-SQL 是等价的。
create proc proc1 as
select col1 from dbo.table1;
The equivalent in C# would be:
public static void proc1()
{
System.Data.SqlServer.SqlCommand cmd=SqlContext.GetCommand();
cmd.CommandText= "select col1 from dbo.table1";
SqlContext.GetPipe().Execute(cmd);
}
对于返回的数据是由执行的查询直接产生的情况,SqlPipe.Execute 可以很好地工作。然而,在某些情况下可能希望1)从数据库中获得结果,进行操作或者转换,然后发送它们,或者 2)将结果发送回原地而不是本地 SQL Server 实例。
SqlPipe 提供了一组可以协同工作以使应用程序可以将任何结果返回给调用者的方法:SendResultsStart、SendResultsRow 和 SendResultsEnd。在很大程度上,这些 API 类似于对扩展存储过程的开发人员可用的 srv_describe 和 srv_sendrow API。
SendResultsStart 将 SqlDataRecord 作为参数接受,并且指示返回的新结果集的开头。该 API 从记录对象读取元数据信息,并且将其发送给调用者。该方法有重载,以允许发送元数据以及记录中的实际值。
随后可以返回行,方法是对要发送的每行调用一次 SendResultsRowows。在发送完全部所需的行之后,需要调用 SendResultsEnd 来指示结果集的结尾。
例如,下面的 C# 代码片段表示一个存储过程,它读取 XML 文档(来自 MSDN 的 Really Simple Syndication [RSS] 供给),使用 System.Xml 类进行解析,并且以相关的形式返回信息。请注意,这些代码应该创建为 EXTERNAL_ACCESS(或 UNSAFE)程序集,因为访问 Internet 所需的代码访问安全 (CAS) 权限只有在这些权限集中才是可用的。
// Retrieve the RSS feed
XPathDocument doc = new XPathDocument("http://msdn.microsoft.com/sql/rss.xml");
XPathNavigator nav = doc.CreateNavigator();
XPathNodeIterator i = nav.Select("//item");
// create metadata for four columns
// three of them are string types and one of the is a datetime
SqlMetaData[] rss_results = new SqlMetaData[4];
rss_results[0] = new SqlMetaData("Title", SqlDbType.NVarChar, 250);
rss_results[1] = new SqlMetaData("Publication Date", SqlDbType.DateTime);
rss_results[2] = new SqlMetaData("Description", SqlDbType.NVarChar, 2000);
rss_results[3] = new SqlMetaData("Link", SqlDbType.NVarChar, 1000);
// construct the record which holds metadata and data buffers
SqlDataRecord record = new SqlDataRecord(rss_results);
// cache a SqlPipe instance to avoid repeated calls to SqlContext.GetPipe()
SqlPipe sqlpipe = SqlContext.GetPipe();
// send the metadata, do not send the values in the data record
sqlpipe.SendResultsStart(record, false);
// for each xml node returned, extract four pieces
// of information and send back each item as a row
while (i.MoveNext())
{
record.SetString(0, (string)
i.Current.Evaluate("string(title[1]/text())"));
record.SetDateTime(1, DateTime.Parse((string)
i.Current.Evaluate("string(pubDate[1]/text())")));
record.SetString(2, (string)
i.Current.Evaluate("string(description[1]/text())"));
record.SetString(3, (string)
i.Current.Evaluate("string(link[1]/text())"));
sqlpipe.SendResultsRow(record);
}
// signal end of results
sqlpipe.SendResultsEnd();
注在 SendResultsStart 和 SendResultsEnd 调用之间,SqlPipe 被设置为繁忙状态,调用除 SendResultsRow 之外的任何 Send 方法都会导致错误发生。SqlPipe 处于繁忙状态中时,SendingResults 属性被设置为 TRUE。
CLR 集成也启用了对用托管语言编写的表值函数 (TVF) 的支持。与 T-SQL 相似,TVF 主要用于返回表结果。最显著的不同在于,T-SQL 表值函数临时将结果存储在工作表中,而 CLR TVF 则能够对产生的结果数据进行流式处理。这意味着结果在从函数返回之前不需要物化。
注 T-SQL 还具有内联 TVF 的概念,即不临时存储结果。内联 TVF 在大部分语义上便于指定子查询(可能带有参数)。
托管 TVF 返回 ISqlReader 接口,这是由 SqlClient 和 SqlServer (in-proc) 托管提供程序中的 SqlDataReader 实现的一种只进光标抽象。查询处理器调用此接口上的 Read() 方法,以在每行返回 FALSE 之前获取它。
将上面的示例改为返回来自 RSS 供给的信息,返回结果的代码如下所示(不包括未实现的方法):
[SqlFunction]
public static ISqlReader tvf1()
{
return (ISqlReader)new RssReader();
}
public class RssReader : ISqlReader
{
SqlMetaData[] rss_results = null;
XPathDocument doc;
XPathNavigator nav;
XPathNodeIterator i;
// Construct helper class, initializing metadata for the results
// reading from the RSS feed, creating the iterator
public RssReader()
{
rss_results = new SqlMetaData[4];
rss_results[0] = new SqlMetaData("Title", SqlDbType.NVarChar, 250);
rss_results[1] = new SqlMetaData("Publication Date",
SqlDbType.DateTime);
rss_results[2] = new SqlMetaData("Description", SqlDbType.NVarChar, 2000);
rss_results[3] = new SqlMetaData("Link", SqlDbType.NVarChar, 1000);
// Retrieve the RSS feed
doc = new XPathDocument("http://msdn.microsoft.com/sql/rss.xml");
nav = doc.CreateNavigator();
i = nav.Select("//item");
}
// # of columns returned by the function
public int FieldCount { get { return rss_results.Length; } }
// metadata for each of the columns
public SqlMetaData GetSqlMetaData(int FieldNo)
{ return rss_results[FieldNo]; }
// Read method positions the navigator iterator on next element
public bool Read() { return i.MoveNext(); }
// methods to return each column
public Object GetValue(int FieldNo)
{
switch (FieldNo)
{
case 0:
return new SqlString((string)
i.Current.Evaluate("string(title[1]/text())"));
case 1:
return new SqlDateTime(DateTime.Parse(
(string)i.Current.Evaluate("string(pubDate[1]/text())")));
case 2:
return new SqlString((string)
i.Current.Evaluate("string(description[1]/text())"));
case 3:
return new SqlString((string)
i.Current.Evaluate("string(link[1]/text())"));
}
return null;
}
public string GetString(int i) { return (string)GetValue(i); }
public DateTime GetDateTime(int i) { return (DateTime)GetValue(i); }
public SqlChars GetSqlCharsRef(int i) {
return new SqlChars((SqlString)GetValue(i)); }
public SqlChars GetSqlChars(int i) {
return new SqlChars((SqlString)GetValue(i)); }
public SqlDateTime GetSqlDateTime(int i) {
return (SqlDateTime)GetValue(i); }
...
}
select title, pubdate, description, link from dbo.GetRssFeed()
显然可以用此数据的 TVF 形式来表示更丰富的查询。假定函数 CanonicalURL() 会返回规范的 URL 版本。现在,可以使用规范的 URL 很容易地返回来自 RSS 供给的数据:
select title, pubdate, description, dbo.CanonicalURL(link)
from dbo.tvf1()
order by pubdate
请注意,在这个示例中,我们没有利用 TVF 的流化功能,因为我们正在使用整个 RSS 供给,在此之上建立导航器,然后在调用 Read() 时循环访问各个项目。然而,可以想象使用流式 API 来使用 Web 资源的结果,并且使用 XmlReader 来循环访问产生的 XML。需要重点注意的是,给出了 CLR 表值函数和 T-SQL 函数之间的执行模型的不同时,最主要的性能差异可能有利于 CLR TVF,特别是在可能将结果数据流化的情况下。
决定将存储过程与 SqlPipe (不管是 T-SQL 中使用隐式管道还是在托管中使用显式托管类)结合使用,还是使用表值函数,取决于以下几个必须考虑的因素:
有时可能需要重用或进一步处理 TVF 或存储过程中产生的结果。从可组合性的角度来说,表值函数更灵活。TVF 的返回类型是相关的行集,可以用在允许此类构建的任何地方。特别是,它可以用在 SELECT 语句的 FROM 子句中,因为这些产生的结果可以受益于子查询中的 SELECT、INSERT/SELECT、派生的表和通用表表达式等的可组合性。
另一方面,从 T-SQL 语言中,存储过程可以组合成 INSERT / EXEC 组合的唯一部分,这使得可以将产生的结果存储在永久或临时表中。INSERT 操作表示数据的实际副本,它可能会影响性能。
如果需要组合和重用服务器中的结果,TVF 是更好的替代方法。如果产生的结果只需要回流到客户端/中间层,任何一种方法都可以完成这项工作。
返回的数据源是在基于 T-SQL 和基于 CLR 的实现之间做出决定的另一个重要因素。可以通过使用 in-proc 提供程序读取本地实例中的一些数据源产生结果,也可以从 SQL Server 之外的数据源产生结果。本文前面描述的基于 Web 请求的结果构造的代码片段便是后者的示例。另一个远程数据源的示例是,使用 SqlClient 托管提供程序从远程SQL Server 实例中检索结果。对于这样的外部源,基于 CLR 的实现是更好的选择,因为使用它可以很容易地实现访问外部数据的逻辑。
现在让我们考虑这种情况,使用 in-proc 提供程序基于在本地实例中执行的查询生成结果。在使用 TVF 的情况下,默认的处理可能是返回由 in-proc 提供程序产生的 SqlDataReader,或者用 ISqlReader 的自定义实现包装这样的阅读器,以便在读取结果时对其进行转换。在使用存储过程的情况下,根据本地实例产生的结果必须执行查询,循环访问读取行,对结果执行一些操作,然后通过管道将其发送回去。
然而,SQL Server 2005 不允许表值函数返回时请求还处于未决状态。在函数体可以返回之前,必须全部执行任何通过 in-proc 提供程序执行的查询并且完全使用结果。如果执行了返回语句,而 in-proc 提供程序中的 SqlDataReader 操作还处于未决状态,就会引发错误。这意味着对于从本地数据库实例返回数据的大多数情况,无法通过 CLR TVF 流化结果。如果因为其他因素(例如可组合性)需要将此编写为 TVF,则使用 T-SQL 编写是唯一的选择。另外,通过 SqlPipe 使用托管存储过程是一个可能的选择。
请注意,对于基于来自本地实例的数据从存储过程中产生结果的情况,SendResultsXXX API 的使用只有在需要对结果进行修改或处理时才有意义。如果在未作修改的情况下将结果发送给调用者,则 SqlPipe.Execute 是更好的执行解决方案。
一般来说,不允许用户定义的函数(特别是表值函数)执行副作用操作。其中包括改变数据库状态的操作(如 DML 语句或事务处理操作)。在产生结果前后可能需要对系统状态作一些修改。例如,业务组件可能需要设置 SAVEPOINT 事务,执行一些 UPDATE,并且通过管道返回结果;但是如果出现错误,则回滚到 SAVEPOINT。
如果不允许从 TVF 执行副作用操作,则只有存储过程才能实现这样的方案,并且必须通过 SqlPipe 返回结果。
请注意,当 SqlPipe 忙于发送结果时,尤其不允许通过 in-proc 提供程序执行副作用操作。只有在完成结果集之前或之后允许进行这些操作。
从上面的代码示例和与 T-SQL 一致的角度来看,由存储过程通过 SqlPipe 产生的结果的描述不同于 TVF。TVF 是强类型化的,并且作为注册 (CREATE FUNCTION) 语句的一部分,它必须静态地定义 TVF 产生的结果的列数和类型。
另一方面,存储过程声明并没有声明产生的结果 - 甚至是否返回结果。这看起来可能很方便,虽然它确实提供了更大的灵活性,但是在编写执行存储过程的应用程序时要更加细心,因为存储过程可以动态地重定义产生的结果的形式。
因此,自然而然建议根据元数据来描述结果:如果结果的架构需要根据调用的不同而变化,则只有 SqlPipe 才能提供这种灵活性。
同样地,通过存储过程内的 SqlPipe 产生的结果的弱类型化不能将单个结果的架构扩展为可能返回可变数量的结果集。存储过程可以自由地根据条件确定是否发送给定的行集和定义其形式。这样的灵活性增加了使用这种可变的结果流的应用程序的复杂性开销。
| • |
存储过程(使用隐式 SqlPipe 或基于显式 CLR 的方法)和 TVF
|
| • |
|
|
需要可组合性?
|
TVF
|
TVF 过程或 TVF
|
|
外部数据源(与只访问本地数据)?
|
CLR TVF 或 CLR 过程
|
(只访问本地数据)T-SQL TVF 或过程
|
|
需要副作用?
|
过程
|
过程或 TVF
|
|
固定的结果架构?
|
过程或 TVF
|
过程
|
|
多个结果集?
|
过程
|
过程或 TVF
|
|
流化结果的能力?
|
CLR TVF
|
T-SQL TVF
|
对于本节的大部分内容,通过 SqlPipe 发送结果是与过程紧密相关的。即使在 CLR 触发器主体中 SqlPipe 是可用的并且返回结果是可能的,也很不提倡这种做法,因为使用在目标对象中定义的触发器发出数据处理语言 (DML) 或数据定义语言 (DDL) 语句可能会导致意外的结果。
经常需要在应用程序中传送多值参数。例如,在定单处理系统中,可能需要编写存储过程来将定单插入到 Orders 表中。存储过程中的参数之一可能是定单中的行项目。在这种情况下,您会遇到 T-SQL 限制,它不支持表值参数或缺乏集合数据类型(如数组)。解决这个问题的一种方法是,将集合编码为一个标量值(如 nvarchar 或 xml),然后将其作为参数传递给存储过程。在存储过程内,可以使用表值函数来接受标量输入,并将其转换成一组行,然后将这些行插入到 LineItems 表中。
虽然可以用 T-SQL 编写表值函数,但是用 CLR 实现它有两个好处:
| • |
System.Text 命名空间中的字符串处理函数使得编写表值函数更加容易。
|
| • |
CLR TVF 提供了更有效的流实现,这避免了将结果加载到工作表中。
|
下面的代码片段显示了如何实现一个表值函数,它接受以‘;’分隔的一组值作为输入字符串,并且以一组行(字符串中的每个值一行)的形式返回该字符串。请注意,MySqlReader 类的构造函数实现了大部分工作,它使用 System.String.Split 方法将输入字符串分解为数组。
// TVF that cracks a ';' separated list of strings into a result
// set of 1 nvarchar(60)column called Value
public static ISqlReader GetStrings(SqlString str)
{
return (ISqlReader)new MySqlReader(str);
}
public class MySqlReader : ISqlReader
{
private string[] m_strlist;
private int m_iRow = -1; // # rows read
//The core methods
//Initialize list
public MySqlReader(SqlString str)
{
//Split input string if not database NULL;
//else m_strlist remains NULL
if (!str.IsNull)
{
m_strlist = str.Value.Split(';');
}
}
// SECTION: Metadata related: Provide #, names, types of
// result columns
public int FieldCount { get { return 1; } }
public SqlMetaData GetSqlMetaData(int FieldNo)
{
if (FieldNo==0)
return new SqlMetaData("Value", SqlDbType.NVarChar, 60);
else throw new NotImplementedException();
}
// SECTION: Row navigation. Read is called until it returns
// false. After each Read call, Get<TypeName> for each
// column is called.
public bool Read()
{
//Return empty result set if input is DB NULL
//and hence m_strlist is uninitialized
if (m_strlist==null) return false;
m_iRow++;
if (m_iRow == m_strlist.Length)
return false;
return true;
}
//Column getters
//Implement Get<SqlTypeName> for each column produced by
//the TVF; in this case just one.
public SqlChars GetSqlChars(int i)
{
if (i == 0)
return new SqlChars(m_strlist[m_iRow]);
else
throw new NotImplementedException();
}
//Methods not used by SqlServer omitted;
//Actual implementation should provide an empty
//implementation.
...
} // public class MySqlReader
} // class StringFunctions;
假定 GetStrings 方法注册为具有相同名称的 TVF。下面是存储过程的代码片段,它使用此 TVF 从定单中提取表形式的行项目。
CREATE PROCEDURE Insert_Order @cust_id int, @lineitems
nvarchar(8000)
AS
BEGIN
...
INSERT LineItems
SELECT * FROM dbo.GetStrings(@lineitems)
...
END
在许多情况下,您可能需要对数据进行聚合。这包括执行统计计算(如 avg、stddev 等等)。如果所需的聚合函数不是作为内置聚合函数直接支持的,SQL Server 2005 中有三种方法可以进行这样的自定义聚合:
让我们在一个称为 PRODUCT(int) 的简单聚合函数的上下文中检查这三种替代方法,该聚合函数计算一组给定值的乘积。
下面是此函数的主干 C# 代码示例。所有的积累逻辑都在 Accumulate 函数中(为了简单起见,其他函数显示为 {...})。
[SqlUserDefinedAggregate(Format.Native)]
public struct Product
{
public void Accumulate(SqlInt32 Value)
{
m_value *= Value;
}
public void Init() {...}
public void Merge(Product Group) {...}
public SqlInt32 Terminate() {...}
}
在定义类型、创建程序集和注册到 SQL Server 之后,就可以通过以下方式使用 T-SQL中的聚合函数:
SELECT dbo.Product(intcol)
FROM tbl
GROUP BY col
作为使用 SqlDataReader 的托管存储过程实现的 PRODUCT
可以创建存储过程来执行查询和循环访问结果,以执行计算。这种循环访问是通过使用 SqlDataReader 类完成的。
[SqlProcedure]
public static void Product(out SqlInt32 value)
{
SqlCommand cmd = SqlContext.GetCommand();
cmd.CommandText = "select intcolumn from tbl";
SqlDataReader r = cmd.ExecuteReader();
bool first = true;
using (r)
{
while (r.Read()) //skip to the next row
{
if (first)
{
value = r.GetSqlInt32(0);
first = false;
}
else
{
value *= r.GetSqlInt32(0);
}
}
}
}
EXEC Product @p OUTPUT
作为使用光标的 T-SQL 存储过程实现的 PRODUCT
可以创建 T-SQL 存储过程来执行查询和通过使用 T-SQL 光标循环访问结果,以执行计算。
create procedure TSQL_ProductProc (@product int output)
as
begin
declare @sales int
declare c insensitive cursor for select intcolumn from tbl
open c
fetch next from c into @sales
if @@FETCH_STATUS = 0
set @product = @sales
while @@FETCH_STATUS = 0
begin
fetch next from c into @sales
set @product = @product * @sales
end
close c
deallocate c
end
决定是使用 UDA 还是使用其他某种解决方案来产生结果取决于几个因素:
| • |
可组合性要求。UDA 实际上是独立的对象,可以用于任何 T-SQL 查询,通常用在可以使用系统聚合函数的任何相同的地方。不需要假定它所操作的查询。例如,可以将其包括在视图定义(不过,索引视图中不支持 UDA)和标量子查询中。
|
| • |
聚合算法细节。在 Order By 子句(如果查询中有)之前可能对 UDA 进行求值,因此不能保证传递给聚合函数的值的顺序。如果聚合算法需要按照特定的顺序使用值,则不能使用 UDA。同样地,UDA 从整组中使用值并且返回单一值。如果需要必须为组中的每个值返回值的聚合函数,则应该考虑使用存储过程或流表值函数来编写您的函数。详细信息请参见本文中的“产生结果”一节。
|
| • |
对副作用和数据访问的需要。不允许 UDA 进行数据访问或有副作用。如果您的函数需要保留大量的数据作为聚合的中间状态,或因为其他某种原因需要进行数据访问,则必须使用过程。
|
使用 UDA 的第一种方法在这三个选择中可能提供最好的性能。通常,如果没有碰到上面所列的限制,就应该尝试将聚合函数编写为 UDA。如果无法使用 UDA 方法,则使用 SqlReader 的托管代码方法可能比 T-SQL 光标方法执行得更好。
可以用 UDA 方法编写的有用的聚合的示例还包括:找到每组中第 N 大(或第 N 小)值,找到每组中前 N 个最大值的平均值或总和,等等。
用户定义的类型 (UDT)
现在,我们来讲 SQL Server 2005 中功能更强大但是经常被错误理解的一个功能。使用用户定义的类型 (UDT),可以扩展数据库的标量类型系统(不仅仅为系统类型定义您自己的别名,这在 SQL Server 以前的版本中一直可用)。定义 UDT 就像用托管代码编写类,创建程序集,然后使用“create type”语句在 SQL Server 中注册该类型一样简单。下面是实现 UDT 的主干代码:
[SqlUserDefinedTypeAttribute(Format.Native)]
public struct SimpleUdt: INullable
{
public override string ToString() {...}
public bool IsNull { get; }
public static SimpleUdt Null { get; }
public static SimpleUdt Parse(SqlString s) {...}
...
}
create type simpleudt from [myassembly].[SimpleUdt]
create table t (mycolumn simpleudt)
SQL Server 2005 中的 UDT 不是对象相关的扩展性机制。它们是扩展数据库的标量类型系统的一种方法。标量类型系统包括 SQLServer 附带的列类型(如 int、nvarchar 和 uniqueidentifier 等类型)。例如,使用 UDT,可以定义您自己的、用于列定义的类型。如果您的类型确实是一个适合建模为列的原子值,请创建 UDT。
如果需要定义您自己的标量类型,请使用 UDT。这种类型的示例情况包括各种日历中的自定义日期/时间数据类型和货币数据类型。使用 UDT,可以创建单个对象来公开类型上可用的所有行为,并且封装或隐藏该类型所存储的基础数据。需要访问数据的每个人都必须使用 UDT 的编程接口。如果能够利用 .NET Framework 中现有的功能(如国际化或日历功能),这实际上又是考虑将类型实现为 UDT 的一个很好的理由。
不要使用 UDT 来对复杂的业务对象(如雇员、联系人或客户)进行建模。您将会陷入 UDT 的所有列限制(如,8KB 大小限制、索引限制)和在更新 UDT 值时更新整个值的不利方面。对于复杂类型,UDT 不是合适的数据建模抽象;因此对于这种情况,最好使用中间层对象相关映射技术。
因为它们是列形式的,所以可以定义整个 UDT 值的索引,并且创建参考完整性约束(如 UDT 列的唯一性)。还可以在比较和排序方案中使用 UDT。
比较 UDT 值是通过比较类型的基础二进制表示完成的。如果使用 Format.Native 作为持久性机制,则会按照同在该类型中定义的一样的字段顺序创建永久形式;因此,请确保这些字段是按照类型的正确顺序排列的。
UDT 上的每个操作(除了比较)都要求 UDT 的值反序列化,接着进行方法调用。这种模式有与之相关的固定开销。如果要将类型建模为 UDT(相对于表中的列),则在访问类型属性(相对于表中的列)时应该考虑这种差别。如果类型上的行为非常复杂,则应该考虑使用 UDT。如果类型没有任何与之相关的行为,则应该考虑将数据存储为表中的列。
如果发现需要实现相关函数的库,则 UDT 中的静态方法是一种可以方便地创建这种库的封装机制。可以通过使用 :: 语法来调用T-SQL 中的静态方法,如下所示:
select imagetype::MyFunction(@arg1)
客户希望在 UmAlQuraCalendar 中存储日期时间值,这与 SQL Server 日期时间数据类型使用的 Gregorian 日历不同。他们想让这个日期类型具有相同的基本行为集,即字符串转换、日期部分、日期算法和 GetDate()。
下面是这一数据类型的代码片段。它使用 UmAlQuraCalendar 2.0 版,这是 .NET Framework 中的新类型。
[SqlUserDefinedType(Format.Native, IsByteOrdered = true)]
public struct UmAlQuraDateTime : INullable
{
//Private state
private long dtTicks;
//Calendar object used for all calendar-specific operations
private static readonly UmAlQuraCalendar s_calendar = new UmAlQuraCalendar();
public static UmAlQuraDateTime Null
{
get
{
UmAlQuraDateTime dt = new UmAlQuraDateTime();
dt.isNull = true;
return dt;
}
}
public bool IsNull
{
get
{
return this.isNull;
}
}
//Convert into CLR DateTime type
public DateTime DateTime
{
get { return new DateTime(this.dtTicks); }
}
public static UmAlQuraDateTime Parse(SqlString s)
...
public override string ToString()
...
public static UmAlQuraDateTime ParseUsingFormat(string data, string fmt)
...
public string ToStringUsingFormat(string format)
...
public static UmAlQuraDateTime Now
{
get
{
return new UmAlQuraDateTime(DateTime.Now);
}
}
public static UmAlQuraDateTime FromSqlDateTime(SqlDateTime d)
{
if (d.IsNull) return Null;
return new UmAlQuraDateTime(d.Value);
}
public int Year
{
get
{
return s_calendar.GetYear(this.DateTime);
}
}
public int Month
...
public int Hour
...
public int Minute
...
public int Second
...
public double Milliseconds
...
public long Ticks
...
public SqlDateTime ToSqlDateTime()
{
return new SqlDateTime(this.DateTime);
}
public UmAlQuraDateTime AddYears(int years)
{
return new UmAlQuraDateTime(s_calendar.AddYears(this.DateTime, years));
}
public UmAlQuraDateTime AddDays(int days)
...
public UmAlQuraDateTime AddMonths(int months)
...
public UmAlQuraDateTime AddHours(int hours)
...
public UmAlQuraDateTime AddMinutes(int minutes)
...
public double DiffDays(UmAlQuraDateTime other)
...
-- convert it to arabic (for testing purposes)
declare @h hijridatetime
set @h = hijridatetime::ParseArabicString('01/02/1400')
select @h.ToArabicString(), @h.ToArabicStringUsingFormat('F')
-- convert sql datetime to hijri and back
declare @h hijridatetime
set @h = hijridatetime::FromSqlDateTime(GetDate())
select @h.ToArabicString(), @h.ToSqlDateTime()
-- get the current hijri datetime, in two ways
select hijridatetime::Now.ToString(),
hijridatetime::FromSqlDateTime(GetDate()).ToString()
-- do some arithmetic:
declare @h hijridatetime, @d datetime
set @h = hijridatetime::Now -- get the current hijri datetime
set @h = @h.AddDays(10) -- add ten days to it
set @d = GetDate() -- current sql date
set @d = DateAdd(day, 10, @d) -- add ten days to the sql datetime
-- print 'em both, should be the same
select @h.ToSqlDateTime(), @d
-- datepart
declare @h hijridatetime
set @h = hijridatetime::Now -- get the current hijri datetime
select @h.Year as year, @h.Month as month, @h.Day as month