SQL Server 2005(Yukon CLR)基本架构
概述
随着SQL Server “Yukon” Beta1(也许也有人说:SQL Server2005)的推出,人们发现微软在Yukon中集成了非常多的新功能,其中最引人瞩目的是在数据库引擎中集成了对Windows .NET Framework Common Language Runtime (CLR 公用语言运行环境)的支持。在2000年7月的专业开发者大会(PDC)上,微软第一次向世界展示了这个新特征。
作为SQL Server 2000的开发人员,编写数据库的应用程序往往只局限于T-SQL,而宿主了CLR的Yukon,引入了许多强大的新特征,我们可以用Visual Basic.NET, C# 等面向对象的语言完成以前T-SQL难于完成的任务。比如,以前我们如果要调用一些系统函数(WIN32API或COM组件),我们必须写扩展存储过程,通过ODS(Open Data Services)开放式数据服务层和数据库引擎交互,而现在我们通过CLR提供的托管对象可以非常方便的调用Windows .NET Framework Class Library (FCL).NET框架类库。这些托管对象包括:
· 托管存储过程
· 托管函数
· 托管触发器
· 自定义复杂数据库类型
· 自定义复杂类型索引
· 自定义集合函数
首先,我们要了解数据库引擎集成CLR的优势和CLR可以提供的一些关键特征。
CLR是一个托管的运行环境,所谓“托管”的意思是许多任务过去需要程序员负责的(比如内存管理)现在可以委托CLR来处理。作为托管代码的一部分,CLR控制代码执行过程中的每一个部分,同时CLR是基于类型安全(CLR扮演了代码验证的角色)和安全许可的环境――彻底地编译为本地代码(Native Code)。CLR可以“辨识”出运行的代码是否企图直接处理内存或调用非.NET的代码。
.NET的可执行代码在CLR中被装载为程序集(assemblies)的形式。程序集中包括中间语言代码(IL指令),描述代码的元数据(Metadata)和其它资源(比如:引用其它文件列表),源代码并不是直接保存在程序集中,而是编译为一种CLR可以读懂的中间语言(IL),在程序执行之前CLR通过即时编译(JIT)将代码转换为本地代码(通常是X86代码)。如果代码在本地运行,这个本地代码会被加载到一段拥有生命周期的缓存,这就是说.NET的代码是非解释型的――它在运行时最终会被编译成本地代码。
CLR拥有自己管理内存的方式:如果一个对象或一组对象没有被其它运行的代码引用,从理论上说要回收所有不在使用的内存,但是对于CLR,内存只是在运行资源不足时才会被回收,CLR在需要额外的内存资源时才调用垃圾收集器,这种内存管理机制使程序员不用担心内存泄漏。
所有的代码都运行在自己的应用程序域(Application Domain)。在CLR环境中,应用程序域的作用是在单个进程中宿主不同的应用程序域,用来隔离不同的执行代码 ―― 换句话说,在一个应用程序域中的运行失常的代码不会影响其它应用程序域中运行的代码。 每个进程中都存在至少一个拥有CLR的应用程序域, 当然, 也可以有多个――这是由寄宿进程和运行的托管代码决定的。当一个程序集被CLR加载时,是指被加载(即时编译(JIT))到一个指定的应用程序域中,同时这个程序集也可以被加载到同一个进程中不同的应用程序域中。也可以加载一个中立的程序集――在这种情况下,一个程序集的单独备份可以为一个进程中所有的用户提供服务。
寄宿
宿主了CLR的Yukon数据库引擎有一些特殊需求。数据库引擎对于资源管理是非常谨慎的――比如:通过直观推断数据库使用的资源变化,在不同的缓存之间移动内存。因此,应该在数据库引擎和CLR的资源管理方式保持某种默契关系。
在CLR的1.0和1.1的版本中, CLR提供一个API来加强进程的控制――比如判断CLR线程池的大小。当然,这个API并非强大到可以替代CLR在Yukon中的作用, 比如:考虑以下几种情况:
内存管理
Yukon 数据库引擎完全控制已经被分配的内存空间,在不同的缓存之间切换,比如:存储过程缓存和数据缓存。在这种情况下,Yukon 数据库引擎需要有更加灵活的方法来运行CLR的垃圾收集器,同时对CLR垃圾收集器发生的条件,分配或释放多少内存产生影响。
线程管理
CLR通过线程技术完成异步任务和多元化的并发操作。CLR拥有自己的托管线程池,用来计划执行线程池中的任务。然而, SQL Server使用更精致的纤程机制(mechanism of fibres)来应对可能的巨量并发请求,因此,SQL Server需要接管CLR的线程管理。
并且,由于存在CLR的运行机制和SQL Server的需要之间的微妙关系,我们意识到这需要对CLR的寄宿接口进行扩展。因此随着Whidbey(是微软Visual Studio .NET 的下一代版本)的发布.NET会提供更多和更强大的寄宿API(Yukon发布的.NET版本和Yukon支持的.NET版本将保持一致)
Whidbey 寄宿 API 的特征
在Whidbey中,有7处寄宿API的关键部分得到了扩展:
1. 内存管理
现在CLR允许宿主机(Yukon)取代WINDOWS和C运行库分配常规内存,因此现在宿主机可以控制和判断内存的分配或释放多少。同时,宿主机可以用自己的内存通知机制取代标准的内存通知机制,从而可以触发垃圾收集器。宿主机也可以不进行内存分配,这时CLR会警告宿主机内存分配失败的结果,这样宿主机可以采取相应的策略。
Yukon通过这种策略施加对内存分配的影响,这样来控制缓存的大小和预防潜在地可能造成内存分页的操作。
2. 线程
现在,CLR从线程中抽象出一个新的概念叫任务(Task)。宿主机可以控制任务的分配和存储,包括任务的开始,结束和同步等等。同时CLR和宿主机相互告知显著的事件(比如当任务从可执行到不可执行的状态改变,反之亦然,宿主机也要通知CLR)。宿主机可以提供配置CLR线程池工具。这样的集成级别允许Yukon延续自己的轨迹同时允许CLR运行2个异步任务。
3. I/O实现
寄宿API现在可以为了CLR的利益配置宿主机I/O同步,在I/O操作完成后通知CLR。这样Yukon可以完全了解进程内所有I/O的操作。
4. 同步
如果CLR放弃对任务的控制,就必须运许宿主机提供同步不同任务的手段。因此,宿主机必须提供一个方法来建立:临界区(critical section),互斥体(mutexes),事件(events),读写锁(reader/writer locks)和监控器(monitors)。 宿主机通过这些控制方式,就完全可以检测死锁操作。这样Yukon就可以正确的处理CLR请求的计划任务从而提高整体运行的可靠性,解决同步过程中的死锁。
5. 托管和非托管
宿主机托管代码完成P/Invoke调用,转换为本地机器码时可能会有问题,因为目前还没有在代码运行时检测何种锁被丢弃的方法。新的宿主API中允许任务在脱离CLR控制时通知宿主机。比如:当代码运行在CLR外部时,Yukon会调整任务到non-fibre scheduled 任务。同样这些非托管的代码块对数据库引擎是透明的-例如:数据访问的API,这些和其它的非托管代码是区别对待的。
6. 应用程序域中性代码
应用程序域中性允许加载一个程序集(assembly)时所有的应用程序域共享一个单独的即使编译的程序集镜像代码。在Whidbey以前版本,CLR有3个选项:加载所有程序集的应用程序域中性;不加载程序集的应用程序域中性;只加载强名称程序集的应用程序域中性。
人们已经意识到这并没有为宿主机提供足够好的控制粒度。一个自己拥有一套系统程序集的宿主机也可能需要加载应用程序域中性,而所有用户的程序集,不论是强名称的还是弱名称的,可以被分别加载到每个应用程序域。这样在一个应用程序域关闭时可以卸载用户的程序集而保留系统的程序集。
Yukon在加载所有的用户程序集到数据库的应用程序域,正是通过这个特征加载进程内提供者程序集和其它应用程序域中性的系统程序集,
7. 程序集查解(Assembly Resolution)
对于一个宿主机可以通过挂接应用程序域加载失败来定制程序集查解。这样做,宿主机就能将程序集加载到一个托管的字节数组并且传递这个数组到程序集分解器。这种机制对于Yukon来说还不够灵活,因为大多数的程序集是通过自己定制的方法加载。允许正常程序集查解失败而且还要传递字节数组,这些内存复制的操作需要消耗大量的性能开销。因此,寄宿API必须允许更多和程序集分解器的交互。
目前,寄宿API允许宿主机决定是否需要CLR执行程序集分解或者需要自己确保加载,这样可以将程序集返回非托管缓存来防止需要额外的内存复制。
Yukon不是在文件系统中保存用户的程序集而是在数据库中,因此Yukon不是使用标准的程序集查解而是使用自己机制自动加载用户的程序集。
Yukon使用的C LR版本是完全紧密集成在Whidbey,而不使用机器上最新的版本,例如:尽管Yukon可以执行以前版本的CLR编译的代码,但只要Whidbey兼容该代码就可以了。幸运地是CLR已经努力保持对前面版本的兼容,尽可能的将公司在Yukon下运行的大量托管代码在Whidbey平台下不需要重新编译。
程序集管理
下面我们来关注一下CLR是如何在Yukon中运行的?我们需要了解Yukon如何管理和保存程序集代码。Yukon不依赖标准的程序集控制进程(当一个程序集要加载时,CLR接管定位这个程序集的进程)而是将需要的程序集保存到自己的数据库中。这样可以建立完整的程序集的数据库备份,而不必引用在数据库备份和恢复时可能改变的文件系统程序集。
使用CREATE ASSEMBLY命令可以将程序集加到数据库中,例如:
CREATE ASSEMBLY <assembly identifier>
FROM <path to assembly on file system>
这个命令不仅将该程序集加载到数据库,而且还包括该程序集调用的其它非系统程序集。比如有一个程序集Customer需要调用另外一个程序集Util:
CREATE ASSEMBLY Customer
FROM ‘C:\build\customer\customer.dll’
这个命令将Customer程序集和Util程序集都保存到数据库中。任何Customer和Util调用的系统程序集都不需要加入到数据库,因为Yukon已经知道这些系统程序集。系统程序集列表是不可配置的。对于Yukon来说是区别对待用户程序集和系统程序集,系统程序集是从文件系统加载的标准程序集。
程序集标识符(在这个实例中是:Customer)在数据库中必须是唯一的,Yukon根据这个标识符代替原来的4段程序集名称,原始的程序集名称被存储而且Yukon保证程序集在一个数据库中只允许保存一次。
程序集保存在数据库中,以通过sys.assemblies 系统视图查看(如表3-1),程序集的字节流可以通过sys.assembly_files系统视图查看(如表3-2)
字段名
数据类型
描述
name
sysname
程序集的名称 (在一个模式中是唯一的).
principal_id
int
模式的拥有者编号.
assembly_id
int
程序集的编号(数据库中是唯一的).
permission_set
tinyint
程序集的CAS配置 .
Explicitly_registered
bit
程序集是否显示地加入到数据库或者依赖另外的程序集加入数据库.
create_date
datetime
程序集的加入时间.
version_major
Int
程序集的主版本号.
version_minor
Int
程序集的次版本号.
version_build
Int
程序集组件的编译号和版本号
version_revision
Int
程序集组件的修订版本号.
culture_info
nvarchar(30)
程序集的修饰 (null 表示中性)
public_key
varbinary(8000)
程序集的公钥 (null 表示弱名称)
表 3-1 -sys.assemblies 系统视图
字段名
数据类型
描述
assembly_id
Int
程序集文件的编号 (数据库中是唯一的)
name
nvarchar(260)
文件名
file_id
Int
文件的编号 (在一个程序集中是唯一的)
content
Image
文件的字节流
表 3-2 - sys.assembly_files系统视图
有一个问题需要指出,存储一个程序集到数据库需要访问文件系统,因此程序集只能通过Windows的用户帐号来加载(不可以用纯SQL Server用户帐号)或sa用户.这是因为Windows(控制文件系统的访问)无法识别SQL Server的登陆用户.使用sa帐户是因为Windows默认将该帐户对应为系统管理员帐户.
在删除一个程序集时有许多选项,你只能删除指定的程序集,包括这个程序集及所关联的文件或这个程序集和所有的附属程序集。
代码访问安全(CAS)
当一个程序集加载到数据库后,程序集代码是否可以做任意想做的事情?数据库引擎整合CLR其中有一个主要目的就是减少对扩展存储过程的依赖和风险。因此理想情况下我喜欢尽量限制Yukon内在程序集认为安全的操作-这样至少对于数据库的稳定性是有益的。
CLR在传统的安全机制上还有一个基于用户权限的安全层,首先执行代码会检查这段代码可以作什么?这一层称作代码安全访问CAS(Code Access Security),CAS使用于基于原代码的许可而不仅仅是谁可以执行它。过去CAS习惯用于允许从不同的位置加载有不同的执行许可代码。可是,在Yukon中所有CLR基本代码都从数据库加载,这是因为,在原始程序集代码中有不同的CAS定义。Yukon定义了3个“桶”(Bucket)来加载不同的程序集,每一个“桶”有不同的一组权限,代码可以做什么?根据程序集代码被加载到哪个“桶”来决定。
这3个CAS“桶”是:安全(SAFE),外部访问(EXTERNAL_ACCESS)和不安全(UNSAFE),有关“桶”的详细安全信息,请看表3-3
CAS 安全性
操作许可
SAFE
允许访问数据和使用CLR类。但不可以访问外部资源(比如系统文件和网络),多线程和单线程同步,非只读静态, 不安全代码和内部操作是禁止的。这个桶的代码必须是CLR验证类型安全的。
EXTERNAL_ACCESS
和内置SAFE桶的操作许可一样,但增加了可以访问外部资源比如文件系统,网络和事件日志,只要可以通过CLR的类库访问。使用内部操作来获得外部资源是禁止的。在这个单元中的代码必须是CLR验证安全的。
UNSAFE
在CAS子系统没有限制执行代码,在这个桶的代码是不需要类型安全。
表 3-3 - CAS桶的安全定义
在UNSAFE桶内的代码和扩展存储过程一样有安全问题。换句话说,直接内存操作和锁争用(包括死锁),这些都可能导致进程不稳定。
在Yukon中,默认所有的程序集都是加载到SAFE“桶”,但这也可以通过CREATE ASSEMBLY命令加WITH PERISSION_SET子句来更改,比如:
CREATE ASSEMBLY Utilities
FROM ‘C:\assemblies\utilities.dll’
WITH PERMISSION_SET=EXTERNAL_ACCESS
将程序集加入到数据库可以通过权限来控制,这些权限根据程序集被加载到到哪一个CAS“桶”。表3-4列出了将程序集加入到不同CAS“桶”的权限信息:
CAS 安全性
权限
注释
SAFE
CREATE ASSEMBLY; REFERENCES
这个权限分配给服务器角色dbowner
EXTERNAL_ACCESS
CREATE ASSEMBLY; REFERENCES; EXTERNAL ACCESS
外部访问的权限必须赋予master数据库
UNSAFE
CONTROL SERVER
控制服务器分配给服务器角色 sysadmin
表3-4 - 加入程序集到不同CAS桶的权限表
静态成员和应用程序域
应用程序域的概念被映射到这个环境中,决定了在寄宿CLR的Yukon代码中 CLR类型的静态成员拥有一些特殊的功能让我们快速回顾一下什么是应用程序域?它在CLR中是如何使用的?所有的代码都在CLR控制下执行,也在一个应用程序域中。应用程序域相当于一个进程,让代码在一个隔离单元中运行。不同应用程序域中的代码必须进行大量的信息交换(它们都调用 .NET架构类库). 如果一个应用程序域中的代码发生严重问题(比如一个没有处理的异常), 只有该应用程序域受影响, 其它域不会受影响继续工作.
在这种隔离机制下:
1. 每个程序集将被加载到单独的应用程序域, 除非它们被显示地加载到应用程序域中性-这意味着每一个应用程序域拥有一段独立的即时编译执行的代码拷贝。
2. 一个类型的静态成员对每个应用程序域都是专有的. 除非该程序集被加载到应用程序域中性.
在目前Yukon的测试版本中, 我们决定让每个数据库拥有一个单独的应用程序域. 这意味着在同一个数据库里如果有两段代码, 即使它们在不同事务中运行, CLR也不能把它们隔离开来. 换句话说, 即便第一个事务已经被提交, 在其中运行的代码仍然能够改变另一个事务的静止状态. 这打破了关于事务的一条黄金法则 – 隔离性
因此, 在SAFE或EXTERNAL_ACCESS CAS“桶”里运行的代码不能被设置为非只读属性, 在UNSAFE桶里运行的代码则没有该限制.
那么, 问题解决了吗? 不幸的是, 这样只解决了问题的一部分. 撇开UNSAFE“桶”不谈(它原本就可以执行一些潜在的危险操作), 允许只读静态字段有什么问题呢? 问题在于在CLR类型的系统中, 只读属性使得值类型和引用类型不能并存. 对于值类型来说, 字段就是数据本身(当字段被声明时内存即被分配). 这意味着一个只读的值类型一旦被建立就会保持不变. 然而, 一个引用类型字段只是简单的对分配给垃圾收集堆的内存块的引用. 只读的引用类型字段只是保证该引用保持不变, 它所指向的对象的所有状态不会受到任何影响. 因此, 在一个事务中运行的代码仍然可以看到另一个事务的状态, 即使它加载在SAFE“桶”。
因此, 任何引用类型字段一旦被构建后必须静态和只读的, 也就是说这种状态是不能被改变的. 这是我们保证一个事务中的改变真实地隔离于另一个并发事务的唯一方法。
因此我们可以将程序集加入到数据库,只是整个故事的一半。我们常说的这是一个公共类的公用方法,但一个程序集中的代码不会自动可以被其它的程序集访问。我们必须公开调用一个程序集的方法和类型,这样,这个程序集才可以被其它数据库代码和客户端程序引用。
托管存储过程
存储过程是许多数据库应用程序的面包和黄油,这是因为存储过程天生有这个优势。既然我们可以在数据库引擎中整合CLR,我们就可以通过托管存储过程的形式访问这些CLR对象。可以被托管存储过程加载的对象必须满足一些必要的条件:
· 所包含的类必须是公共的
· 方法必须是公用的
· 方法必须是静态的
我们来看看实例3-1: 中的代码 Foo.Method1方法是不能访问的,因为Foo类不是公共类。Bar.Method2也不能访问,尽管Bar类是公共类但是它的Method2方法不是公用的。Baz.Method3也不能访问,尽管这个类和方法都是公有的,但它的方法不是静态的-因此数据库引擎不知道如何建立一个Baz的实例来调用Method3方法。因此只有公用方法才可以被托管存储过程访问(或暴露给CLR的其它构造函数),在Quux 类中,方法和类都是公用的。
class Foo
{
public static void Method1()
{
}
}
public class Bar
{
static void Method2()
{
}
}
public class Baz
{
public void Method3()
{
}
}
public class Quux
{
public static void Method4()
{
}
}
实例 3-1 – CLR函数的访问
当一个程序集被加载到数据库,这些公共类的公用方法并不是简单的变成托管存储过程;每个方法的入口必须根据需求显示的暴露出来。具体的语法如下:
CREATE PROCEDURE <Procedure Name>
AS EXTERNAL NAME <Assembly Identifier>:<Type Name>::<Method Name>
存储过程名<Procedure Name>在数据库中必须是唯一的,程序集编号<Assembly Identifier>匹配已经被加载到数据库的程序集,类型名<Type Name> 是在方法中定义的类型名称,方法名<Method Name> 是程序集中的方法名称。注意:存储过程名<Procedure Name>和方法名<Method Name>是不需要匹配的。
因此,根据实例 3-1的Quux.Method4 类,我们建立一个托管存储过程,名称为:MyMethod4,假设这个代码包含在一个程序集已经通过标识符Utils加载到数据库:
CREATE PROCEDURE MyMethod4
AS EXTERNAL NAME Utils:Quux::Method4
参数传递
在实际应用中很少有存储过程没有参数,在正常情况下,需要操作的数据会传递参数给存储过程-至少是包括这些数据的关键信息。因此我们需要有一个给托管存储过程传递参数的方法。
大家看看实例 3-2的C#的代码,这段代码中我们产生一个类有3个方法,每个方法传递参数的方式略有不同。
· Method1 通过值x传递,在这个方法中参数值的改变对于调用者是不透明的。
· Method2 通过输出参数x传递,在C#编译器中,x的值只有通过输出参数的方法才可以获得,x的新值对于调用者是透明的。
· Method2 通过引用型参数x传递,这个引用型参数x的值对于方法是可用的同时参数值的改变对于调用者是透明的。
public class Params
{
public static void Method1( int x )
{
}
public static void Method2( out int x )
{
x = 42;
}
public static void Method3( ref int x )
{
x = x + 2;
}
}
实例 3-2 - C# 传递参数的方法
下面我们演示如何在一个托管存储过程中调用一个方法(假设这些代码已经被加载为标识符为parameters的程序集):
Method1 是直接调用,代码如下:
CREATE PROCEDURE Method1
@x int
AS EXTERNAL NAME parameters:Params::Method1
我们可以这样调用存储过程Method1:
EXEC Method1 5
Method2要稍微复杂一些,但是TSQL有个OUTPUT输出参数的语法,因此这个也还算简单。
CREATE PROCEDURE Method2
@x int OUTPUT
AS EXTERNAL NAME parameters:Params::Method2
我们可以按下面的方式调用存储过程Method2:
DECLARE @x int
SET @x = 0
EXEC Method2 @x OUTPUT
SELECT @x
这个SELECT的结果为42。
最后,Method3比初看的要复杂一些,因为虽然.NET有传递参数的概念就像in/out,但TSQL没有区分output 和in/out 参数.OUTPUT子句简单地声明这个值在返回时可能会改变,它没有对方法中的值可以有或可能没有进行判断。因此Method3的语法和Method2的是一样的。但是C#编译器语法分析会预防这些代码作相同的事情。
CREATE PROCEDURE Method3
@x int OUTPUT
AS EXTERNAL NAME parameters:Params::Method3
这样我们就可以如下调用存储过程Method3:
DECLARE @x int
SET @x = 3
EXEC Method3 @x OUTPUT
SELECT @x
这段代码的返回值是5。
还需要注意一点-因为Method2 和 Method3,我们应该在托管存储过程调用这个程序集之前设定变量@x为某一个确定的值,这样保证传递的参数值是非空的。如果我们遗漏这一行,会导致一个异常NullReferenceException。这个也说明还有其它因素隐藏在转换中-SQL Server的数据类型和.NET的数据类型是不一样的,当我们的代码从一个环境迁移到另外一个,会发生类型转换。我们这个实例表明SQL Server中一些可以为空的类型而在.NET对应的类型是不能为空。
返回值
托管存储过程可以返回.NET函数的返回值。然而,当声明一个托管存储过程时,你不需要指定返回的类型(这个和自定义函数不同,自定义函数是需要声明返回值的类型)。因此,这需要在.NET的方法中声明
public class RetVals
{
public static int GetUltimateAnswer()
{
return 42;
}
}
实例 3-3 – 拥有返回值的方法
我们在数据库中可以这样调用(假设包含这段代码的程序集已经被加载到数据库,标识符为ReturnValues)
CREATE PROCEDURE GetUltimateAnswer
AS EXTERNAL NAME ReturnValues:RetVals::GetUltimateAnswer
我们就可以这样获得.NET函数的返回值:
DECLARE @x int
EXEC @x = GetUltimateAnswer
SELECT @x
这个案例中的返回值是42。
用户自定义函数(UDF)
自定义函数,非常简单,和存储过程非常相似。然而,在定义,声明和使用自定义函数时,还有一些额外要素我们必须重视。
下面是用户自定义函数调用一个.NET函数的语法:
CREATE FUNCTION <Function Name>
(
<Parameter List>
)
RETURNS <Return Type>
AS EXTERNAL NAME <Assembly Identifier>:<Type Name>::<Method Name>
函数名<Function Name>在数据库中必须是唯一的, 参数列表<Parameter List> 是参数名和参数类型列表,返回类型<Return Type> 是函数的返回值的类型。程序集编号<Assembly Identifier> 是已加载的程序集的标识符,类型名<Type Name> 是CLR的声明方法时的数据类型名称, 方法名<Method Name> 是CLR方法的名称。此外,函数名<Function Name> 和 方法名<Method Name>不需要匹配。
因此,可以参照下面的语法声明一个.NET的方法:
public class Calc
{
public static int Add( int x, int y )
{
return x + y;
}
}
我们可以使用下面的代码在托管自定义函数调用上面.NET代码声明的方法(假设这个程序集已经被加载到数据库并且标识符为:Calculator)
CREATE Function MyAdd
(
@x int,
@y int
)
RETURNS int
AS EXTERNAL NAME Calculator:Calc::[Add]
注释:要注意退出调用Add的方法,因为在TSQL中已经有一个叫Add的函数。
这个函数可以这样调用:
SELECT dbo.MyAdd(5, 10)
这个SELECT 的返回值为15。
注意在函数中你不能使用OUTPUT参数。
到目前为止,我们看了最简单的用户自定义函数,但是这里都隐藏了用户自定义函数的复杂性,用户自定义函数有许多概念存储过程是不涉及的。
· 数据访问
这个概念是指函数是否访问进程内托管提供者。如果函数没有访问进程内托管提供者,优化器就不会为进程内托管提供者初始化。假如存储过程请求数据访问CLR架构会自动初始化进程内托管提供者。
· 系统数据访问
如果函数访问的是用户数据而不是系统数据,优化器可以在构造WAITFOR查询时生成。在这个实例中,执行函数可以被一个通知事件恢复而不需要向系统数据一样需要加入额外资源。
· 精度
如果一个函数是精确返回的,它不会使用浮点指针算法并且不会有舍入错误。
· 确定性
一个确定性的函数返回值和输入参数是一致的-不考虑变量因素,比如当前日前和时间。比如一个函数返回的是2个数字的相加是确定性,一个函数返回当前的时间是不确定性。
· 外部访问
函数是否访问数据库的外部资源,比如系统的文件系统或注册表。
因为优化器这个概念是非常重要的,而且在索引中能够使用函数也是基于这些概念。可以用于索引的函数必须是:
1. 确定性-在不同的时间点,一个特定行的索引应该是常量,这是必须的-否则索引页的物理构造是无效的。
2. 精确性-如果一个函数的返回值存在舍入错误,这个函数不可能是确定性的。
3. 无数据访问-函数的输入参数不是唯一决定输出的,因此这不确定函数。
4. 无外部访问-同数据访问一样,其它因素也会影响函数的确定性。
对于TSQL函数,数据库引擎可以根据调用内部函数确定这些因素(比如:NEWID()是一个非确定性的函数同时会使调用它的函数也是非确定性)
可是,在CLR类库中内部函数量是非常巨大的,因此,要求数据库引擎或一些“超级验证器”来决定函数是否是确定性的是非常困难的。所以,对于CLR函数,可以通过System.Data.Sql.SqlFunctionAttribute来指定函数的属性。该属性可以根据下表(表 3-5)进行配置。
名称
类型
默认值
注释
IsDeterministic
Boolean
False
标记函数是确定性还是非确定性
IsPrecise
Boolean
False
标记函数是精确还是非精确
DataAccess
DataAccessKind
None
指定函数是否访问进程内托管提供者,默认值是不访问,如果是READ表示访问
SystemDataAccess
SystemDataAccessKind
None
指定函数是否访问系统数据,默认值是不访问,如果是READ表示访问
表 3-5 - SqlFunctionAttribute 属性列表
数据库引擎可以确定函数是否访问了进程内托管提供者,而且验证是否真实的匹配在属性中声明的值。可是,至于其它属性,数据库让你来配置,因此你可能会写一个将自己声明为确定性的函数,但这在实际时是不行的。将IsDeterministic的属性设定为真(true),那么,你可以绝对确信函数是确定函数,否则该函数可能会使索引页构成无效。
实例 3-4 和 实例 3-5 分别用C#和VB.NET展示了使用SqlFunction函数的属性来标记该函数为确定性的。
[SqlFunction(IsDeterministic = true)]
public static int Add( int x, int y )
{
return x + y;
}
实例 3-4 – 用C#声明一个函数为确定性
<SqlFunction(IsDeterministic:=True)> _
Public Shared Function Add(ByVal x As Integer, ByVal y As Integer) As Integer
Return x + y
End Function
实例 3-4 – 用VB.NET声明一个函数为确定性
你可能注意到运用SqlFunction函数的属性是可选的,如有这个属性遗漏,可以使用默认值。省略是被允许的,所以可以不需要修改的代码就允许重用大量已经存在的CLR代码。
结论
在这篇文章中我们讲解了支持内嵌托管代码的数据库应用程序的基本架构,而且介绍了二种托管代码的表述方法――托管存储过程和托管函数。
通过CLR与SQL Server引擎的整合,使Yukon数据库有更加强大的功能,我们可以通过面向对象的语言C#, VB.NET, Visual C++, Visual J#等等与.NET兼容的语言开发存储过程(我们甚至可以用PHP和汇编来写),这样我们可以通过面向对象的方法来开发数据库应用程序,这比以前T-SQL面向过程的语言有更大的优势,并且开发环境也转变为在Microsoft Visual Studio .NET 下开发,这提供了更加方便,快捷和高效地解决复杂的商业案例的手段。
我们同时要注意,在数据库引擎和CLR的关系中,数据库引擎占主导地位,控制内存的分配和响应客户端的请求。CLR为Yukon提供了访问系统和网络资源的组件,使我们可以方便的调用.NET Framework提供的大量类库(FCL),同时,CLR组件也可以通过进程内托管提供者访问数据库。
在安全方面,Yukon既有基于用户的SQL Server的安全模式,也有CLR基于权限的安全模式。CAS给我们提供了非常好的粒度来控制CLR代码的安全,也给DBA更多的手段来控制CLR对“外部”的访问。
Yukon要到2005年才会上市,我们这里介绍的只是众多新特性的一部分,在微软的MSDN中网站已经有很多文章进行了介绍,大家可以发现Yukon还提供了许多非常实用的新功能。