摘要:探索在数据仓库框架 (Data
Warehousing Framework) 中使用 DTS
以捕获和提供数据,从而实现业务智能解决方案的最佳实践。
目录
简介
业务智能 (BI) 解决方案、数据集市
(Data Marts) 和数据仓库 (Data Warehouses)
都非常依赖于用于在异类数据源之间迁移数据以及支持进行综合分析决策的工具。Microsoft®
SQL Server™ 2000 数据转换服务 (DTS)
提供的灵活性和高级功能可以自动创建体系结构,通过该体系结构可以捕获操作数据并将它们传送给
BI 应用的最终用户。对于利用 Microsoft
数据仓库框架的解决方案,DTS
平台所提供的灵活性为解决其数据仓库需求提供了多种方法。
本白皮书介绍了若干最佳实践,在通过
Microsoft 数据仓库框架提供复杂的 BI
解决方案时可以利用这些实践。本白皮书包括六部分:
数据仓库框架概述、包设计实践、ETL(提取、转换和装载)实践、分析服务管理实践、审核和错误处理实践以及增强
DTS 功能实践。每个最佳实践部分都介绍了使用该实践的首选方法、指导原则和优点。
本白皮书首先探讨了数据仓库框架以及其中的
DTS 角色。然后详细介绍了实现 DTS
包设计的最佳实践,并阐明了一些概念(例如,元数据驱动配置和模块化包开发)。接下来又详细介绍了以下最佳实践:源数据的提取和分级处理、转换和清理数据以便为生产数据仓库做好准备,以及管理变化缓慢的维度和事实区域的装载。然后介绍了对
SQL Server 2000
分析服务进行集成管理的最佳实践,其中包括管理
OLAP 分区以及其他 OLAP
多维数据集和维度的创建和处理的方法。在后面的小节中,介绍了通过错误处理和审核来监视
DTS
包解决方案体系结构的总体执行情况的最佳实践,以及通过自定义编程增强此体系结构的最佳实践。
本白皮书主要适用于熟悉 SQL Server 2000
数据转换服务平台的技术人员。有关数据转换服务的功能概述,请参阅
Data
Transformation Services in SQL Server 2000(英文)白皮书或查阅
Microsoft SQL Server 联机图书。
DTS
和数据仓库框架
数据仓库框架概述
Microsoft
开发的数据仓库框架是一个可伸缩的开放式体系结构,可以加快和简化现今业务智能应用程序的生成、管理和使用并降低这些活动的成本。Microsoft
数据仓库框架具有功能完善的高性能集成数据仓库平台(SQL
Server 和 Microsoft Office)的全部优点,同时还为寻求可伸缩性和专门应用程序的信息技术
(IT) 专业人士提供了最广泛的选择和灵活性。
图 1:Microsoft
数据仓库框架
上图显示了数据仓库框架的核心特性。有关数据仓库框架的详细信息,请访问
Microsoft
业务智能和数据仓库合作伙伴网站,其网址为:http://www.microsoft.com/partner/horizontalsolutions/bi/(英文)。
数据仓库框架中的 DTS 角色
数据转换服务是数据仓库框架的一部分,可以与数据仓库框架的所有元素进行交互。作为用于从
OLEDB 和 ODBC
兼容数据源中完全提取异类数据(通过转换操作数据并将其装载到可分析的多维数据存储中)的服务提供程序,DTS
在成功实现这些解决方案的过程中扮演了重要角色。使
DTS
在这一框架中取得成功的关键因素是采用了一个合理的方法,生成
DTS
包体系结构以获得最大的灵活性。这是本白皮书要介绍的第一个最佳实践。
图 2:数据仓库框架中的
DTS 角色
包设计实践
要为 SQL Server 2000
业务智能解决方案提供一个有价值的、可持续的、特别是灵活的
DTS
技术体系结构,需要在设计方面考虑如何实现这些目标。本节将介绍设计
BI 解决方案中的 DTS 包的核心最佳实践的概念。
元数据驱动方法
使用动态属性任务驱动包
Dynamic Properties(动态属性)任务(SQL
Server 2000 中的新增功能)是利用存储在 DTS
包之外的包配置信息(或元数据)的最有效方法之一。此信息在运行时被读取,其执行是由此任务动态自定义的。任务允许从各种源(例如
INI 文件、关系 DBMS
查询和文本数据文件)获取属性信息。通过将包信息(更确切地说是任务信息)存储在外部信息源中,使得包能够在开发环境和生产环境之间很好地迁移,同时又将对包配置的更改隔离出来,放到文本文件或
DBMS
表中。这也为利用元数据的模块化包设计奠定了基础。
通过 Execute SQL
任务来填充全局变量
另一种获得包元数据信息的方法是使用
Execute SQL 任务并输出 rowset
参数,以便在单个查询中捕获全局变量的所有设置,从而代替了使用
Dynamic Properties 任务在多个查询中进行捕获。
使用 Execute SQL
任务要求完成少量的 ActiveX
脚本,将返回的多行全局变量记录作为一个行集 (rowset)
转换为实际的全局变量。下面的示例显示了将存储在全局变量“GlobalVariablesRowset”中的三个列行集(GlobalName、GlobalValue
和 GlobalConversion)转换为包的全局变量集合时所需的脚本。
Dim GlobalRowset, GlobalName, GlobalConversion
Set GlobalRowset = DTSGlobalVariables("GlobalVariablesRowset").Value
' 检查 GlobalRowset 中的 GlobalVariable 记录
If GlobalRowset.RecordCount <= 0 Then
' 退出函数并报告失败
Main = DTSTaskExecResult_Failure
Exit Function
Else
GlobalRowset.MoveFirst()
Do While Not GlobalRowset.EOF
GloballName = GlobalRowset("GlobalName").value
GlobalConversion = GlobalRowset("GlobalConversion").value
' 动态生成 VBScript,用于
' 为全局变量赋值
' 注意包含“CStr”、“CBool”和“CInt”
' 的 Conversion 列的使用
EXECUTE( "DTSGlobalVariables(cstr(GlobalName)).Value =" & _
GlobalConversion & "(GlobalRowset(""GlobalValue"").value)")
GlobalRowset.MoveNext()
Loop
End If
Set GlobalRowset = Nothing
Main = DTSTaskExecResult_Success
执行此脚本时,将使用相应的值和数据类型创建或更新全局变量。
设计元数据驱动包
DTS
提供了可以快速方便地生成元数据驱动包的工具集,使得这一工作的基本原理非常清晰易懂。在设计过程开始时,符合逻辑的做法是先了解一下要开发的包的类别。在这些类别中,应当找出当包处于生产环境中时易于发生变化的变量参数。具有面向对象开发背景的开发人员可以将设计由元数据驱动的包视为设计对象(包),这些对象提供了由对象的方法(任务)使用的属性(全局变量)。
下面是一个包设计的简化图示,表明了使用
Execute SQL 任务来检索全局变量行集、使用 ActiveX
Script 任务将行集转换为全局变量以及使用 Dynamic
Properties
任务将全局变量的值赋给包中的其他任务属性(图中未显示)。
图 3:简化的包设计
在包设计中包含元数据的指导原则
DTS
提供的灵活性允许使用不同但兼容的方法来创建面向元数据的包设计。无论使用哪种方法,设计使用元数据时都应考虑以下几个关键因素:
- 与 BI
解决方案中的其他体系结构设计一样,制定一个使用元数据的策略,并体现出采用这一策略的优点。优点可以有很多,例如易于维护、支持包的远程配置等等。
- 对于基于文本文件的元数据(例如
INI 文件或 XML 文件),请确保使用全限定的 UNC(例如
\\MyServer\MyShare\MyINIFile.INI)来引用这些文件,以确保在网络环境中获得最大的可移植性。
- 考虑为全局变量信息使用 SQL Server
关系表,因为这种方法可以确保源数据库或目标数据库的整体连接性,并防止出现诸如资源锁定(使用基于文件的资源时可能会出现)等问题。
- 由元数据驱动易于发生变化的设置,例如服务器名称、OLAP
多维数据集和源文件装载路径。
- 不要试图由元数据驱动所有设置。如果过度使用元数据,那么不仅元数据将难以管理,通常包本身也将难以管理。
进行合理的判断,采用前面讨论的方法并考虑这些指导原则,这将有利于生成和构造实用且灵活的元数据驱动的包。
父/子包方法
全局变量
全局变量通常是所有包体系结构中的重要元素。全局变量提供了在任务之间进行通信的方法,并且如果作为参数对于任务的执行所起到的作用越多,包的整个设计也将变得越复杂。但是,全局变量并不仅仅用于单个包中任务之间的通信。实际上,全局变量对于使用一种称为父/子包(或分层结构包)设计的设计方法是非常重要的。
在父/子包方法中,包中的工作流将通过子包的工作流(或从属包的执行)来继续。常见的例子是父
ETL 装载包执行子 OLAP
处理包。本白皮书后面会对该示例进行进一步介绍。
Execute Package 任务
Execute Package
任务为实现父/子包设计提供了现成的功能。父包可以包含一个
Execute Package
任务,然后标识存储位置和要传递给子包的全局变量。当任务执行时,将随之执行子包,这样该任务便可以让子包联接其事务。
此任务可以通过一种“生成块”功能在整个设计中快速包含子包。
通过对象模型执行包
开发人员是在 SQL Server 7.0
中第一次引入 DTS 时开始使用父/子包设计的。该功能是通过以下方法实现的,即使用
COM 自动化和 ActiveX
脚本将全局变量传递给子包并调用子包。这种方法有利有弊。使用
COM 自动化,包无法联接父包的事务。但是,使用 COM
自动化的优点在于不要求标识所执行包的版本 GUID,而
Execute Package 任务则要求标识。
设计父/子包层次结构
为了更清楚地说明实现父/子包层次结构的上述两种设计方法,下面给出了这两种技术的比较图示。
图 4:Execute Package Task
Properties(执行包任务属性)窗口
图 5:ActiveX Script Task
Properties(ActiveX 脚本任务属性)窗口
幸运的是,除了上述差别外,使用 Execute
Package 任务或 ActiveX
脚本执行包时都会产生相同的结果,即一个包被另一个包执行。设计和开发父/子包层次结构通常是出于以下目的:
- 减少单个包中工作流的复杂性(即,使包在设计环境中易于阅读)
- 使包的设计模块化(即,将包的 ETL
处理划分为维度提取、维度装载、事实提取、事实装载、OLAP
处理以及其他方面)以便于进行有效的维护
- 管理彼此之间具有相关关系的多个包的执行
- 希望在包之间共享变量
- 封装由元数据驱动的特定功能,以便可以重复使用子包
对于 BI 解决方案中普通或复杂的 ETL
设计,一般都会要求实现上述多个设计目标。
父/子包指导原则
虽然用于创建父/子包体系结构的工具使得实现工作很简单,但在进行这一实践时还要考虑其他一些指导原则,
特别是以下原则:
- 管理包之间的嵌套级别,最多不要超过两个子级。
- 理解包和任务的两个 Failure
属性,它们分别是“Fail Package on First Error”和“Fail
Package on Step Failure”。
- 建立可重复使用的包的并发使用情况估计;如果此值较高,则将子包存储在
SQL Server
中。这样可以避免子包上出现独占文件锁定而破坏并发性(当包作为结构化储存文件存储时可能会出现锁定)。
- 使用表明包的核心功能的命名约定(即,维度装载包以“DIM”开头,事实装载包以“FACT”开头,等等)。
实践摘要
优点
将元数据引入 DTS
体系结构并依赖于多个父/子包为 BI
解决方案奠定了坚实的基础。其中最显著的优点包括:
- 可重复的部署
使用元数据来配置包使得部署工作很简单,只需更新元数据即可。如果能够保留从
Execute Package 任务中调用的子包的版本(或
GUID),这种方法会更有效。
- 易于实现并行
在整个设计的初期采用分层结构思想使开发人员可以选择很高程度的并行性,以便通过多个线程调用同一个包。
- 可维护性(支持快速更改)
通过为特定目的设计一个子包,可以“升级”企业
DTS
体系结构的某个元素而无需涉及多个包。其结果是为包的维护提供了一个更快速的方法。
注意事项
此实践及其相关方法的注意事项虽然不多,但还是有一些。主要的注意事项包括:
初始设计需要较长时间
由于对元数据的依赖性以及需要确定如何使用元数据,因而使用上述方法在设计包的初始阶段会需要较长时间。但是,磨刀不误砍柴功。为快速建立原型或一次性装载数据,使用父/子包或元数据可能比使用
DTS 进程更有效。
开发子包所用的方法可能会创建一个非常易于管理或非常难以管理的体系结构,这取决于设计中采用的思想以及是否考虑了前面提到的设计目标。按照这里提供的指导原则和技术来操作,应当有助于成功实现父子包(建立在元数据驱动的设计基础上)。除了这些“最佳实践”设计活动外,以下各节还将介绍用于使用
DTS 实现整个 BI 解决方案体系结构的解决方案集。
提取、转换和装载实践
从定义上看,多数 BI
解决方案的好坏仅取决于用来提取、转换数据以及将数据装载到关系数据存储中的方法。对于多数
BI 解决方案来说,这些 ETL
进程是其成功与否的关键。作为一种工具,DTS
提供了以下功能,即提取异类数据、自动转换数据以及支持将数据从
OLTP
架构装载到采用维度模型的数据存储的端到端进程。出于这些原因,本节将介绍
ETL 的最佳实践。
源提取和分级装载方法
- 通用数据链接 (UDL) 连接文件
UDL 文件是位于 DTS
之外的对象。顾名思义,该文件 (*.UDL)
提供了对任何安装了 OLEDB
提供程序的系统上数据的访问。UDL
文件维护了文件中的所有连接信息。DTS 为使用
UDL 文件作为包连接的数据源提供了很好的支持。
- 文本文件目标
DTS
支持文本文件目标,这为要从源系统提取到文件中的数据提供了一个方便的标准格式。将源系统的提取结果导出到文件中,使得
ETL 进程的其余部分更加易于管理。
- Bulk Insert 任务
Bulk Insert
任务是将数据从文本文件直接装载到关联的 SQL
Server 分级表中的一种有效方法。使用 Bulk Insert
任务有很多显著优点,优点之一就是装载速度很快。
通过 UDL 管理可移植性
前面已简要介绍过,配置元数据提供了许多重要优点,其中包括平台可移植性。许多从事
BI 解决方案开发的组织都提供了一个开发/测试平台和一个生产平台,以便在项目进行过程中使用。有时,甚至需要使用更多物理服务器来支持解决方案的开发。除了基于服务器的计算机外,开发人员通常还要使用自己的桌面计算机进行开发。其最终结果是,同一版本的
DTS
包可能需要在各种计算机上运行,并且要使其连接信息能够指向相应的目标。
图 6:Connection Properties(连接属性)窗口和
Datalink Properties(数据链接属性)窗口(单击以查看大图像)
UDL
文件解决了连接信息在解决方案的服务器资源之间的可移植性。下面的图示显示了从
DTS 中配置和引用的 UDL 文件。
选择“Always read properties from UDL file”将确保基于
UDL 的连接在每次执行时都引用 UDL
文件中的连接信息。对于基于元数据的连接信息,此功能使
UDL 文件成为“丢失的链接”。
通过文本文件导出获得独立性
执行源提取的最佳方法是将数据从异类数据源中提取到一个通用的文本文件格式中。通过结合使用
OLEDB 或 ODBC
兼容数据源和一个分隔的目标文件(目标文本文件),可以执行
ETL
进程而无需立即执行后续的转换和装载进程。在设计中使用此技术的某些常见原因包括:
- 源系统与 BI
解决方案通常驻留在不同的服务器上
- 源系统与数据集市之间的生产安排的可用窗口不同
- 源系统分布在多个不同的地理位置
使用文本文件的优点包括:
- 能够快速从源系统中导出数据
- 大容量插入到分级表中时,能够立即确定数据问题
- 能够提供数据的脱机副本,供 BI
开发人员用于故障排除、数据分析和其他功能
- 源系统的非依赖性 = 真正的独立
通过将数据放在文本文件中,源系统可以在关系数据库平台之间进行迁移,或者更实际地,可以仅更改架构,而
BI
解决方案却不会(也无需)知道任何这些事件。通过确保文本文件的布局保持一致,所要完成的工作将只是要求更新的源系统和提取包将数据转换为此格式。ETL
体系结构的其余部分将保持不变。使用文本文件的模型副本,可以在完成提取之前建立数据源的原型,从而有可能获得更有效的开发生命周期。
DTS ETL 进程中的分级处理
将在提取进程中捕获的数据装载到关系表中的进程可以提供一个单独的环境,即分级环境。数据将在此环境中准备就绪,以便用于生产数据集市/数据仓库
BI
解决方案。分级环境中驻留的内容应当是在结构上与从源系统中提取的文本文件类似的非索引关系表。这种类似应当包括数据类型(从源系统中提取的列的预期数据类型)。
要将文本文件数据装载到分级表中,应当通过
DTS 中的 Bulk Insert 任务来执行。使用 Bulk
Insert
任务能够有效地装载表,同时还会对与分级环境中的列不一致的数据给出异常。通过在装载分级环境的过程中给出异常,可以从源系统开始进行故障排除。如果不给出异常,则可能会获得部分成功装载的产品设置,从而造成混乱。
提取和分级装载的指导原则
虽然用于提取数据和执行分级装载的方法技术并不十分复杂,但遵循以下指导原则仍然可以减轻此部分解决方案的总体开发工作量。这些原则包括:
- 通过 UDL
文件实现数据源的可移植性。首先使用一个桌面数据库(例如
Microsoft Access)或一个 SQL Server 实例,
然后移动到另一个 SQL Server。如果通过仅更改 UDL
文件仍然可以进行提取,
则表明该设计是成功的。
- 在提取或装载分级环境时避免尝试转换数据。因为这会给源系统带来不必要的负担,同时还会延长提取时间以及在源系统发生变化时破坏逻辑的有效性。
- 使用分级环境作为 BI
沙箱。任何操作都可以在装载之前先进入此环境,特别是非日志密集型操作,例如
TRUNCATE TABLE。
按照此处介绍的概念管理总体设计将为
BI
解决方案提供一个灵活的方法来提取数据以及对数据进行分级处理。当数据进入分级环境中后,即可执行数据转换和清理。
数据转换和清理方法
计算列和校验值函数
SQL Server 2000
中引入的一个重要功能是支持计算列。使用计算列能够创建一个由计算过程定义的列,该列可以随计算过程的其他元素的更改而更新。在
SQL Server BI 解决方案中,其特别意义在于实现了 CHECKSUM()
或 BINARY_CHECKSUM()
函数与计算列功能的结合。
使用 CHECKSUM() 和 BINARY_CHECKSUM()
函数能够将多个变量、表的列或表的整个行作为参数进行传递。这两个函数的结果将是所传递的整个参数集的校验值。BINARY_CHECKSUM()
函数与 CHECKSUM()
函数只有一点微小差别,即前者将值转换为二进制表示形式。从本质上讲,二进制表示形式将使结果区分大小写,从而对于具有不同大小写的同一字符串,会生成不同的校验值。为充分了解校验值函数的作用,下面给出了一个示例:
CREATE TABLE [Person]
(
[PersonID] [varchar](20) NOT NULL ,
[LastName] [varchar] (30) NOT NULL ,
[FirstName] [varchar] (30) NOT NULL ,
[MiddleName] [varchar] (30) NULL ,
[PersonChecksum] AS
(checksum([PersonID],[LastName],[FirstName],[MiddleName])) ,
[PersonBinaryChecksum] AS
(binary_checksum([PersonID],[LastName],[FirstName],[MiddleName]))
)
上面的 DDL
使用这两个校验值函数作为计算列创建了一个 Person
维度分级表。从该表中查询一行将生成以下结果:
表 1:查询结果
PersonID |
LastName |
FirstName |
MiddleName |
PersonChecksum |
PersonBinaryChecksum |
1234-0001 |
Smith |
John |
Edward |
-52588501 |
-1292791390 |
可以在 PersonChecksum 和/或
PersonBinaryChecksum
上放置一个索引以创建一个哈希索引。这样便可以获得表中计算列的具体的数据结果。放置索引后,便无需在执行维度更新时引用有关
Person
的文本信息。本最佳实践的稍后部分将对此加以介绍。
计算列还支持某些常见的转换类型,这些转换通常发生在为最终的
BI 关系数据存储准备数据之时。
转换类型
对于从提供维度和事实记录的源系统中获取的数据,可以存在任意多种转换类型。其中一些较为常见的转换类型包括:
- 字符串分析 - 获取 char 或
varchar 数据类型中的字符串信息的子集
- 类型转换 -
获取源列中的值,然后将其在最终 BI
解决方案中以不同的数据类型表示(例如,将
tinyint 值 1 或 0 转换为 char(1) 值 Y 或 N)
- 域查找 -
一种转换,其中使用源系统中的值查找数据存储中的替换值(例如,使用维度的自然键在数据集市中查找该维度的代理键)
- 数字转换 -
将数字转换为标准化的值以符合整个数据集市中的标准(例如,将以多国货币表示的销售额转换为美元以便进行分析)
- 域数据验证(范围检查)-
一种转换,其中对值进行检查以查看它们是否位于可接受的范围内(例如,美元销售额应位于
$20,000 和 -$20,000 之间)
在 ETL
中,数据的转换同样可以发生在任意位置。这些转换位置的常见类别包括:
- 出站转换 -
当从源系统中提取数据时转换数据
- 入站转换 -
当将数据装载到分级区域时转换数据
- 分级转换 -
在分级区域中,在数据装载之后及提供给生产之前发生的转换
- 生产装载转换 -
从分级区域获取数据并将其插入到生产数据存储中的维度和事实表中时内置在装载进程中的转换
用于执行转换的技术
有三种技术方法可用于为 DTS
体系结构中的数据实现转换逻辑。这三种方法按其优劣程度依次是:
使用 Transact SQL 进行转换
可以使用 Transact SQL
向存储在关系分级表中的数据应用各种类别的转换。Transact
SQL 通常由 UPDATE 和 DELETE
语句组成,它为转换进程提供了很多优点,其中包括:
- 分级区域中数据的高性能处理
- 通过数据的多重传递支持复杂的转换实现
- 通常使数据库开发人员更易于实现转换逻辑
- 与用于生产装载的首选逻辑联系紧密
使用 DTS 常用转换进行转换
DTS 提供了许多常用转换,可用于完成
BI
分级环境中的许多数据处理任务。用于将这些转换与分级环境配合使用的技术通常需要多个分级表,以便通过各种方式对数据进行转换。使用
DTS 转换的主要优点包括:
- 出站/入站转换过程中的高性能处理
- 可以通过使用 Visual C++
开发自定义转换进行扩展
- 在包设计环境中受到广泛支持
使用 DTS ActiveX
脚本转换进行转换
DTS 提供的常用转换之一是 ActiveX
脚本转换。此转换可用于多种情况,这对转换进程是有益的。但是,这种转换方法也可能会被错误地使用。下面列出了使用这种技术方法的某些好处。
- 支持在出站/入站转换过程中使用复合的自定义逻辑
- 支持在转换过程中包含 COM 对象
- 在 DTS 中集成了多阶段数据泵
(Multiphase Data Pump) 功能
对于这最后一种方法,最需要注意的就是其性能不如
Transact SQL 和常用 DTS
转换。与使用其他技术方法执行转换相比,使用
ActiveX
脚本逻辑所需的系统开销通常会导致速度明显变慢。
通过多重传递进行转换
在数据转换过程中,通过多重传递原始数据以便进行转换通常是一个很好的方法。SQL
Server 和 Transact SQL
通常能够很好地使用基于集的操作在多重传递中实现复合的转换逻辑。基于集的操作为处理较大数据集提供了一种手段,这比单个处理(通常基于一次处理一行的光标逻辑)更有效。
将 T-SQL
逻辑分块以获得更好的日志性能
虽然 SQL Server
能够很好地支持基于集的操作,但是大量数据的仓储和转换会影响对事务日志总体大小的需求。要在执行日志数据的处理时减轻对事务日志的影响,一种常见的方法是将数据分块以形成较小的子集。这些较小的数据子集便于处理,并且需要记录的事务数量也较少,从而降低了总的事务日志需求。
在 Transact SQL
中,可以通过多种不同的方式将数据分块。通常,这需要使用一个
WHILE
循环并在此循环中使用相应技术来更新子集。这些技术依赖于
ROWCOUNT 或 WHERE
子句以便仅影响数据的子集。通过循环每个逻辑块,与此逻辑关联的事务将被提交,从而使
SQL Server
能够释放该关联事务的日志空间。由于这样做是为了将事务隔离到所处理的每个数据块中,因此最好不要在
WHILE 循环以外开始或提交事务。
集中处理转换
在转换逻辑的所有实现方法中,可以选择在
ETL
进程的多个部分中创建转换。虽然确实可以这样做,但它并不是一种最佳实践。而将转换逻辑集中放在分级区域中的
Transact SQL 操作中则可以提供以下优点:
- 使转换逻辑易于维护
- 减少了因源系统平台的变化而受到的影响
- 如果逻辑是在分级区域中完成的,则可以最大限度地减小对源系统的影响
- 可以创建转换函数知识库以便在开发其他
BI 解决方案时使用
转换和清理数据的指导原则
通过将所讨论的用于转换和清理数据的技术组合在一起便可能得到适当的
BI
解决方案。在考虑这些组合时,可以遵循一些指导原则以帮助获得最佳的方法。这些原则包括:
- 在使用 CHECKSUM() 和 BINARY_CHECKSUM()
函数时,确保以相同的顺序执行校验值比较。如果列顺序或参数顺序发生变化,将导致不同的校验值结果。
- 如果使用 ActiveX
脚本转换,请使用列序号代替列名称以获得更好的性能。
- 考虑将数据分块,即使初始数据的大小并不大。在转换过程中,数据分块为确保在
Transact SQL
处理中实现可伸缩性提供了最简便的方法。
- 如果可能,确保仅在数据装载到分级区域后再添加索引,以便获得最佳性能。
通过在分级区域中转换数据以获得高效的结果,可以方便地过渡到
ETL 进程的装载阶段。
管理缓慢变化的维度装载方法
缓慢变化的维度类型
在业务智能领域,有以下三种缓慢变化的维度类型:
- 类型 I -
当某个维度成员的数据发生变化时,最新的列值将覆盖以前的维度记录,从而清除了该维度成员的历史记录。
- 类型 II -
当某个维度成员的数据发生变化时,最新的列值将存储为维度中的新记录,从而提供了一个维度成员的多个实例,这样便保留了历史记录。
- 类型 III -
当某个维度成员的列数据发生变化,而数据集市要保留该变化列的最后一个版本时,原始数据将移到该维度记录的最后一个版本列中,并且所有新维度信息将覆盖现有列。
注意:通常情况下,特别对于本白皮书,不会考虑采用类型
III 缓慢变化维度。
多阶段数据泵
多阶段数据泵是 SQL Server 2000
中引入的一个功能,为使用 DTS
数据泵在数据源之间移动数据提供了更大的灵活性。数据泵现在支持以下阶段:获取源数据之前、在行的转换过程中、转换行之后、完成行的批处理之后、从数据源读取了最后的数据之后以及完成数据泵操作之后。通过使用
ActiveX
脚本转换,可以提供这些阶段并在其中包含关联的脚本。
虽然使用 ActiveX
脚本转换处理数据并不适用于转换大量数据,但对于装载维度记录这一特定进程,通常却不需要处理大量数据。有鉴于此,本节介绍的技术提供了一个首选方法,用于管理维度变化并利用了
DTS 的内部功能来完成此任务。
全局 ADO 断开连接的记录集
如前面的“通过 Execute SQL
任务填充全局变量”一节中所述,此方法的一个关键因素是能够缓存生产数据集市中的维度成员。此数据的缓存是一个客户端断开连接的记录集,由
Execute SQL 任务存储在全局变量中。
索引断开连接的记录集
索引可以加快数据的搜索操作而不管其位置如何。这对断开连接的记录集更是如此。要在断开连接的记录集上放置索引,应当将该记录集对象字段集合中的字段的
Optimize(优化)属性设置为 True。下面的代码片断显示了建立记录集索引的确切进程。
Rs.Fields(<Field Name or Ordinal Here>).Properties("Optimize") = True
BatchUpdate()
断开连接的记录集
断开连接的记录集支持更新其值。本节稍后部分讨论的类型
II
维度变化管理提供了执行此操作的示例。如果断开连接的记录集的更新无法重新写入此记录集的原始连接,这些更新将没有任何意义。幸运的是,ADO
支持记录集的重新连接,并且记录集对象还提供了一个
BatchUpdate() 方法。BatchUpdate() 方法可以将断开连接时完成的更新全部填充到记录集的源中。
使用 DTS 管理类型 I
缓慢变化维度的更改
管理类型 I
缓慢变化维度的维度更改要求执行以下操作:
- 将新维度成员插入到生产维度表中
- 更新包含一个或多个已变化列的现有维度成员
- 未变化的现有维度成员不需要进行任何修改,但是可以将其覆盖以简化逻辑。
在维度的装载过程中,可以使用 DTS
的函数方便地执行这三步操作。DTS 提供了 Data
Driven Query
任务和查找功能,允许按照条件来插入、更新或跳过行。下图显示了用于执行维度装载的
DTS 包工作流。
图 7:连接任务
由于目标维度表包含校验值计算列,因此需要使用
Configure Connection 任务来更改 ARITHABORT
选项。具体地说,此 Execute SQL
任务将执行以下语句:
SET ARITHABORT ON
该装载逻辑的其余部分将在“装载维度”数据驱动查询任务中完成。下面列出了用于装载类型
I 缓慢变化维度的转换逻辑。
' 声明每列的常量,以便在运行时
' 更有效地解析列
Const ACCOUNT_NUM = 0
...
Const COUNTRY = 9
Function Main()
Dim AccountWK
' 执行查找以查看是否存在帐户
AccountWK = _
DTSLookups("LookupAccountKey").Execute(DTSSource(ACCOUNT_NUM))
DTSDestination(ACCOUNT_NUM) = DTSSource(ACCOUNT_NUM)
... The same mapping is performed for 10 columns
DTSDestination(COUNTRY) = DTSSource(COUNTRY)
if IsEmpty(AccountWK) then
Main = DTSTransformstat_InsertQuery
else
Main = DTSTransformstat_UpdateQuery
End if
End Function
使用 DTS 管理类型 II
缓慢变化维度的更改
管理类型 II
缓慢变化维度的维度更改要求执行以下操作:
- 将新维度成员插入到生产维度表中
- 将具有一个或多个已变化列的现有维度成员标记为不再是当前版本并给出一个到期日期。
- 对于已变化的维度成员,将创建一个新记录以便保留历史记录,并且该记录被标识为当前记录。
完成这些步骤需要配合使用以下技术,即比较分级表和生产表中的校验值信息、访问更改以及使用
Transform Data
任务将新记录插入到维度表中。有一个因素没有包含在
Transform Data
任务中,即对更新的支持。因此需要使用客户端断开连接的记录集以便支持管理维度更改时所必需的更新。
开始时,首先要使用 Transform Data
任务在两个连接之间创建一个包。然后,该任务的转换将被修改为
ActiveX 脚本转换,转换代码如下:
' COLUMNS 的常量
Const PRODUCTID_WK = 0
...
Const CURRENT_RECORD_IND = 20
Const EFFECTIVE_BEGIN_DATE = 21
Const EFFECTIVE_END_DATE = 22
Const CHECKSUM = 23
Dim Conn
Dim Rs
Dim nRowsInserted
Dim nRowsSkipped
Dim nRowsUpdated
Function PreSourceMain()
' 在转换任务的开始部分运行
nRowsInserted = 0
nRowsUpdated = 0
nRowsSkipped = 0
' 我们需要建立
' 一个 ADO 断开连接的记录集
' 创建连接对象的实例,然后打开该
' 连接
Set Conn = CreateObject("ADODB.Connection")
Conn.Open "file name=" & DTSGlobalVariables("gsUDLFile").Value
' 创建记录集对象的实例,然后在某个表中打开该
' 记录集对象
Set Rs = CreateObject("ADODB.Recordset")
' 将光标位置设置到客户端对于
' 获得断开连接的记录集非常重要
Rs.CursorLocation = adUseClient
' 装载所有当前维度记录
Rs.Open "Select * from <production data mart dimension> " + _
" where Current_Record_Ind = ' Y' order by productid_nk ASC ", _
Conn, _
3, _
adLockBatchOptimistic
' 断开记录集的连接
Set Rs.ActiveConnection = Nothing
Conn.Close
If Rs.EOF And Rs.BOF Then
Rs.Close
Set Rs = Nothing
Else
Rs.MoveFirst
' 在断开连接的记录集上放置一个索引
Rs.Fields(PRODUCTID_NK).Properties("Optimize") = True
End If
PreSourceMain = DTSTransformstat_OK
End Function
完成数据泵的 PreSource(获取源数据之前)阶段后,将创建一个索引的、断开连接的记录集,以便执行更新并使数据泵任务仅执行
INSERT
操作。使用转换数据任务时允许使用快速装载选项。在多数类型
II
维度中,大部分数据写入工作是插入新记录,而不是通常所认为的执行大量更新。
要检查的下一个阶段是行转换阶段,其逻辑使用了
Main()
函数。在此阶段中,将按自然键搜索索引,生产记录的校验值将与分级记录的校验值进行比较,同时还要执行前面介绍的维护逻辑。以下代码揭示了该进程:
Function Main()
' 为每个基于行的转换运行
If Not (Rs Is Nothing) Then
If Not Rs.BOF And Not Rs.EOF Then
Rs.MoveFirst
' 在断开连接的记录集中搜索记录
Rs.Find "productid_nk = " & DTSSource("ProductID_NK")
End If
If Not Rs.EOF Then
If (Rs.Fields(CHECKSUM).Value) = _
(DTSSource(CHECKSUM).Value) Then
' 跳过该行,因为它完全相同
' 退出该函数,因为不需要进行
' 列映射
Main = DTSTransformStat_SkipRow
Exit Function
Else
' 在断开连接的记录集中将该行标记为不是当前记录
' 因为新记录已更改
' 并且旧记录必须到期
Rs.Fields(EFFECTIVE_END_DATE).Value = Now
Rs.Fields(CURRENT_RECORD_IND).Value = "N"
End If
End If
End If
' 映射列值,因为此时已插入了所有行
...
DTSDestination(PRODUCTID_NK) = DTSSource(PRODUCTID_NK)
' 为此维度成员设置有效的起始日期
' 并将有效的结束日期设置为一个非常遥远的日期
' 将此记录设置为当前 (CURRENT) 版本
DTSDestination(EFFECTIVE_BEGIN_DATE) = Now
DTSDestination(EFFECTIVE_END_DATE) = "01/01/2075"
DTSDestination(CURRENT_RECORD_IND) = "Y"
Main = DTSTransformStat_InsertQuery
End Function
通过该行转换阶段,类型 II
的更改得到了维护。最后一个重要步骤是确保对客户端记录集所执行的更新能够写入到数据库中。这一步骤将在数据泵的
Post Source(获取源数据之后)阶段执行。以下代码显示了用于重新连接记录集并同步更新的记录的逻辑(即
PostSourceMain() 函数)。
Function PostSourceMain()
' 创建连接对象的实例,然后打开该
' 连接
Set Conn = CreateObject("ADODB.Connection")
Conn.Open "file name=" & DTSGlobalVariables("gsUDLFile").Value
If Not (Rs Is Nothing) Then
' 连接记录集
Set Rs.ActiveConnection = Conn
' 将所有更新送回服务器
Rs.UpdateBatch
Rs.Close
Conn.Close
' 清除
Set Rs = Nothing
Set Conn = Nothing
End If
PostSourceMain = DTSTransformstat_OK
End Function
当数据泵的 Post Source
阶段完成后,维度中的所有数据将被成功更新。
使用断开连接的记录集增强了数据驱动查询方法的性能。虽然数据驱动查询方法是用于显示类型
II 维度变化管理的,但类型 I
维度也可以使用此方法。
使用 Transact SQL
管理维度变化和进行装载
使用 DTS
的数据泵功能的另一种适当方法是使用 Transact SQL
语句从分级表中装载维度。对于每个要装载的维度,通常可以通过执行两条
Transact SQL 语句来获得与早先的以 DTS
为中心的方法相同的结果。
类型 I 维度逻辑
要管理类型 I
维度的更改,第一个语句应该为 UPDATE
语句,用来在自然键列(由源系统提供)中执行分级表和生产表的内部联接。第二个语句应该为在结尾具有
NOT EXISTS 子句的 INSERT
语句,以确保仅插入不存在的维度成员。
类型 II 维度逻辑
要管理类型 II
维度的更改,第一个语句应该为 UPDATE
语句,用来在自然键列(由源系统提供)中执行分级表和生产表的内部联接。此
UPDATE
语句将设置有效的结束日期并将记录标记为非当前版本(其中校验值不匹配)。第二个语句应该为在结尾具有
NOT EXISTS 子句的 INSERT 语句,以确保仅附加没有现有“当前”记录的维度成员。
在 DTS 中利用此逻辑需要使用 Execute
SQL 任务。
管理慢速维度更改的指导原则
除了转换逻辑之外,维度更改管理是
ETL
处理中逻辑性较强的操作之一。下面所介绍的各种方法将提供维度装载的基础知识。除了提供的方法之外,这里还介绍了实用的指导原则,以帮助实现各种方法。
- 使用数据驱动查询时,请务必使用单独的数据库连接进行查找,以确保数据泵的读写操作不会因为使用相同的连接执行查找而被序列化。
- 使用校验值或二进制校验值功能可大大简化类型
II 更新的比较逻辑 (comparison logic)。
- 在 UDL
文件中存储连接信息,以便通过使用在 UDL
文件上打开的连接来实现重新连接记录集。进行此操作可以减少对
DTS
包内的连接信息进行硬编码的可能性,并且要求仅将
UDL 文件位置的路径存储为全局变量中的元数据。
- 在尝试装载之前对维度成员执行所有其他数据的转换,可以简化装载进程。
- 要获取有关多相数据泵的文档,请参阅
SQL Server 联机图书 (SQL Server Books Online)。
然后进行所有维度的装载,装载 BI
解决方案的事实数据是完成装载关系数据存储进程的最后一个步骤。
管理事实装载方法
使用事务处理 SQL (Transact
SQL) 联接指派密钥
在装载事实数据(存在于维度相交部分的附加数据元素)过程中,最基本的部分是:在将源系统中的数据放入已装载的维度成员中时建立数据的关系。建立此关系与在自然键列中执行生产维度表和分级事实表的联接一样简单。
数据泵选项 -
插入批处理规模 (Insert Batch Size)
使用数据泵的主要驱动程序的任务之一是可以批处理事务。通过修改数据泵的“插入批处理规模”选项可以启用事务批处理。如果所提供的值大于零,数据泵将在事务批处理中执行指定的插入数量。事务的批处理是重要的可缩放性元素。这一点在装载大量事实和尝试将整体事务日志大小要求维持为“小”时体现得尤为突出。
将总体插入分解为批量具有与“转换”方法和“清洗
(Cleansing)”方法中所述的“分块 (Chunking)”方法一样的优点。
限制索引以提高速度
在“BI 解决方案装载”窗口中,装载事实数据是占用较多时间的操作之一。在解决方案设计初期需要考虑的因素包括:是否查询关系数据或此数据是否仅成为
Analysis Services OLAP
多维数据集体系结构的源。如果是后者,则存在消除事实数据中所有索引的机会。在
Analysis Services
中,多维数据集可能具有优化的架构,此架构可以使多维数据集仅处理事实表的数据,且不需要联接至维度表。
不进行索引的优点是在将数据装载至事实表时,效率会增加两倍(或更多)。尽管审核是在随后的白皮书中进行讨论的,但是此处却需要在事实表中对审核列进行索引。执行此操作后,如果装载进程中出现任何错误,则可以退出一个或多个事实数据的增量装载。
装载事实记录的进程
在事实表和维度表之间执行 LEFT OUTER
JOIN 可以精确地表示缺少的维度成员密钥。可以在
SELECT 子句中使用 CASE 语句和 IS NULL
求值来检查缺少的维度成员密钥。执行此检查的目的是将缺少维度密钥的事实记录的空密钥值分配给两个特殊的维度记录。这两个维度记录为“无效的”或“未知的”维度成员。下面提供了一个样例事务处理
SQL
语句,显示了生产中的维度表和分级区域中的事实表之间的联接。
SELECT DimensionKey = CASE
WHEN Dim.Key is null AND Fact.NaturalKey is null
THEN 0 /*Unknown Dimension Record Key*/
WHEN Dim.Key is null AND Fact.NaturalKey is not null
THEN -1 /*Invalid Dimension Record Key*/
ELSE Dim.Key END,
Fact.Measure1, Fact.Measure2
FROM StagingDB..Fact LEFT OUTER JOIN
ProductionDB..Dimension ON (Fact.NaturalKey = Dim.NaturalKey)
映射现有密钥和空值密钥会得到十分精确的数据,该数据反映了数据质量的整体状态并被插入到事实表中。
随后可以用 DTS
数据泵将此联接操作返回的记录装载至目标事实表。尽管仅使用事务处理
SQL 和 INSERT 语句也可以实现此操作,但 ETL
可能会放松对如何处理单个错误记录或小批量错误记录的控制。使用数据泵操作的批量装载功能,可以将错误的数据隔离至一个批量,并装载事实记录的其余部分。
事实装载的指导原则
在大多数 ETL
设计中,装载事实记录的进程比 ETL
的其他进程简单。此处介绍的方法提供了完全(或增量)装载事实数据的解决方案,且支持多个事实表分区的装载,以获得最大可缩放性。考虑到此方法中介绍的元素,此处提供了开发产品事实装载
ETL 时要注意的一些指导原则:
- 如果关系数据存储中包括多个事实表分区,每次每个装载线程都应处理单个分区。请尽量避免对视图使用条件分区逻辑或拆分事实数据插入。
- 通过在数据装载后运行 UPDATE
STATISTICS 语句或使用“自动更新统计信息”数据库选项,以确保拥有维度表索引的最新统计信息。
- 因为批处理提交大小 (batch commit size)
用来管理插入到事实表的批量大小和用来捕获错误的粒度,所以应在这两个因素之间建立合理的平衡。使用设置
0 或 1,通常会造成单个批量或每行的批量被取消。
- 请考虑将数据装载至多个事实分区表,以支持
BI 解决方案将来的修改。如果操作的要求是保持
36 个每月分区,则每月删除第 37
个分区与删除该表并重新创建分区视图一样简单。
实践摘要
优点
使用为“提取、转换和装载”最佳实践提供的方法具有很多优点,可以通过
SQL Server BI
平台使操作数据可用。此最佳实践的主要优点包括:
- 可重启性
使用 DTS
数据泵将数据提取到文本文件、在分级区域转换数据并装载数据为将此进程的某个分区选作“重新启动”点提供了可能(如果存在问题)。将数据放入文本文件后,没有必要在生产源系统中诊断数据质量问题,而且更重要的是如果需要重新启动进程,没有必要再次从此源系统进行提取。
- 可管理性
在分级区域定义转换以及在装载时执行密钥查找可以确保能保留这些
ETL 进程中的关键任务。
- 综合装载功能
结合事务处理 SQL 和 DTS
数据泵的功能后,装载解决方案兼有这些平台的独特功能,利用各自正确的技术特点可以获取综合的解决方案。
- 控制数据质量报告
通过指出最常见的数据质量问题(例如“无效的”和“未知的”维度成员),DTS
ETL
解决方案在提醒组织注意日常数据质量问题方面发挥了重要作用,且不会破坏分析数据的可用性。
注意事项
此实践和各方法的注意事项是有限的,但确实存在。关键的注意事项包括:
以往,ETL
进程仅涉及到通过关系数据集市或数据仓库使数据可用的内容。如今,使用数据仓库框架,许多
BI 解决方案中的数据最终会放在 Analysis Services(分析服务)平台中。下一部分将讨论作为
DTS BI 解决方案基础结构一部分的管理 Analysis Services
分区的角色。
Analysis
Services 管理实践
本部分重点介绍 Analysis Service
维度、多维数据集以及分区的管理和处理。此处介绍的方法是通过
DTS 和其他自定义的应用程序管理 Analysis Services
的多种方法的子集。Analysis Services
提供了多个界面,用来编程管理 OLAP
数据库架构的结构和分析服务器 (analysis server)
的功能。各元素(例如管理分区上的聚合 [包括基于用法的优化方法],通过安全角色动态控制对
OLAP 对象的访问,执行 OLAP 数据库归档并管理其他
OLAP 对象 [如链接的多维数据集]
的生命周期)都可以通过这些界面访问并应在数据仓库框架中自动化(不提及其各自的白皮书)。本部分重点介绍决策支持对象
(Decision Support Objects) 的角色、一个 Analysis Services
界面和 DTS Analysis Services
的处理任务(创建于决策支持对象顶部),作为进行
Analysis Services 管理自动化的两种技术。
分区管理方法
DTS
中的决策支持对象
要在中型到大型业务智能解决方案中使用
Analysis Services
平台以获取更好的性能和更大的可缩放性,应该考虑在单个多维数据集中管理多个
OLAP
分区。此外,如果已对分区关系数据进行了决策,则必须选择创建并随后管理
OLAP 分区。使用所介绍的其他技术和实践,作为整体
BI 体系结构一部分,DTS
在关联关系数据存储和多维数据存储、通过 Analysis
Services 管理进程(包括创建、克隆、合并和删除 OLAP
分区)来管理此数据的 OLAP
可用性方面可以发挥重要的作用。连同 DTS,形成此功能的主要因素还有
SQL Server 2000 的 COM
库的集合。这些库(决策支持对象或 DSO)可各自安装为
Analysis Service 安装中的一个选项,或默认为 SQL Server
2000 的 Analysis Services 组件的完全安装的一部分。
DSO 是 Analysis Services 管理功能的 COM
界面。通过使用 DSO 和 DTS,Analysis Manager
用户界面的很多功能可以从 ActiveX 脚本和其他支持
COM
自动化的编程语言中获得。要使用决策支持对象和
DTS 为管理分区打下基础,需要使用部分 DSO
对象模型层次结构以获得此方法,如下所示:
图 8:DSO 和 DTS
创建分区
创建分区的进程是在 DTS 中使用 DSO
的较常见方案之一。自动化 OLAP 分区创建进程使
Analysis Services 多维数据集和多维数据存储可以随 BI
解决方案中的关系数据不断变化发展。经常需要使用驱动程序(用于使用
DTS 创建新的 OLAP 分区)来帮助管理 OLAP
架构,以动态反映关系数据存储更改。
以下脚本可以放入任何 ActiveX
脚本对象(通常用于 ActiveX Script
任务)中并可以用来创建 OLAP 分区。
Function CreatePartition (strServer, strDabase, strCubeName, strPartition)
Dim objServer
Dim objDB ' 一个 DSO.MDStore 对象
Dim oCube ' 一个 DSO.MDStore 对象
Dim oPart ' 一个 DSO.MDStore 对象
Set objServer = CreateObject("DSO.Server")
objServer.Connect(strServer)
上文中第一个步骤用来例示与 DSO
对象模型不同的本地服务器对象。此对象将用来检查
OLAP
数据库、多维数据集以及分区是否存在,并在必要时创建
OLAP 分区。
If objCube.MDStores.Find(strPartition) Then
Set objPart = objCube.MDStores(strPartition)
End If
If objPart Is Nothing Then
' 添加新分区
Set objPart = objCube.MDStores.AddNew(strPartition)
End If
Set oPart = Nothing
Set oCube = Nothing
Set oDB = Nothing
Set oServer = Nothing
CreatePartition = True
End Function
通过支持动态创建分区,OLAP
多维数据集将随需要增大以容纳新数据,而不是使用
Analysis Services
的管理功能预先分配不必要的分区,这会妨碍或影响用户的交互操作。
克隆分区
克隆的分区,顾名思义,是先前存在的
OLAP
分区的副本。当编程管理具有底层聚合的分区时,克隆分区很重要。随着对整体分区管理方法研究的深入,更清楚地表明要适当地进行分区克隆。但是要解释使用
DSO 和 DTS
来克隆分区的好处,首先要研究一下如何在 DTS 的
ActiveX 脚本中克隆现有分区的示例。
Function ClonePartition(strServer, strDabase, strCubeName, _
strPartition, strBasedOnPartition, strSliceValue)
Dim objServer
Dim objDB ' 一个 DSO.MDStore 对象
Dim oCube ' 一个 DSO.MDStore 对象
Dim oPart ' 一个 DSO.MDStore 对象
Dim objClonePart ' 一个 DSO.MDStore 对象
Dim objSourcePartDimension ' 一个 DSO.Dimension 对象
Dim objSourcePartLevel ' 一个 DSO.Level 对象
Dim lngDim
Dim lngLev
Set objServer = CreateObject("DSO.Server")
objServer.Connect(strServer)
' 添加分区并克隆该分区
Set objPart = oCube.MDStores.AddNew(strPartition)
' 复制分区以获取其基本结构
objClonePart.Clone objPart
If oCube.MDStores.Find(sBasedUponPartitionName) Then
Set oClonePart = oCube.MDStores(strBasedOnPartition)
Else
Exit Function
End If
' 遍历各级别,从新克隆的分区中清除每个扇区值
cloned partition
' 因为扇区值需要重建
For lngDim = 1 To objSourcePart.Dimensions.Count
Set objSourcePartDimension = objSourcePart.Dimensions(lngDim)
For lngLev = 1 To objSourcePartDimension.Levels.Count
Set objSourcePartLevel = objSourcePartDimension.Levels(lngLev)
objSourcePartLevel.SliceValue = ""
Next
Next
set objSourcePartDimension = nothing
set objSourcePartLevel = nothing
到此时已完成克隆分区的创建,其扇区信息(过滤信息)已被清除。没有扇区信息,分区将从事实表中装载所有的数据,而这不是我们所希望的。需要将新的扇区值应用到已克隆的分区,下面列出了进行该操作所需的代码。
' 应用新的扇区值
' 假设扇区值是完全符合要求的
(即 "Calendar=All Years.1997.1.January.3" 表示 1997 年 1 月 3 日)
Years.1997.1.January.3" for January 3rd, 1997)
call ApplySliceValue(objPart, CStr(strSliceValue))
set objClonePart = nothing
set objPart = nothing
set objCube = nothing
set objDB = nothing
set objServer = nothing
End Function
Function ApplySliceValue(pobjPart, pstrSliceValue)
Dim strSliceValue
Dim strDimensionName
Dim lngStringPosition
Dim objDimension
Dim objLevel
Dim astrLevelSlices()
Dim lngLevel
ApplySliceValue = False
If pstrSliceValue = "" Then
Exit Function
End If
' 假设扇区值是完全符合要求的
'
(即 "Calendar=All Years.1997.1.January.3" 表示 1997 年 1 月 3 日)
lngStringPosition = InStr(1, pstrSliceValue, "=")
If lngStringPosition > 1 Then
strDimensionName = Trim(Mid(pstrSliceValue, 1, lngStringPosition - 1))
End If
If strDimensionName = "" Then
Exit Function
End If
If Not pobjPart.Dimensions.Find(strDimensionName) Then
Exit Function
End If
Set objDimension = pobjPart.Dimensions(strDimensionName)
strSliceValue = _
Trim(Mid(pstrSliceValue, lngStringPosition + 1, _
Len(pstrSliceValue) - (lngStringPosition)))
astrLevelSlices = Split(strSliceValue, ".")
For lngLevel = LBound(astrLevelSlices) To UBound(astrLevelSlices)
If objDimension.Levels.Count - 1 < lngLevel Then
Exit Function
Else
Set objLevel = oDimension.Levels(lngLevel + 1)
objLevel.SliceValue = astrLevelSlices(lngLevel)
End If
Next
ApplySliceValue = True
End Function
克隆分区的方法与创建分区的方法有些类似,但也有一些不同的地方。创建分区时,会进行检查以确保不存在相同名称的另一个分区。克隆分区时,也会进行检查,但是要确保另一个分区确实存在。此外,通常会为已克隆的分区更新扇区信息,以避免在
OLAP 多维数据集中复制数据。
克隆分区会得到两个结构相同但扇区值不同的分区,可以在今后对其进行同化或合并。
合并分区
尽管分区管理方法重点介绍了创建分区和克隆分区,以使其随着关系数据存储的变化而动态增加,但是一旦这些分区不再是多维数据集中的分析“热点”时,就完全有必要进行分区合并。DSO
可以合并两个具有相同聚合(或不具有聚合)的相同结构的分区。通过编程方式,DTS
使在 BI
解决方案体系结构中实现此操作的进程变得相对简单。
以下示例脚本中合并了两个分区(源分区和目标分区)。
Function MergePartitionsbyName( strDatabase, _
strCubeName, _
strSourcePartition, _
strTargetPartition, _
objServer)
Dim objDB ' 一个 DSO.MDStore
Dim objCube ' 一个 DSO.MDStore
Dim objSourcePart ' 一个 DSO.MDStore
Dim objTargetPart ' 一个 DSO.MDStore
Dim objSourcePartDimension ' 一个 DSO.Dimension 对象
Dim objSourcePartLevel ' 一个 DSO.Level 对象
Dim objTargetPartDimension ' 一个 DSO.Dimension 对象
Dim objTargetPartLevel ' 一个 DSO.Level 对象
Dim lngDim
Dim lngLev
MergePartitionsByName = False
' 检查数据库是否有效
If objServer.MDStores.Find(strDatabase) Then
Set objDB = objServer.MDStores(strDatabase)
Else
MergePartitionsByName = False
Set objServer = Nothing
Exit Function
End If
' 检查多维数据集名称是否有效
If objDB.MDStores.Find(strCubeName) Then
Set objCube = objDB.MDStores(strCubeName)
Else
Set objDB = Nothing
Set objServer = Nothing
MergePartitionsByName = False
Exit Function
End If
' 检查分区是否存在
If objCube.MDStores.Find(strSourcePartition) Then
Set objSourcePart = objCube.MDStores(strSourcePartition)
Else
Set objCube = Nothing
Set objDB = Nothing
Set objServer = Nothing
Exit Function
End If
If objCube.MDStores.Find(strTargetPartition) Then
Set objTargetPart = objCube.MDStores(strTargetPartition)
Else
Set objCube = Nothing
Set objDB = Nothing
Set objServer = Nothing
Exit Function
End If
在 DSO
下,不能合并具有不同扇区的分区。要编程解决这个问题,首先可以删除每个分区的扇区,然后进行合并,再重新应用合适的扇区值。
' 遍历各级别,从源中清除每个扇区值
' 因为扇区值需要重建
For lngDim = 1 To objSourcePart.Dimensions.Count
Set objSourcePartDimension = objSourcePart.Dimensions(lngDim)
For lngLev = 1 To objSourcePartDimension.Levels.Count
Set objSourcePartLevel = objSourcePartDimension.Levels(lngLev)
objSourcePartLevel.SliceValue = ""
Next
Next
' 遍历各级别,从目标中清除每个扇区值
' 因为扇区值需要重建
For lngDim = 1 To objTargetPart.Dimensions.Count
Set objTargetPartDimension = objTargetPart.Dimensions(lngDim)
For lngLev = 1 To objTargetPartDimension.Levels.Count
Set objTargetPartLevel = objTargetPartDimension.Levels(lngLev)
objTargetPartLevel.SliceValue = ""
Next
Next
set objSourcePartDimension = nothing
set objSourcePartLevel = nothing
set objTargetPartDimension = nothing
set objTargetPartLevel = nothing
objTargetPart.Merge strSourcePartition
' 应用新的扇区值
' 假设扇区值是完全符合要求的
(即 "Calendar=All Years.1997.1.January.3" 表示 1997 年 1 月 3 日)
Years.1997.1.January.3" for January 3rd, 1997)
call ApplySliceValue(objTargetPart, CStr(strSliceValue))
objCube.Update
objServer.Refresh
MergePartitionsByName = True
' 执行清除
Set oSourcePart = Nothing
Set oCube = Nothing
Set oDB = Nothing
Set oServer = Nothing
Exit Function
通过 DTS 和 DSO
合并分区可以解决管理分区发展、分区的数量随时间的推移而增加的体系结构难题,并通过删除分区为整理
OLAP 数据提供明确的指导。
删除分区
尽管此处介绍的方法是关于使用 DSO 和
DTS 删除 OLAP
分区的,但实际上方法的中心是解决如何维护数据的可用性的问题(这对于
BI
解决方案的用户很有意义)以及消除从分析角度已不再可行的数据。结果,执行删除分区任务时通常会同时从关系数据存储中删除或清理旧的、无意义的事实数据(由
DTS 体系结构控制)。
删除分区操作实际上是使用 DSO
进行的较简单编程操作之一。用来删除分区的实际代码为:
Function DeletePartition(strDatabase, _
strCubeName, _
strPartition, _
objServer)
Dim objDB ' 一个 DSO.MDStore
Dim objCube ' 一个 DSO.MDStore
Dim objPart ' 一个 DSO.MDStore
DeletePartition= False
' 检查数据库是否有效
If objServer.MDStores.Find(strDatabase) Then
Set objDB = objServer.MDStores(strDatabase)
Else
DeletePartition= False
Set objServer = Nothing
Exit Function
End If
' 检查多维数据集名称是否有效
If objDB.MDStores.Find(strCubeName) Then
Set objCube = objDB.MDStores(strCubeName)
Else
Set objDB = Nothing
Set objServer = Nothing
DeletePartition= False
Exit Function
End If
' 检查分区是否存在
If objCube.MDStores.Find(strPartition) Then
Set objPart = objCube.MDStores(strPartition)
Else
Set objCube = Nothing
Set objDB = Nothing
Set objServer = Nothing
DeletePartition = False
Exit Function
End If
' 检查分区是否存在
If objCube.MDStores.Find(strPartition) Then
If objCube.MDStores.Count = 1 Then
' 无法删除上一个多维数据集分区
Set objCube = Nothing
Set objDB = Nothing
Set objServer = Nothing
DeletePartition = False
Exit Function
End If
End If
objCube.MDStores.Remove (strPartition)
End If
objCube.Update
DeletePartitionbyName = True
' 执行清除
Set objPart = Nothing
Set objCube = Nothing
Set objDB = Nothing
Set objServer = Nothing
Exit Function
使用 DSO 和 DTS
来自动化删除分区操作是建立对整个体系结构编程管理的绝对控制的最后一个元素,这提供了复杂的自动的基础结构,以提供
SQL Server BI 解决方案。
管理
OLAP 分区的指导原则
使用 DSO 和 DTS 来管理分析服务中的
OLAP
分区具有一些核心要求。下面是满足这些要求的一些指导原则:
- SQL Server
代理服务帐户应当是域帐户。如果 DTS
包要用于管理安装在某台计算机上的分析服务,而该计算机位于运行了
SQL Server
代理的本地计算机上下文以外,则更是如此。
- 在利用 DSO 执行 DTS 包之前,SQL Server
代理服务帐户(无论是在本地计算机上还是在域中)必须是分析服务的
本地“OLAP 管理员”组的成员 。
- 在 BI
解决方案的初始阶段,合并分区通常是不可能的,因为要实现基于使用情况的优化和其他旨在提高性能的初始
OLAP 设计策略。
- 使用 DTS
防止出现没有基本关系数据支持的孤立的 OLAP
分区。如果没有关系数据,OLAP
数据也将不存在。
有关管理分析服务的详细信息,请参阅本白皮书结尾的“更多信息”一节。
处理分析服务的方法
分析服务处理任务
将 DTS
与分析服务相集成的一个关键因素是处理 DTS 包的
OLAP 对象,即维度、多维数据集和分区。幸运地是,DTS
具有一个常用任务,恰好可以用于此目的。Analysis
Services Processing
任务可用来处理任何可访问的分析服务器上的 OLAP
对象。
此任务的存在使 DTS
不仅可以执行传统的 ETL
操作,还可以将装载操作从关系数据存储扩展到 OLAP
多维数据存储。对于 Analysis Services Processing
任务,有一点并不广为人知,即该任务是用 Visual
Basic 6.0 和自定义任务界面(DTS
通过自己的组件对象模型提供的)编写的。
在主线程中执行
熟悉 Analysis Services Processing
任务的 DTS
开发人员可能会发现以下事实,即当任务添加到包中时,此任务的工作流属性与大多数其他“常用”DTS
任务的工作流属性不同。最值得注意的是,Execute on
main package thread(在主包线程中执行)被设置为 True(如下所示)。这表明该任务是使用不支持自由线程模式(或单元线程模式)的语言开发的(在本例中是
Visual Basic 6.0)。
图 9:Workflow Properties(工作流属性)窗口
因为 DTS
在封送资源中使用了自由线程模式,所以不支持此模式的任务必须在主执行线程中运行才能正常工作。“正常工作”是指可以在
DTS 环境中的自由线程中进行集成,并且没有给 DTS
环境带来不稳定性。对于包含和执行 Analysis
Services Processing 任务的任何子包也是如此。在父/子包方案中,需要将父包中的
Execute Package
任务的工作流属性手动设置为在主执行线程中执行子包。这样做将要求子包中的所有并发工作流在单个线程中序列化。
使用
DTSRUN 执行分析服务处理
DTSRun 是随 SQL Server 2000
提供的一个命令提示实用工具,可用来从命令行执行
DTS
包。此外,命令行参数还允许在开始执行时为包传递全局变量。dtsrun.exe
是安排 DTS
包的执行的最常用方法。还有一个替代实用工具,即
DTSRunUI 或 dtsrunui.exe,它提供了一个图形环境,从中可以生成带参数的
dtsrun 命令行。
虽然 DTSRun 通常用来将包作为 SQL Server
代理中的作业步骤来执行,但它也可以与 DTS 中的 Execute
Process 任务相结合,以提供一个全新的 Win32
进程,从中可以建立一个新的执行线程。下图显示了将普通的
Win32 进程执行功能与 dtsrun
实用工具相结合,以启动一个 OLAP 处理子包。
图 10:Execute Process Task
Properties(执行进程任务属性)窗口
在这种情况下,dtsrun
实用工具的实际命令行为:
DTSRUN /F "C:\olapprocess.dts" /L "c:\olapprocess.log"
/A "OLAPDBName":"8"="FoodMart 2000"
/A "CubeName":"8"="Sales" /A "PartitionName":"8"="Sales 1997"
/A "ServerName":"8"="Localhost"
/A "FactTable":"8"="sales_fact_1997"
/A "ProcessOption":"8"="0"
需要注意的是,此命令行包含了指定以下情况的全局变量,即要执行该处理的分析服务器、OLAP
服务器上的 OLAP 数据库、OLAP 数据库中的 OLAP
多维数据集、多维数据集的分区、分区的事实数据表以及处理选项。“/A”命令行参数提供了执行封装的
OLAP 处理包所需的全局变量名称-类型-值对。下图显示了这些命令行元素、包以及基本任务之间的相互关系。
图 11:
通过使用 DTSRUN
执行带有说明的包,可以满足 Analysis Services
processing 任务和 Execute Package
任务的单线程要求。这是因为执行“执行进程”任务的主
DTS 包创建了一个独立的 Win32
进程空间,具有其自己的线程。在这个独立的 Win32
进程空间中,将执行整个 OLAP
处理包。此方法允许通过由多个“执行进程”任务实例化的新的
DTS 包进程空间同时处理多个来自相同根 DTS 包的 OLAP
分区或对象。
为分析服务处理任务编程
Analysis Services processing
任务提供了允许在包的执行过程中动态配置此任务的属性。可以修改以下属性:
DataSource
在 OLAP
数据库中定义的关系数据源,用于被处理的对象。
FactTable
关系事实数据表,它是多维数据集或分区的源。
Filter
处理 OLAP 对象时应用的过滤器。
IncrementallyUpdateDimensions
处理 OLAP
多维数据集对象或分区对象时,是否要以增量的方式处理维度。可接受的值有为
1 = 是,或 0 = 否。
ItemType
正在处理的 OLAP
对象的类型。可接受的值为 1 = OLAP 数据库、4 =
多维数据集、7 = 分区和 9 = 维度。
ProcessingOption
处理指定的 OLAP
对象的方式。可接受的值为 0 = 完全处理、1 =
刷新数据和 2 = 增量更新。
TreeKey
正在处理的 OLAP
服务器、数据库和对象的层次结构。
通过对各种属性进行定义,可以使用
ActiveX
脚本来设置全局变量。然后,动态属性任务又将引用这些全局变量以配置分析服务处理任务。下面是一个
ActiveX 脚本示例,用于建立接受的 TreeKey
格式(用于处理分区)。
Dim strTreeKey ' 要处理的 OLAP 对象的层次结构
Dim strServerName ' 由全局变量指定的 OLAP 服务器
Dim strOLAPDBName ' 由全局变量指定的 OLAP 数据库
Dim strCubeName ' 由全局变量指定的 OLAP 多维数据集名称
Dim strPartition ' 由全局变量指定的 OLAP 分区名称
strServerName = DTSGlobalVariables("ServerName").Value
strOLAPDBName = DTSGlobalVariables("OLAPDBName").Value
strCubeName = DTSGlobalVariables("CubeName").Value
strPartitionName = DTSGlobalVariables("PartitionName").Value
' 建立分区的 TreeKey 层次结构
' ServerName\DBName
strTreeKey = strServerName & "\" & strOLAPDBName & _
"\CubeFolder\" & strCubeName & "\" & strPartitionName
' 将 TreeKey 放在全局变量中
DTSGlobalVariables("TreeKey").Value = strTreeKey
上述脚本(在 ActiveX Script
任务中执行)将后跟一个 Dynamic Properties
任务,以便将全局变量“TreeKey”指定给 Analysis
Services Processing 任务的 TreeKey 属性。然后,Analysis
Services Processing
任务将使用此属性来处理对象,作为前面提到的封装的
OLAPProcess 包的一部分。
使用
DTS 执行分析服务处理的指导原则
虽然有多种方式可以将分析服务与 DTS
相集成,但是为充分利用该处理方法,还是要遵循一些非常特定的指导原则。
- 当该处理不是主要目的时,应当使用
DSO。除分区管理外,还可以执行若干其他非处理功能,其中包括:创建本地多维数据集、建立安全性角色、更改用于连接的数据源和用于维度及分区的表源。
- 只要有可能,就应当使用常用的 Analysis
Services Processing 任务。使用其他方法(如 DSO)来重复实现分析服务处理的功能不是一个理想方法。
- 不要忽略操纵 Analysis Services
Processing 任务的属性的作用。
- 使用 Dynamic Properties
任务和全局变量来操纵 Analysis Services Processing
任务。否则,如果通过 DTSPackage
对象模型以编程方式操纵该任务,将要求由序号值来引用的任务的
properties() 集合。
- 更改 Dynamic Properties
任务的工作流属性以便在主线程中执行,否则将导致
EXCEPTION 消息。
- 如果不能确定如何编写 Analysis
Services Processing 任务,请查看 DTS 的“断开的编辑”功能,以帮助了解该任务的对象模型。
- 要在分区处理中实现并行性,应当采用一个优化的多维数据集设计,以便在提取数据时不会对关系数据存储执行装载操作。
遵循这些指导原则将有助于确保获得成功的执行分析服务处理的方法。
实践总结
优点
通过 DTS
管理分析服务平台是一种最佳实践,它具有许多优点。其中最显著的优点包括:
可重复的扩展性
该实践将 ETL 处理扩展到 BI
体系结构的表示层,这提供了一种可重复的方法,使得在装载关系数据存储之后,可以直接获得可供使用的有意义的、及时的业务分析信息。
成功获得自治性
以编程方式处理多维数据集、分区和维度为实现一种独立的方法建立了基础,该方法将装载不具有交叉数据集市或
BI 解决方案相关性的数据集市。
无错误的自动化
本实践中介绍的方法显示了将复杂的进程自动化的可能性。如果依赖于管理分析服务的其他方法,管理用户会将更多风险带进进程,这种风险将超过大多数用户在生产系统中的可接受程度。
注意事项
此实践及方法的注意事项虽然不多,但还是有一些。主要的注意事项包括:
增强的可用性
为通过自动化的分析服务进程迅速提供
BI
解决方案中的分析数据,强烈要求在关系数据存储的装载中创建正确的过程,以确保不会向解决方案的
OLAP 表示元素传递不需要的数据。
审核和错误处理实践
内部审核方法
在 DTS
中,存在许多审核包以及处理发生的错误的方式。第一个要讨论的方式是使用
DTS
中内置的内部(或固有的)审核和错误处理功能。这些是最简单、快捷的实现方式,它们为大多数项目提供了足够的审核功能。
包中的日志选项
DTS 可以自动将包级别的事件记录到 SQL
Server
数据库中,而不必将包保存到知识库中(在早期版本中,这是必需的)。BI
开发人员可以使用 DTS
包审核来检查任何包,以确定其是成功还是失败。通过检查存储在
MSDB 数据库中的 sysdtspackagelog 和 sysdtssteplog
表中的日志信息,还可以检查这些包中哪些任务已成功执行、哪些任务已失败以及哪些任务未执行。每次执行包时,将向日志表中添加一组新记录,这样便可以记录该包的整个历史记录。此外,只要启用了日志,该包每个版本的执行历史记录也将保留在这些表中。
要启用日志,应当打开包的 Package
Properties(包属性)对话框并选择 Logging(日志记录)选项卡。
图 12:DTS Package Properties(DTS
包属性)窗口的 Logging(日志记录)选项卡
关键的一点是要确保根据包的执行计划正确设置身份验证。如果计划使用
SQLAgent 来执行包,SQLAgent 身份验证必须对 MSDB
数据库具有足够的读/写权限。否则,可能会使用 SQL
Server 身份验证来记录错误。
将数据沿袭用于日志
沿袭信息提供了一种确定数据源的方式,是关系架构中的表和列还是导致数据被记录的包。通过数据沿袭,我们可以对架构中的数据元素(也称为元数据)的定义以及它们在包中的使用情况具有特别的认识。通过记录此沿袭信息(作为变量提供的),我们可以准确地跟踪架构元素是如何通过
DTS
包、数据提取器、转换和装载传输到目标的。使用此类信息可以查看因更改包的体系结构而产生的数据问题。
两种形式的沿袭
在 DTS
中,有两种明显不同的沿袭形式。DTS
通过在转换时从 OLE DB
兼容数据源扫描架构信息,实现列级别的沿袭(也称为目录元数据)。目录信息将保持在
SQL Server 和元数据服务中以便以后引用。
DTS
中的第二种沿袭形式是包执行;每次执行包时,如果选择了该功能选项,DTS
将为可用于任务的特定包创建沿袭标识符。
全局沿袭变量
与包执行有关的沿袭变量可用于 DTS
包的全局变量中。通过选择 Package Properties(包属性)对话框的
Advanced(高级)选项卡并选择 Show lineage variables as
source columns(将沿袭变量显示为源列)选项就可以使用这些全局变量。全局变量可以被命名为
DTSLineage_Short(包沿袭的整数表示)和 DTSLineage_Full(包沿袭或当前执行的
GUID [全局唯一标识符] 表示)。
注意:要利用这些全局变量,DTS
包不需要存储在元数据服务中。
沿袭和审核
沿袭信息是在记录包的执行以及执行步骤时捕获的。此信息保持在
MSDB 数据库的系统表中。在审核表(sysdtspackagelog 和
sysdtssteplog)中,此信息显示为 GUID 和整数。
在转换中包含日志信息
如果启用了 Show Lineage variables as source
columns(将沿袭变量显示为源列),则创建数据转换任务时,DTSLineage_Full
和 DTSLineage_Short
将显示为源列。可以将这些变量指定为数据列并随其他列一起插入或更新,以便跟踪每行的数据沿袭。下图显示了如何以这种方式使用日志全局变量。
图 13:Transform Data Task
Properties(转换数据任务属性)窗口
内部错误处理方法
DTS
具有处理和记录包中的错误的内置功能。在包的执行过程中发生的任何错误都可以传递给事件日志或某个外部文本文件以便进行记录。在事件日志中,任何失败都被记录为一个
DataTransformationServices 源并且其类型为 Error。下图显示了一个错误事件示例。
图 14:Event Properties(事件属性)窗口
在 Description(说明)文本框中,可以收集大量相关信息:该错误何时、何地以及为何发生。需要注意的是,DTS
使用了发生错误的步骤的内部名称(例如,
DTSStep_DTSActiveScriptTask_1),而不是开发人员可能为该任务提供的英文说明。
通过使用事件日志来记录错误,外部应用程序可以监视这些错误类型的日志并警告相应的支持人员。如果可以通过编程解决错误,也可以使用日志条目来执行更正操作。
Fail
Package on First Error
默认情况下,不管遇到任何错误,DTS
包将始终完成并显示为成功状态。但是,如果在任何步骤中遇到错误并且没有明确处理,DTS
也可以停止包的执行。可以使用 Fail Package on First
Error 设置来操纵此行为。
要启用包级别的错误处理,应打开要审核的包,查看
Package Properties(包属性)对话框并在 Logging(日志)选项卡上选中
Fail package on first error(第一次出错时使包失败)复选框。这样,当发生错误时,DTS
将返回一个失败返回代码。这样操作将调用外部调用应用程序(如
SQLAgent),或者一个 MS DOS
批处理文件,以便在包失败时进行注册和执行某些其他操作。它还允许
Execute Package
任务在子包的执行过程中处理子包的失败。
步骤失败时使包失败
在某些情况下,如果一个步骤或仅选定的几个步骤失败,开发人员可能希望使整个包失败,但是如果其他步骤失败,则继续进行。例如,如果某个步骤要删除源提取文件,但该文件不存在,该步骤将失败,但开发人员可能希望包继续执行。在这种情况下,开发人员可以明确地设置当其失败时将触发整个包失败的步骤。
要启用基于步骤的错误处理,应右击该任务,选择
Workflow(工作流)菜单,然后选择 Workflow Properties(工作流属性),打开
Options(选项)选项卡,然后选中 Fail package on step
failure(步骤失败时使包失败)复选框,如下所示。
图 15:Workflow Properties(工作流属性)窗口
这两种处理错误的方法都是 DTS
的一部分;如果某个步骤失败,这两种处理都将退出包。但在许多情况下,BI
解决方案可能希望对该情况进行修复并继续处理。这时便需要使用工作流管理来处理错误。
工作流错误处理
DTS 具有三种类型的内置工作流: On
Completion、On Success 和 On Failure。
- On Completion
“On Completion”工作流确保执行后续任务,而不管前面任务的结果如何。这意味着错误将被忽略,并且未被处理。在某些情况下,这可能是适当的工作流;但在多数情况下,BI
解决方案可能希望更明确地处理错误。
- On Failure
当某个任务由于任何原因失败时,将执行“On
Failure”工作流。这时既不能设置 Fail package on
first error 选项,也不能设置 Fail package on step
failure
选项。如果设置了任一选项,它们将优先于包中的任何进一步操作,从而永远也不会执行
On Failure 任务。
选择正确的组合
DTS 的强大功能之一是可以使用 ActiveX
Script 任务操纵其对象模型。因为 BI
解决方案中的许多 DTS
包最初是从外部应用程序中调用的,所以每个包都应当返回正确的返回代码。要管理这一情况,同时仍然保留对日志和处理的一定控制,唯一的方式就是利用对象模型。
如果解决方案设计使用错误处理任务管理其他任务中的错误,但是希望强制
DTS 包返回一个错误代码,则可以向错误处理 ActiveX
Script 任务中添加以下代码来实现这一目的。
Set oPackage = DTSGlobalVariables.Parent
oPackage.FailOnError = True
...
Main = DTSTaskExecResult_Failure
请注意,ActiveX 脚本的 Main
入口函数的结果被设置为返回失败。如果包的
FailOnError 属性被设置为 True,该包将在此任务完成时结束并相应地将状态返回给它的调用程序。使用此方法的缺点在于,所有日志都将反映有两个错误导致了包失败,即出现错误的处理函数及其前面的任务。
添加此代码逻辑将使整个解决方案能够结合两种最有用的方法来处理错误。
内部错误处理的指导原则
虽然有多种方法可以管理在包执行过程中发生的错误,但是为充分利用所提供的方法,还是要遵循一些非常特定的指导原则。
- 使用外部错误处理程序任务或存储过程,以便最小化现有包的更改并使代码和任务模块化。
- 通过为每个主要工作任务设计单独的错误处理任务,使用单独的工作流来管理“失败时”工作流。
- 确保错误处理任务返回
DTSTaskExecResult_Failure,并且如果包是从 SQLAgent、DOS
批处理文件或其他要求返回代码的应用程序中调用的,则确保将包设置为在第一次出现错误时失败。
遵循这些指导原则将有助于确保获得成功的管理错误处理的方法。
使用自定义方法
自定义审核方法
如果包体系结构要求为审核或错误处理执行更复杂的任务系列,则
DTS
的灵活性允许使用自定义的功能来代替其内部功能。这一部分的最佳实践将讨论如何自定义
DTS 包以利用 DTS 的日志可扩展性。
将包作为作业的一部分来审核
DTS 内部脚本对象 -
DTSPackageLog
如果内部审核不足以完成任务,DTS
还提供了用于日志的内部脚本对象 DTSPackageLog。此对象可以在与
DTSGlobalVariables
脚本对象相同的上下文中使用。有两种方法可用于向日志目标写入信息:
WriteStringToLog 和 WriteTaskRecord。这两种方法都可用来向日志中添加附加信息,使用
WriteTaskRecord
可以向日志中添加错误代码。下面给出了如何使用此脚本对象以及这些方法的示例。
Function Main()
...
Set oFS = CreateObject("Scripting.FileSystemObject")
DTSPackageLog.WriteStringToLog "Opening File"
Set oTS = oFS.OpenTextFile("C:\FileNotFound.TXT", 1)
If (Err.Number <> 0) OR (oTS Is Nothing) Then
DTSPackageLog.WriteTaskRecord Err.Number, Err.Description
Main = DTSTaskExecResult_Failure
End If
End Function
此示例说明了 BI
解决方案开发人员如何能够实现丰富的环境,以便将信息和错误记录到
DTS 日志目标中。
实践总结
优点
通过 DTS
管理审核和错误处理可以根据所需的功能级别和覆盖范围选择多种实现方式。
实现审核和错误处理的一些主要优点包括:
- 使用审核检查和平衡的数据验证功能
- 更精确地控制错误处理以及 DTS
如何响应错误
- 在日志中捕获了丰富的信息,使错误更容易解决
- 使用和捕获数据沿袭信息,以便可以追溯每条数据的源
- DTS
可以将事件和错误记录到适当位置,以便可以通过外部应用程序(如
SQLAgent、事件日志等)进行错误管理
注意事项
需要注意的主要事项是,使用外部组件处理审核和日志时,将增加整个解决方案的复杂性。请记住要完整记录组件和代码,以便其他开发人员能够参考并解决问题。
增强
DTS 功能实践
使用 VB .NET
方法开发自定义任务
使用自定义任务支持的可扩展性
虽然许多人会说,作为一个产品,在使用
DTS 开发 BI 解决方案的过程中,DTS
已经提供了大量灵活和现成的功能,但是有时仍然需要对其功能进行扩展。而扩展
DTS
的方法之一就是开发自定义任务。通过自定义任务可以将核心逻辑封装到由属性和执行方法组成的单个
DLL 中。然后,可以像任何其他任务一样,在 DTS
包环境中注册和使用此逻辑。实际上,前面介绍的 Analysis
Services Processing
任务就是一个自定义任务的很好的示例,它可以添加到
DTS 设计环境中以获得扩展的功能。
所需的 DTS COM 界面
CustomTask 界面
DTS
通过以下方式来支持这种可扩展性,即要求自定义任务开发人员在数据转换服务包对象模型中实现
DTS.CustomTask COM 界面。该界面提供了主要的 Execute()
方法,包环境可以在运行时调用该方法,以便将任务包括在包的执行工作流中。下面给出了该界面的图示。
图 16:CustomTask COM 界面
在 CustomTask
界面的实现中所做的主要修改是将大多数核心逻辑放在
Execute()
方法中,同时向任务添加扩展的、由开发人员定义的属性。这些属性通常用来获得其他逻辑。
CustomTaskUI 界面
要为任务的属性页面提供一个自定义用户界面,需要实现
DTS.CustomTaskUI COM
界面。这将允许开发人员创建丰富的用户界面,以支持自定义任务的属性定义。
PersistPropertyBag 界面
为提供 VB .NET
自定义任务,所要讨论的最后一个界面是
DTS.PersistPropertyBag COM 界面。PersistPropertyBag
界面提供的界面供 DTS
用来执行相应的操作,即从基本包结构装载任务的属性以及将任务的属性保存在基本包结构中。
注意:在编写本白皮书之时,对于自定义任务的
VB .NET
实现,要求使用所有这三种界面以便提供一个集成在
BI 解决方案的 DTS 部分中的综合的自定义任务。
VB .NET 和组件对象模型 (COM)
的互操作性
要了解开发 VB .NET
自定义任务的细微差别,一个关键因素是要了解 COM
与 .NET Framework
的公共语言运行库之间的互操作性层。为了让 VB .NET
类实现指定的 DTS COM
界面,必须通过创建一个运行时可调用包装 (RCW)
来定义此互操作性层,以使 VB .NET 能够将 COM
界面作为 .NET 程序集使用。DTS(COM
服务器集合)使用自定义任务的 VB .NET
程序集也是如此。必须为 DTS 建立一个 COM
可调用包装 (CCW),以使 DTS 能够与 VB .NET
程序集进行交互。下图显示了这两个平台之间的互操作性。
图 17:COM
和运行时可调用包装
基本的 .NET 实用工具
以下 Visual Studio .NET
实用工具将用来创建上述包装、注册产生的 DLL、使之成为
.NET 中的程序集以及将其放在 .NET
全局程序集缓存中。
类型库导入器 (TLBIMP.exe) -
用于导入类型库(COM
界面的描述符)并创建运行时可调用包装 (RCW)。运行时可调用包装输出为
DLL,可以在 VB .NET 项目中注册。
严格名称工具 (SN.exe) -
提供了一个严格名称密钥 (.SNK)
文件,用在项目的程序集中。
程序集注册工具 (REGASM.exe) -
用于在注册表中注册 .NET 程序集,以便 COM
组件能够利用该程序集提供的界面。
全局程序集缓存工具 (GACUTIL.exe) -
用于在全局程序集缓存中注册包装,以完成 DTS 与 VB
.NET 自定义任务之间互操作性的最后一步。
在编写 VB .NET
自定义任务代码的过程中要用到所有这些工具。
为 VB .NET
自定义任务编写代码
为此最佳实践开发的自定义任务示例是一个错误处理程序自定义任务。此任务将检索上一步骤中的错误信息(由
OnFailure
工作流限制定义)并将其记录到一个文本日志文件和/或
Windows 事件日志中,如下面的包图示所示。
图 18:
为更好地解释生成 VB .NET
自定义任务的方法,本节介绍了所有相关概念并提供了用于开发自定义任务代码的部分代码示例。在附录
B 中可以找到整个 .NET
错误处理程序自定义任务代码的完整列表。
入门
在最初创建 Visual Basic .NET
类项目后,首先要执行的任务之一就是设置用于自定义任务的
.NET 托管代码和 COM
对象之间的互操作性。第一步是创建要包含在程序集中的严格名称密钥文件
(.SNK)。假设自定义任务代码在目录 C:\dotNetErrorHandler\
中,则应当运行以下命令行:
"<your VS.NET Folder>\FrameworkSDK\Bin\sn.exe" -k
"C:\dotNetErrorHandler\dotNetErrorHandler.snk"
创建严格名称密钥文件后,应当在程序集本身的代码(可在
AssemblyInfo.vb
中找到)中添加一个密钥文件属性。要添加的一行代码与以下代码类似:
' 注意,要使 CustomTask 能够工作,必须生成并引用一个 KeyFile
<Assembly : AssemblyKeyFile(".\dotNetErrorHandler.snk")>
下一步是创建运行时可调用包装 (RCW)
并在项目中添加对它的引用。使用类型库导入器创建
RCW 的命令如下所示:
"<VS .NET Folder>\FrameworkSDK\Bin\tlbimp.exe" "C:\Program Files\Microsoft
SQL Server\80\Tools\Binn\dtspkg.dll"
/keyfile:"C:\dotNetErrorHandler\dotNetErrorHandler.snk" /out:
"C:\dotNetErrorHandler\Interop.DTS.dll"
此命令将为在该项目中使用 DTS 包对象
COM 库构造 RCW,具体地说,就是 Interop.DTS.dll。
完成这些预备步骤后,便可以建立对
RCW 的引用,如下所示:
图 19:(单击以查看大图像)
实现正确的界面
此最佳实践的前面部分已经指出,必须要由自定义任务类实现三个界面。该类(称为
EHTask)首先给出实现声明。
Public Class EHTask
' 您必须实现 CustomTask、
' CustomTaskUI 和 PersistPropertyBag 界面
Implements Interop.DTS.CustomTask
Implements Interop.DTS.CustomTaskUI
Implements Interop.DTS.PersistPropertyBag
这三个 Implements
语句设定了任务开发的基础,其中包括要实现每个界面的所有必需的属性和方法。在继续该步骤之前,最好创建一些私有变量来存储该任务的公有属性的值。以下是该
EHTask 类的变量声明部分。
' 创建用于存储标准属性的私有变量
Private _Name As String
Private _Description As String
' 创建对任务对象的私有引用
Private _TaskObject As Interop.DTS.Task
' 创建用于存储扩展的属性的其他私有变量
Private _LogFileName As String
Private _LogToEventLog As Boolean
Private _LogToFile As Boolean
Private _HandledStepName As String
Private _UseGlobals As Boolean
' 为 UI 设置友元变量,用于报告它是否被取消
Friend bolUICancelled As Boolean
现在可以开始实现界面的任务了。要计算的第一个界面是
DTS.CustomTask 界面。
DTS.CustomTask 界面
DTS.CustomTask 界面用于实现标准属性(Name
和 Description)、执行方法和一个属性提供程序,以便获得自定义任务对象的属性集合。第一个重要部分是用于支持标准属性的代码。
Public Property Description() As String _
Implements Interop.DTS.CustomTask.Description
Get
Return _Description
End Get
Set(ByVal Value As String)
_Description = Value
End Set
End Property
Public Property Name() As String Implements Interop.DTS.CustomTask.Name
Get
Return _Name
End Get
Set(ByVal Value As String)
_Name = Value
End Set
End Property
该 .NET
错误处理程序自定义任务所需的其他属性为:
- HandledStepName -
自定义任务通过 GetExecutionErrorInfo()
方法询问错误信息时所执行步骤的内部名称。
- LogToFile -
一个布尔值,用于指示是否将捕获的错误信息记录到文件中。
- LogFileName -
用于存储该信息的日志文件的名称和完整路径。
- LogToEventLog -
一个布尔值,用于指示是否将捕获的错误信息记录到
Windows 应用程序事件日志中。
- UseGlobals -
一个布尔值,用于指示是否要从包的全局变量中获取
LogToFile、LogFileName 和 LogToEventLog
属性的值。如果 DTS
包中有多个错误处理程序(这很有可能),则此属性将确保所有
.NET 错误处理程序任务都使用相同的设置。
在标准属性(Description 和 Name)和单个自定义任务的属性域之后是标准的
Execute()
方法。下面列出了该方法的完整代码,随后又解释了其中调用的流和子例程。
Public Sub Execute(ByVal pPackage As Object, _
ByVal pPackageEvents As Object, _
ByVal pPackageLog As Object, _
ByRef pTaskResult As Interop.DTS.DTSTaskExecResult) _
Implements Interop.DTS.CustomTask.Execute
Dim objPackage As Interop.DTS.Package
Dim objTasks As Interop.DTS.Tasks
Dim objStep As Interop.DTS.Step
Dim objPackageLog As Interop.DTS.PackageLog
Dim strSource As String
Dim lngNumber As Long
Dim strDescription As String
Dim strErrorString As String
Try
' 将传递的参数转换为
' 本地更严格的变量类型
objPackage = pPackage
objPackageLog = pPackageLog
' 确定是否有一个优先步骤
Call EstablishPrecedence(objPackage)
' 检查 HandledStepName 并引发异常
If Me.HandledStepName = "" Then
Throw New System.Exception( "Task cannot execute due to
no Precedence Constraint being defined.")
End If
' 需要检查任务是否被定义为由全局变量驱动
'
Call ReadFromGlobalVariables(objPackage)
' 获取一个 Step 对象引用
objStep = objPackage.Steps.Item(Me.HandledStepName)
' 检索错误信息
Call objStep.GetExecutionErrorInfo(lngNumber, strSource, _
strDescription)
' 生成由竖线分隔的错误字符串
strErrorString = Format(Now, "MM/dd/yyyy hh:mm:ss tt") & "|" & _
objPackage.Name & "|" & objStep.Name & "|" & _
" The Step, " & objStep.Description & _
" failed with the following error: " & _
lngNumber.ToString() & " - " & strSource & " - " &
strDescription
' 确定是否要将错误写入日志文件
If Me.LogToFile And Trim(Me.LogFileName) = "" Then
' 由于缺少文件名而引发异常
Throw New System.Exception("Task cannot log to file due to
no log file being defined.")
ElseIf Me.LogToFile And Trim(Me.LogFileName) <> "" Then
' 写入日志文件
Dim objFile As System.IO.File
Dim objStreamWriter As System.IO.StreamWriter
objStreamWriter = objFile.AppendText(Me.LogFileName)
' 关闭 StreamWriter 并清除文件对象
objStreamWriter.WriteLine(strErrorString)
objStreamWriter.Close()
objStreamWriter = Nothing
objFile = Nothing
End If
' 确定是否要写入事件日志
If Me.LogToEventLog Then
Dim objEventLog As New EventLog()
objEventLog.WriteEntry(".Net DTS Error Handler", _
strErrorString)
objEventLog = Nothing
End If
' 如果执行成功,则返回 Success
pTaskResult = Interop.DTS.DTSTaskExecResult.DTSTaskExecResult_Success
Catch ex As Exception
Throw New System.Exception("An Error occured during the tasks
execution.", ex)
' 如果执行失败,则返回 Failure
pTaskResult = Interop.DTS.DTSTaskExecResult.DTSTaskExecResult_Failure
End Try
End Sub
Execute()
方法按照以下顺序处理执行过程:
- 通过 EstablishPrecedence
方法确定是否存在一个优先步骤。
- 基于子例程 ReadFromGlobalVariables()
中的 UseGlobals
设置,使用全局变量计算并填充其余任务属性。
- 捕获由 HandledStepName
属性中的名称定义的 Step 对象的错误信息。
- 将这些信息写入目标日志文件和/或目标事件日志。
还剩下唯一一个方法需要实现,即用于访问自定义任务的属性集合的
Properties 属性方法。
在该 .NET
错误处理程序示例中,我们通过实现 PropertiesProvider
来填充属性集合。此提供程序通过调用 GetPropertiesForObject
方法来填充属性集合。
Public ReadOnly Property Properties() As Interop.DTS.Properties _
Implements Interop.DTS.CustomTask.Properties
Get
' 为 Properties 集合
' 和 ProviderClass 声明变量,以便检索属性信息
Dim objProperties As Interop.DTS.Properties
Dim objPropProvider As _
Interop.DTS.PropertiesProviderClass = _
New Interop.DTS.PropertiesProviderClass()
' 使用对现有
' 任务对象的引用,获取对要传递到属性提供程序的
' 任务对象的引用
objProperties = objPropProvider.GetPropertiesForObject(_TaskObject)
' 对属性提供程序执行某些清除操作
objPropProvider = Nothing
' 返回属性集合的值
Return objProperties
End Get
End Property
The next interface, required for a custom task written with VB .NET
is the CustomTaskUI interface.
DTS.CustomTaskUI 界面
DTS.CustomTaskUI
界面用于实现一组标准方法,处理由 DTS
设计器发出的特定请求。为在 .NET
错误处理程序示例中成功实现该界面,必须以占位方式包含所有方法,只有
Initialize、Edit、New(作为别名方法)和 Delete
方法包含实际的逻辑。下面列出了这些填充的方法并给出了必要的注释。
Public Sub Initialize(ByVal pTask As Interop.DTS.Task) _
Implements Interop.DTS.CustomTaskUI.Initialize
_TaskObject = pTask
End Sub
Initialize 方法以 pTask
参数变量的形式为自定义任务提供了非常重要的信息,如上所示。pTask
参数将自定义任务标识为一个对象,以便将来在
DTS 包对象层次结构中引用。下一个方法是 Edit
方法。只要开发人员启动任务的属性对话框,DTS
设计器就会调用此方法。
Public Sub Edit(ByVal hwndParent As Integer) _
Implements Interop.DTS.CustomTaskUI.Edit
' 此处应当显示一个 UI
ShowPropertyUI()
End Sub
Public Sub Delete(ByVal hwndParent As Integer) _
Implements Interop.DTS.CustomTaskUI.Delete
_TaskObject = Nothing
End Sub
Delete
方法为任务提供了清除所有内部引用(例如,最初在
Initialize 方法中捕获的 _TaskObject
引用)的机会。在上述示例中,_TaskObject 引用被清除,被丢弃的对象将转移至公共语言运行库内存回收器。
由于语言限制,必须为 New
方法的实现提供一个别名。在下面的示例中,使用了
New2 作为 New 方法的别名。
Public Sub New2(ByVal hwndParent As Integer) _
Implements Interop.DTS.CustomTaskUI.New
' 预设任务的设置
Me.LogToEventLog = True
Me.LogToFile = True
Me.LogFileName = ""
Me.UseGlobals = True
' 此处应当显示一个 UI
ShowPropertyUI()
End Sub
在 New
方法中,任务的属性被预设,并且首次向用户显示用户界面。下面是自定义任务的
UI 元素。
图 20:.NET Error Handler(.NET
错误处理程序)
每次调用任务的属性页面并且未找到优先步骤时都会向用户显示以上对话框。
图 21:.NET Error Handler Task
Properties(.NET 错误处理程序任务属性)
上图中显示了存储的属性。如果具有一个优先步骤,任务的说明将发生变化。同样,如果全局变量(由此任务的第一个实例创建的)被修改,并且选中了
Use Global Variable(使用全局变量)选项,则其他属性可能会发生变化。
图 22:通知全局变量已被更新
任何时候关闭该自定义任务的 Properties(属性)对话框时,上面的对话框都会通知用户全局变量已更新。
DTS.PersistPropertyBag 界面
DTS.PersistPropertyBag
界面对于包中存储的属性来说是一个重要界面。在处理
DTS.CustomTask
属性集合未显示的属性时尤其如此。在该 .NET
错误处理程序自定义任务中就是这种情况。然而,通过实现
PersistPropertyBag
界面,可以保存和加载为自定义任务定义的所有属性。下面列出了实现
PersistPropertyBag 界面所需的两种方法。
Public Sub Load(ByVal PropertyBag As Interop.DTS.PropertyBag) _
Implements Interop.DTS.PersistPropertyBag.Load
' 创建用于将 PropertyBag 中的信息读入类级别变量中的
' 所有 PropertyBag 读取器
_Name = PropertyBag.Read("Name")
_Description = PropertyBag.Read("Description")
_LogFileName = PropertyBag.Read("LogFileName")
_LogToEventLog = PropertyBag.Read("LogToEventLog")
_LogToFile = PropertyBag.Read("LogToFile")
_HandledStepName = PropertyBag.Read("HandledStepName")
_UseGlobals = PropertyBag.Read("UseGlobals")
End Sub
Public Sub Save(ByVal PropertyBag As Interop.DTS.PropertyBag) _
Implements Interop.DTS.PersistPropertyBag.Save
' 创建用于将类级别变量中的信息持久保持在 PropertyBag 中的
' 所有 PropertyBag 写入器
PropertyBag.Write("Name", _Name)
PropertyBag.Write("Description", _Description)
PropertyBag.Write("LogFileName", _LogFileName)
PropertyBag.Write("LogToEventLog", _LogToEventLog)
PropertyBag.Write("LogToFile", _LogToFile)
PropertyBag.Write("HandledStepName", _HandledStepName)
PropertyBag.Write("UseGlobals", _UseGlobals)
End Sub
注册 VB .NET 自定义任务
遗憾的是,在编写本白皮书之时,尚无法使用
DTS 设计环境用户界面来注册 VB . NET
自定义任务。下面的方法需要编辑注册表,这是使
VB .NET CustomTask 能够在 DTS
中可用的一个复杂且必要的步骤。
- 如前一节和附录 B 中所述使用 VB .NET
开发自定义任务。
- 生成 .NET
自定义任务解决方案的程序集。
- 使用程序集注册工具为生成的 .NET
DLL(与 Regsvr32.exe 类似)注册程序集。
例如:
C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\RegAsm.exe
"C:\dotNetErrorHandler\bin\dotNetErrorHandler.dll"
- 使用程序集注册工具为提供 .NET 和
COM 互操作性(为 DTS
包对象库)的运行时可调用包装注册程序集。
例如:
C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\RegAsm.exe
"C:\dotNetErrorHandler\Interop.DTS.dll"
- 使用全局程序集缓存工具将在步骤 3
和 4 中注册的程序集添加到 .NET 全局程序集缓存
(GAC) 中。
例如:
"C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin\gacutil.exe"
/i "C:\ dotNetErrorHandler \bin\ dotNetErrorHandler.dll"
and then
"C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin\gacutil.exe"
/i "C:\ dotNetErrorHandler \Interop.DTS.dll"
注意:每次重新生成解决方案时,都必须运行用于将
.NET 程序集放到 GAC 中的第一个命令。
- 现在必须编辑注册表,因为当前还无法通过
DTS 用户界面注册 .NET CustomTask。需要指出的是,不熟悉注册表编辑的用户请不要尝试此操作,因为这可能会获得意外的结果。
编辑注册表之前需要了解以下项目:
建立以上所有项目后,便可以准备创建注册表条目文件来更新注册表。此方法的注册表条目文件模板为:
Windows Registry Editor Version 5.00
HKEY_CLASSES_ROOT\CLSID\{<The GUID>}]
@="<The Prog ID>"
"AppID"="{<The GUID>}"
"DTSIconFile"="<Path to the DLL>"
"DTSIconIndex"=dword:00000000
"DTSTaskDescription"="<The Task Description>"
[HKEY_CLASSES_ROOT\CLSID\{<The GUID>}\Implemented Categories]
[HKEY_CLASSES_ROOT\CLSID\{<The GUID>}\Implemented Categories\
{10020200-EB1C-11CF-AE6E-00AA004A34D5}]
[HKEY_CLASSES_ROOT\CLSID\{<The GUID>}\Implemented Categories\
{40FC6ED5-2438-11CF-A3DB-080036F12502}]
[HKEY_CLASSES_ROOT\CLSID\{<The GUID>}\InprocServer32]
@="<Path to the DLL>"
"ThreadingModel"="Both"
[HKEY_CLASSES_ROOT\CLSID\{<The GUID>}\ProgID]
@="<The PROGID>"
[HKEY_CLASSES_ROOT\CLSID\{<The GUID>}\Programmable]
[HKEY_CURRENT_USER\Software\Microsoft\Microsoft SQL Server
\80\DTS\Enumeration\Tasks\<The GUID>]
@="<The PROGID>"
下面是该自定义任务示例的实际注册表条目文件的内容。可以将这些内容复制到文本编辑器中,然后将它们保存到
TaskReg.reg 或其他适当的 .reg 文件中。
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}]
@="dotNetErrorHandler.EHTask"
"AppID"="{EF4AC3E4-3D72-4117-AB91-8B417826792F}"
"DTSIconFile"="C:\\dotNetErrorHandler\\ErrorHandler.ICO"
"DTSIconIndex"=dword:00000000
"DTSTaskDescription"=".NET Error Handler"
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}
\Implemented Categories]
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}
\Implemented Categories\{10020200-EB1C-11CF-AE6E-00AA004A34D5}]
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}
\Implemented Categories\{40FC6ED5-2438-11CF-A3DB-080036F12502}]
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}
\InprocServer32]
@="C:\dotNetErrorHandler\bin\dotNetErrorHandler.dll"
"ThreadingModel"="Both"
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}\ProgID]
@="dotNetErrorHandler.EHTask"
[HKEY_CLASSES_ROOT\CLSID\{EF4AC3E4-3D72-4117-AB91-8B417826792F}
\Programmable]
[HKEY_CURRENT_USER\Software\Microsoft\Microsoft SQL Server
\80\DTS\Enumeration\Tasks\EF4AC3E4-3D72-4117-AB91-8B417826792F]
@="dotNetErrorHandler.EHTask"
- 打开 DTS
设计器。现在,自定义任务应当显示为任务工具栏中的一个注册的条目。
开发和部署自定义任务的指导原则
在 VB .NET 中开发自定义任务的方法与
Visual Basic 6.0
中的方法并无太大差别。不管开发自定义任务的经验如何以及倾向于使用哪种语言,遵循以下指导原则都将有助于获得成功的
VB .NET 错误处理程序自定义任务。
- 花些时间了解一下 VB .NET
的优点。就最佳实践而言,使用 VB .NET
使开发人员能够创建不需要在主线程中执行的自定义任务,因为
VB .NET
解决方案采用了自由线程模式,不会受到线程关系的影响。
- 开发自定义任务时,不要仓促开始。为自定义任务创建一个基壳,使用这里介绍的方法,通过
DTS
对其进行编译和注册。建立基壳后,下一步就是开发和部署更新的功能。
- 考虑使用 Visual Basic 6.0
生成任务。为什么呢?首先,它是一种易于开发自定义任务的很好的语言。其次,由它入手可以很好地了解
COM 和 .NET
之间的差异。最后,这样做可以根据线程的情况确定是否需要使用
.NET。如果任务可以在主线程中运行并且没有问题,那么便并不特别需要迁移到
.NET 自定义任务中。此时通常会优先选择使用
Visual Basic 6.0。
- 使用新的语言和框架时,有时可能需要解决某些问题。在
.NET 错误处理程序示例中,该任务在 DTS.CustomTaskUI.Properties()
方法中实现了属性提供程序。这样做的主要原因是为了绕开以下问题,即该示例没有很好地允许
DTS
设计环境来管理属性。在编写本白皮书之时,尚无法解决这一难题。
- 但是,我们要尽力思考并寻找使用较少代码实现该相同功能的方法。.NET
框架基类的内容非常丰富,提供了一种比单纯使用
COM 和 Win32 API 更好的实现 Win32
功能的方法(注意,它只用三行 .NET 代码就能将
Windows 事件日志添加到自定义任务中!)。
- 通过研究并保存一份可能受到 .NET
自定义任务部署影响的注册表节点的副本,了解有关该部署的更多信息。注册表中的现有任务无疑为成功注册
.NET 自定义任务以便在 DTS
中使用提供了某些需要考虑的必要因素。
遵循这些指导原则可以为开发 VB.NET
自定义任务并使该任务成为整个 BI
解决方案中的成功一环奠定良好的基础。
使 ActiveX 脚本可以重复使用
脚本的本地上下文
在 DTS 中的很多地方都可以使用 ActiveX
脚本来扩展包中的现有任务,甚至创建自定义任务。无论是
ActiveX Script 工作流、ActiveX Script 任务还是
ActiveX Script
转换,这些独特的元素都有一个共同且明显的限制特征,即其中每个环境中的脚本都不能在另一个任务中使用,ActiveX
脚本在任务之间进行通信的唯一方式是通过全局变量。要维护在整个
DTS 包基础结构或 BI
解决方案中重复使用的某个公共脚本函数,如果在本地环境中执行每个任务、每个工作流或每个转换的脚本,则代价是很高的。
入口函数和解析的脚本执行
虽然 ActiveX
脚本的入口函数属性确实是执行 ActiveX 任务、ActiveX
工作流或 ActiveX
转换时要调用的函数,但它不必是要执行的第一个
ActiveX 脚本。脚本的“global”区域中定义的任何脚本都将在入口函数调用之前执行。为说明此执行顺序,下面给出了一个简单的
ActiveX 脚本示例。
图 23:说明此执行顺序的
ActiveX 脚本
上述脚本由两个 msgbox
方法调用组成。一个在 Main()
入口函数之前,另一个在该函数中。其结果是按顺序出现的两个消息对话框。
图 24:由脚本执行获得的消息对话框
还有一点需要注意,即入口函数不必是
Main 函数,如果找到一种用于生成可重复使用的
ActiveX
脚本的方法,也可以使用该替代入口函数名称以获得更好的性能。
ActiveXScript 属性
使 ActiveX
脚本可用的首要步骤之一是了解每个任务、工作流或转换是否具有可在整个包对象层次结构中访问的
ActiveXScript
属性。此属性提供了一个由所有被访问对象的 ActiveX
脚本填充的字符串。
在字符串中包含其他任务的脚本是重复使用
ActiveXScript 的开始。
Execute() 和 eval()
VBScript 和 JScript ActiveX 脚本语言是 DTS
脚本中最常用的语言。其中每种语言都提供了在字符串中包含定义的脚本的运行时方法。
在 VBScript 中,这种支持是由 Execute()
函数提供的。Execute() 函数将包含
VBScript
命令的字符串作为单个参数。字符串中定义的命令和函数包含在运行时环境中,可以像其他任何命令那样进行调用。下面给出了此概念的示例。
图 25:
此示例是在前一个示例的基础上构建的,其中显示了三个消息对话框。第一个对话框是脚本中第一个
msgbox 语句的结果,第二个对话框是 Main()
函数中 msgbox()
调用的结果,第三个对话框是对动态添加的 NewMessage()
函数(通过在 VBScript 中使用 Execute()
函数实现)进行调用的结果。
对于 Jscript,可以使用 eval()
函数获得大体上相同的方法。所有的行为都相同,唯一的差别是语言本身。所获得的功能完全相同。
开发单一脚本代码库
对于经常使用脚本和公共脚本函数的开发人员来说,获得重复使用功能的最好方式是通过在
DTS 任务之间或 DTS
任务与包之间进行复制和粘贴。完成初始的复制和粘贴后,便在包之间实现了代码的共享。此外,还有可能形成分支,即允许进行更改而不会将更改传播给所有包和任务中的每个实例。
通过开发父/子包模块可以部分替代这些方法,而通过开发自定义任务可以进一步消除这些方法。但是,这两种方法都不能完全消除这种情况。真正的解决方案是开发可在运行时包含在包中且可以由包的
ActiveX 脚本组件引用的单一代码库。
单一代码库包
下面是用于开发单一脚本代码库的示例包的图示。
图 26:单一代码库包示例
下面将详细介绍每个元素,首先介绍的是“装载脚本文件”ActiveX
脚本任务。
装载脚本文件 -
利用文件系统对象
FileSystemObject
是脚本运行时的最灵活元素之一,因为它在 ActiveX
脚本中提供了对文件系统的必要访问。在我们的方法和示例中,开始部分的代码将使用
FileSystemObject 从 ASCII 文本文件中为 OLAP
分区管理加载公共 VB 脚本,将其存储在称为“代码库”的
ActiveX 脚本任务中并允许继续执行该包。
以下是该任务的 ActiveX 脚本:
Function Main()
' 此函数负责加载
' 在全局变量 gstrScriptFile 中定义的脚本文件
Dim objFSO
Dim objFile
Dim objPackage
Dim objTask
Dim strScript
' 读取公共 OLAP 分区处理脚本
set objFSO = CreateObject("Scripting.FileSystemObject")
set objFile = objFSO..OpenTextFile
(DTSGlobalVariables("gstrScriptFile").Value, forReading)
strScript = objFile.ReadAll
objFile.Close
' 将公共脚本放到“代码库”任务中
set objPackage = DTSGlobalVariables.Parent
set objTask = objPackage.Tasks(cstr("DTSTask_DTSActiveScriptTask_3"))
objTask.CustomTask.ActiveXScript.Value = strScript
set objTask = nothing
set objPackage = nothing
set objFile = nothing
set objFSO = nothing
Main = DTSTaskExecResult_Success
End Function
执行完上述脚本后,Code Library
任务应当包含该文本文件中的数据的副本。在将工作流继续到
Create Partition 任务之前,理解 Code Library
任务的选择和配置是非常重要的。
Code Library 任务
Code Library ActiveX Script
任务用作从文本文件加载的脚本的运行时知识库。选择
Code Library 任务方法的原因十分简单:
- ActiveX Script
任务可以存储大量脚本文本,用于支持加载较大公共脚本文件的方法。
- ActiveX Script
任务支持查看已加载的脚本。
- ActiveX Script
任务支持分析已加载的脚本文件,从而更便于进行调试。
为了使用 ActiveX Script
任务实现此目的,最好将入口函数更改为 Main()
以外的其他函数。这一点在下面的任务中被进一步明确。其次,应当将该步骤的工作流属性
Disable this Step(禁用此步骤)设置为 True,如下所示。
图 27:将 Disable this Step
属性设置为 True
此步骤确保当该任务位于其他任务的工作流之外时不会被执行。
创建分区 -
重复使用标准化的脚本
Create Partition ActiveX Script
任务才真正显示了可重复使用代码的强大功能,即它可以显著减少任务中的代码总量并实现复杂的功能。通过在
VBScript 中引用 Code Library 任务并使用 Execute()
函数,可以将 Code Library
任务的内容动态加载到 Create Partition
任务的运行时环境中。这样便允许任务引用之前加载的文本文件中定义的函数。
下面列出了该任务的代码:
Dim strCommonScript
Dim objPackage
Dim objTask
Set objPackage = DTSGlobalVariables.Parent
Set objTask = objPackage.Tasks("DTSStep_DTSActiveScriptTask_3")
strCommonScript = objTask.CustomTask.ActiveXScript.Value
Execute(strCommonScript)
Function Main()
Dim strTreeKey ' 要处理的 OLAP 对象的层次结构
' 运行动态添加的函数,该函数将为全局变量收集参数并生成
' 要在分析服务处理任务中
' 使用的 TreeKey!
call CreatePartition(strTreeKey)
' 将 TreeKey 放在全局变量中
DTSGlobalVariables("TreeKey").Value = strTreeKey
Main = DTSTaskExecResult_Success
End Function
该 CreatePartition()
逻辑类似于本白皮书前面部分显示的逻辑。此逻辑包含大量代码(远远多于这里的
Main
函数中的三行代码),用于执行分区的创建。通过使用
Main() 以外的其他名称命名 Code
Library
任务中的入口函数,将不会产生任何冲突。但是,如果我们在读取
Code Library 任务中的 ActiveX
脚本后试图使用相同的入口函数名称,则会产生冲突并禁止使用此重复使用方法。
指导原则
提供可重复使用的标准化脚本代码库实际上比人们可能采用的一些其他方法要简单得多,这样便可以利用由长期积累而形成的非常好的属性在
DTS 中编写脚本。为了充分利用在 DTS
体系结构中的已有投入,应当牢记以下指导原则。
- 考虑在文本文件或多个文件中存储
ActiveX 脚本。这将有利于控制 DTS
环境之外的脚本,并且可以使用诸如 Visual Source
Safe 等产品来管理公共脚本的版本。
- 使用文件系统对象代替其他方法来加载文本文件,以避免产生争用和文件锁定等问题。
- 尽量实现模块化。可重复使用的脚本函数应当明确地传递参数;如果确实必要,也可以依赖于全局变量中的设置。重要的是,不要在任务中的变量和该变量在动态脚本中的使用之间建立相关性。
- 有时,可以通过使用多个 DTS
包来更好地实现模块化。请注意,SQL Server 2000
为嵌套的子包提供了更多支持,因而使用子包解决方案可能更利于实现可重复使用功能。
实践总结
优点
DTS 功能的增强使之成为 SQL Server BI
平台的更强大的组件。由这些增强功能实现的一些核心优点包括:
- 模块化
通过实现这里介绍的方法,在开发
DTS
包的过程中可以使用更为模块化的方法,从中又会衍生出其他内在优点。
- 标准化
通过使用这些方法,可以在整个 DTS
包体系结构中更好地实现标准化。通过标准化,可以在
BI 解决方案 DTS
包的整个开发、维护和执行过程中获得更高的效率。
- 无限的功能扩展
通过采用这里讨论的技术方法,可以通过添加
COM 或 .NET
组件来引入许多其他功能。通过集成这些组件,可以(并且通常会)远远超越传统
BI ETL 平台的标准功能。
注意事项
使用此实践时,还是有一些事项需要注意。主要包括:
复杂性
复杂性主要是由以下原因导致的,即对逻辑实现的管理不当,以及在开发过程中开始时未考虑周全而不得不事后补救。使用这些技术时,要求将重点放在解决方案的设计上,以便为内置到可重复使用的脚本和自定义任务中的公共逻辑建立一个良好的基础。
小结
DTS 是一种灵活的体系结构,可用来在
Microsoft
数据仓库框架中建立复杂的解决方案。本白皮书重点介绍了在
BI 解决方案中使用 DTS
的最佳实践,并给出了这些最佳实践和方法的实际应用示例,这将有助于在特定解决方案体系结构中强化并利用这些技术。
更多信息
SQL Server 2000 联机图书提供了有关
Microsoft 数据仓库框架、SQL Server
关系数据库、数据转换服务和分析服务的更多信息。要获得其他信息,请访问以下资源:
附录 A - 关于作者
Trey Johnson 是 Professional Association for SQL
Server (PASS) 的董事会成员,并且是 Encore Development(一家专门从事基于
Web
的业务解决方案开发的创新软件咨询公司)的业务智能体系结构设计师。Johnson
的职责是协助 Encore
为中间市场提供完整的业务智能解决方案,他服务过的组织已达
1000 家。自 1994 年以来,Johnson 一直致力于 SQL Server
中的决策支持、数据仓库和数据采集工作。他曾在各种
SQL Server 学术会议上发表过演讲,其中包括多次 PASS
会议、多次 VSLive!SQL2TheMAX 会议以及 EDevCon 2000
会议;此外,他还从事过人工智能数据清除/采集以及关系决策支持和数据仓库/OLAP
在医疗保健、金融、零售、工业仓储以及法律实施等组织中的应用工作。
Mark
Chaffin 是 Encore Development(一家专门从事基于 Web
的业务解决方案开发的创新软件咨询公司)的业务智能执行主管。他是许多业务智能解决方案的首席体系结构设计师,其客户涉及众多领域,其中包括零售、日常消费品、医疗保健、金融、市场营销、银行、技术以及运动娱乐等行业。他在点击流
(clickstream) 分析、数据采集、事务应用体系结构、Internet
应用体系结构、数据库管理和数据库设计等方面具有丰富经验。他还与他人合著了《SQL
Server 2000 Data Transformation Services》一书(由 Wrox Press
出版)并撰写了许多有关业务智能、SQL Server、DTS
和分析服务的文章。
附录 B - .NET
自定义任务的代码列表
下面按单个文件名列出了 VB .NET
错误处理程序的所有代码。
AssemblyInfo.VB 文件的内容
Imports System.Reflection
Imports System.Runtime.InteropServices
' 有关程序集的一般信息通过以下属性集
' 控制。更改这些属性的值可以修改与程序集
' 关联的信息。
' 查看程序集属性的值
<Assembly: AssemblyTitle(".NET Error Handler")>
<Assembly: AssemblyDescription("DTS Custom Task handling errors originating
in other Steps")>
<Assembly: AssemblyCompany("Encore Development")>
<Assembly: AssemblyProduct(".NET Error Handler")>
<Assembly: AssemblyCopyright("")>
<Assembly: AssemblyTrademark("")>
<Assembly: CLSCompliant(True)>
' 如果此项目被提供给 COM,则以下 GUID 将用于 typelib 的 ID:
<Assembly: Guid("EF4AC3E4-3D72-4117-AB91-8B417826792F")>
' 程序集的版本信息由以下四部分组成:
'
' 主要版本
' 次要版本
' 内部版本号
' 修订版
'
' 您可以指定所有值,也可以通过使用“*”忽略内部版本号和
' 修订版号,如下所示:
<Assembly: AssemblyVersion("1.0.*")>
' 注意,要使 CustomTask 能够工作,必须生成并引用一个 KeyFile
<Assembly: AssemblyKeyFile(".\dotNetErrorHandler.snk")>
EHTask.VB 文件的内容
Public Class EHTask
' 您必须实现 CustomTask、
' CustomTaskUI 和 PersistPropertyBag 界面
Implements Interop.DTS.CustomTask
Implements Interop.DTS.CustomTaskUI
Implements Interop.DTS.PersistPropertyBag
' 创建用于保存标准属性的私有变量
Private _Name As String
Private _Description As String
' 创建对任务对象的私有引用
Private _TaskObject As Interop.DTS.Task
' 创建用于存储扩展的属性的其他私有变量
Private _LogFileName As String
Private _LogToEventLog As Boolean
Private _LogToFile As Boolean
Private _HandledStepName As String
Private _UseGlobals As Boolean
' 为 UI 设置友元变量,用于报告它是否被取消
Friend bolUICancelled As Boolean
Public Property Description() As String Implements
Interop.DTS.CustomTask.Description
Get
Return _Description
End Get
Set(ByVal Value As String)
_Description = Value
End Set
End Property
Public Property Name() As String Implements Interop.DTS.CustomTask.Name
Get
Return _Name
End Get
Set(ByVal Value As String)
_Name = Value
End Set
End Property
Public Sub Execute(ByVal pPackage As Object, ByVal pPackageEvents
As Object, ByVal pPackageLog As Object, ByRef pTaskResult
As Interop.DTS.DTSTaskExecResult) Implements
Interop.DTS.CustomTask.Execute
Dim objPackage As Interop.DTS.Package
Dim objTasks As Interop.DTS.Tasks
Dim objStep As Interop.DTS.Step
Dim objPackageLog As Interop.DTS.PackageLog
Dim strSource As String
Dim lngNumber As Long
Dim strDescription As String
Dim strErrorString As String
Try
' 将传递的参数强制转换为本地增强类型的变量
objPackage = pPackage
objPackageLog = pPackageLog
' 确定是否有一个优先步骤
Call EstablishPrecedence(objPackage)
' 检查 HandledStepName 并引发异常
If Me.HandledStepName = "" Then
Throw New System.Exception("Task cannot execute due to no
Precedence Constraint being defined.")
End If
' 需要检查任务是否被定义为由全局变量驱动
Call ReadFromGlobalVariables(objPackage)
' 获取一个 Step 对象引用
objStep = objPackage.Steps.Item(Me.HandledStepName)
' 检索错误信息
Call objStep.GetExecutionErrorInfo(lngNumber, strSource,
strDescription)
' 生成错误字符串
strErrorString = Format(Now, "MM/dd/yyyy hh:mm:ss tt") & "|" & _
objPackage.Name & "|" & objStep.Name & "|" & _
" The Step, " & objStep.Description & _
" failed with the following error: " & _
lngNumber.ToString() & " - " & strSource & " - " & strDescription
' 确定是否要写入日志文件
If Me.LogToFile And Trim(Me.LogFileName) = "" Then
' 由于缺少文件名而引发异常
Throw New System.Exception("Task cannot log to file due to
no log file being defined.")
ElseIf Me.LogToFile And Trim(Me.LogFileName) <> "" Then
' 写入日志文件
Dim objFile As System.IO.File
Dim objStreamWriter As System.IO.StreamWriter
objStreamWriter = objFile.AppendText(Me.LogFileName)
' 关闭 StreamWriter 并清除文件对象
objStreamWriter.WriteLine(strErrorString)
objStreamWriter.Close()
objStreamWriter = Nothing
objFile = Nothing
End If
' 确定是否要写入事件日志
If Me.LogToEventLog Then
Dim objEventLog As New EventLog()
objEventLog.WriteEntry(".Net DTS Error Handler", strErrorString)
objEventLog = Nothing
End If
' 如果执行成功,则返回 Success
pTaskResult = Interop.DTS.DTSTaskExecResult.DTSTaskExecResult_Success
Catch ex As Exception
Throw New System.Exception("An Error occured during the
tasks execution.", ex)
' 如果执行失败,则返回 Failure
pTaskResult = Interop.DTS.DTSTaskExecResult.DTSTaskExecResult_Failure
End Try
End Sub
Public Property LogFileName() As String
Get
Return _LogFileName
End Get
Set(ByVal Value As String)
_LogFileName = Value
End Set
End Property
Public Property LogToFile() As Boolean
Get
Return _LogToFile
End Get
Set(ByVal Value As Boolean)
_LogToFile = Value
End Set
End Property
Public Property LogToEventLog() As Boolean
Get
Return _LogToEventLog
End Get
Set(ByVal Value As Boolean)
_LogToEventLog = Value
End Set
End Property
Public Property HandledStepName() As String
Get
Return _HandledStepName
End Get
Set(ByVal Value As String)
_HandledStepName = Value
End Set
End Property
Public Property UseGlobals() As Boolean
Get
Return _UseGlobals
End Get
Set(ByVal Value As Boolean)
_UseGlobals = Value
End Set
End Property
Public ReadOnly Property Properties() As Interop.DTS.Properties
Implements Interop.DTS.CustomTask.Properties
Get
' 为 Properties 集合和 ProviderClass 声明变量,以便检索属性信息
ProviderClass to retrieve property info
Dim objProperties As Interop.DTS.Properties
Dim objPropProvider As Interop.DTS.PropertiesProviderClass =
New Interop.DTS.PropertiesProviderClass()
' 使用对现有任务对象的引用,获取对要传递到属性提供程序的
' 任务对象的引用
objProperties = objPropProvider.GetPropertiesForObject(_TaskObject)
' 对属性提供程序执行某些清除操作
objPropProvider = Nothing
' 返回属性集合的值
Return objProperties
End Get
End Property
Public Sub Load(ByVal PropertyBag As Interop.DTS.PropertyBag)
Implements Interop.DTS.PersistPropertyBag.Load
' 创建用于将 PropertyBag 中的信息读入模块级别变量中的
' 所有 PropertyBag 读取器
_Name = PropertyBag.Read("Name")
_Description = PropertyBag.Read("Description")
_LogFileName = PropertyBag.Read("LogFileName")
_LogToEventLog = PropertyBag.Read("LogToEventLog")
_LogToFile = PropertyBag.Read("LogToFile")
_HandledStepName = PropertyBag.Read("HandledStepName")
_UseGlobals = PropertyBag.Read("UseGlobals")
End Sub
Public Sub Save(ByVal PropertyBag As Interop.DTS.PropertyBag)
Implements Interop.DTS.PersistPropertyBag.Save
' 创建用于将模块级别变量中的信息持久保持在 PropertyBag 中的
' 所有 PropertyBag 写入器
PropertyBag.Write("Name", _Name)
PropertyBag.Write("Description", _Description)
PropertyBag.Write("LogFileName", _LogFileName)
PropertyBag.Write("LogToEventLog", _LogToEventLog)
PropertyBag.Write("LogToFile", _LogToFile)
PropertyBag.Write("HandledStepName", _HandledStepName)
PropertyBag.Write("UseGlobals", _UseGlobals)
End Sub
Public Sub New()
End Sub
Public Sub Initialize(ByVal pTask As Interop.DTS.Task) Implements
Interop.DTS.CustomTaskUI.Initialize
_TaskObject = pTask
End Sub
Protected Overrides Sub Finalize()
MyBase.Finalize()
End Sub
Public Sub Edit(ByVal hwndParent As Integer) Implements
Interop.DTS.CustomTaskUI.Edit
' 此处应当显示一个 UI
ShowPropertyUI()
End Sub
Public Sub GetUIInfo(ByRef pbstrToolTip As String, ByRef
pbstrDescription As String, ByRef plVersion As Integer, ByRef pFlags
As Interop.DTS.DTSCustomTaskUIFlags) Implements
Interop.DTS.CustomTaskUI.GetUIInfo
' 未实现
End Sub
Public Sub Delete(ByVal hwndParent As Integer) Implements
Interop.DTS.CustomTaskUI.Delete
_TaskObject = Nothing
End Sub
Public Sub Help(ByVal hwndParent As Integer) Implements
Interop.DTS.CustomTaskUI.Help
' 未实现
End Sub
Public Sub CreateCustomToolTip(ByVal hwndParent As Integer,
ByVal x As Integer, ByVal y As Integer, ByRef plTipWindow
As Integer) Implements Interop.DTS.CustomTaskUI.CreateCustomToolTip
' 未实现
End Sub
Public Sub New2(ByVal hwndParent As Integer) Implements
Interop.DTS.CustomTaskUI.New
' 为实现新方法,我们需要以这种方式声明
' 预设任务的设置
Me.LogToEventLog = True
Me.LogToFile = True
Me.LogFileName = ""
Me.UseGlobals = True
' 此处应当显示一个 UI
ShowPropertyUI()
End Sub
Private Sub ShowPropertyUI()
' 处理 UI 属性页面显示
Dim objForm As New frmProperty()
Dim objPackage As Object
Dim objTasks As Object
objForm.objCustomTask = Me
objForm.objMyTask = _TaskObject
Try
' 构造对 Tasks 集合和 Package 对象的引用
objTasks = _TaskObject.Parent
objPackage = objTasks.Parent
' 确定该错误处理程序是否存在全局变量
Call CheckGlobalVariables(objPackage)
' 从全局变量中读取数据并分配给属性
Call ReadFromGlobalVariables(objPackage)
' 检查优先约束
Call EstablishPrecedence(objPackage)
' 设置由 Friend 声明的布尔值,以便我们知道用户如何处理 UI
bolUICancelled = True
' 告诉窗体在显示它之前装载属性
objForm.GetProperties()
' 显示窗体
objForm.ShowDialog()
' 检查用户取消的方式
' 将属性保存到全局变量中(如果适用)
If Not bolUICancelled Then
Call SaveToGlobalVariables(objPackage)
End If
Catch ex As Exception
MsgBox(ex.Source & " - " & ex.Message, MsgBoxStyle.Critical,
".NET Error Handler")
End Try
End Sub
Private Sub EstablishPrecedence(ByVal pobjPackage As
Interop.DTS.Package)
Dim objStep As Interop.DTS.Step
Dim objPC As Interop.DTS.PrecedenceConstraint
' 检查匹配 TaskNames 的步骤
' 计算优先约束的数目
For Each objStep In pobjPackage.Steps
If objStep.TaskName = _Name Then
If objStep.PrecedenceConstraints.Count >= 1 Then
' 建立优先引用
' 获取对优先约束的引用并确定
' 用于处理程序的步骤信息
objPC = objStep.PrecedenceConstraints.Item(1)
Me.HandledStepName = objPC.StepName
' 动态生成任务说明
Me.Description() = "Handler for " &
pobjPackage.Steps.Item(objPC.StepName).Description
ElseIf objStep.PrecedenceConstraints.Count < 1 Then
Me.HandledStepName = ""
Exit For
End If
Exit For
End If
Next
objStep = Nothing
objPC = Nothing
End Sub
Private Sub CheckGlobalVariables(ByVal pobjPackage As Interop.DTS.Package)
Dim objGlobalVariables As Interop.DTS.GlobalVariables = pobjPackage.GlobalVariables
Dim objGV As Interop.DTS.GlobalVariable2
Dim bolErrorLogFileFound As Boolean
Dim bolErrorLogToEventLogFound As Boolean
Dim bolErrorLogToFileFound As Boolean
Dim bolShowMessage As Boolean
' 检查是否存在全局变量
For Each objGV In objGlobalVariables
Select Case UCase(objGV.Name)
Case "GSTRERRORLOGFILE"
bolErrorLogFileFound = True
Case "GBOLERRORLOGTOEVENTLOG"
bolErrorLogToEventLogFound = True
Case "GBOLERRORLOGTOFILE"
bolErrorLogToFileFound = True
Case Else
' 不重要
End Select
Next
' 如果未找到变量,则在包中添加该变量
globalvariables collection
If bolErrorLogFileFound = False Then
objGlobalVariables.AddGlobalVariable("gstrErrorLogFile",
CStr("c:\errorhandlerlog.log"))
bolShowMessage = True
End If
' 如果未找到变量,则在包中添加该变量
globalvariables collection
If bolErrorLogToEventLogFound = False Then
objGlobalVariables.AddGlobalVariable("gbolErrorLogToEventLog",
CBool(True))
bolShowMessage = True
End If
' 如果未找到变量,则在包中添加该变量
globalvariables collection
If bolErrorLogToFileFound = False Then
objGlobalVariables.AddGlobalVariable("gbolErrorLogToFile",
CBool(True))
bolShowMessage = True
End If
' 添加全局变量后,让用户知晓
If bolShowMessage Then
MsgBox("Global Variables have been added to this package to
allow all Error Handlers to reference one setting." & vbCrLf
& "Please indicate in individual Error Handler tasks
whether or not the global variables are to be used.",
MsgBoxStyle.Information, ".NET Error Handler: Global
Variables Added")
End If
End Sub
Private Sub SaveToGlobalVariables(ByVal pobjPackage As Interop.DTS.Package)
Dim objGlobalVariables As Interop.DTS.GlobalVariables = pobjPackage.GlobalVariables
Dim objGlobalVariable As Interop.DTS.GlobalVariable2
' 如果任务使用全局变量中的值,则应当将
' 在 UI 中输入的值
' 保存到 GlobalVariables 集合中以使其保持一致
If Me.UseGlobals Then
' 特别是:使全局变量可以按其正确
' 类型(例如,字符串和布尔值)访问的唯一方法
' 是删除然后重新添加它们
objGlobalVariables.Remove("gstrErrorLogFile")
objGlobalVariables.AddGlobalVariable("gstrErrorLogFile",
CStr(Me.LogFileName.ToString))
objGlobalVariables.Remove("gbolErrorLogToEventLog")
objGlobalVariables.AddGlobalVariable("gbolErrorLogToEventLog",
CBool(Me.LogToEventLog))
objGlobalVariables.Remove("gbolErrorLogToFile")
objGlobalVariables.AddGlobalVariable("gbolErrorLogToFile",
CBool(Me.LogToFile))
' 让用户知晓执行了此操作
MsgBox("The .NET Error Handler global variables were updated",
MsgBoxStyle.Information, ".NET Error Handler")
End If
' 清除并转移到 GC 中
objGlobalVariable = Nothing
objGlobalVariables = Nothing
End Sub
Private Sub ReadFromGlobalVariables(ByVal pobjPackage As Interop.DTS.Package)
Dim objGlobalVariables As Interop.DTS.GlobalVariables =
pobjPackage.GlobalVariables
' 如果任务使用全局变量中的值,则应当将在 GlobalVariables 中输入的值
' 分配给任务的属性
If Me.UseGlobals Then
Me.LogFileName = objGlobalVariables.Item("gstrErrorLogFile").Value
Me.LogToEventLog = objGlobalVariables.Item("gbolErrorLogToEventLog").Value
Me.LogToFile = objGlobalVariables.Item("gbolErrorLogToFile").Value
End If
End Sub
End Class
FrmProperty.VB 文件的内容
Public Class frmProperty
Inherits System.Windows.Forms.Form
' 声明公共变量,使任务可以允许对其自身的引用
' (作为 Task 对象和 CustomTask 对象)
Public objMyTask As Interop.DTS.Task
Public objCustomTask As dotNetErrorHandler.EHTask
' 为对 Tasks 集合和 Package 对象的引用
' 声明全局私有变量
Private objTasks As Interop.DTS.Tasks
Private objPackage As Interop.DTS.Package
#Region " Windows Form Designer generated code "
Public Sub New()
MyBase.New()
' 此调用是 Windows 窗体设计器要求的。
InitializeComponent()
' 在 InitializeComponent() 调用之后添加任何初始化
End Sub
' 窗体覆盖处理,以清除组件列表。
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
' Windows 窗体设计器要求的
Private components As System.ComponentModel.IContainer
' 注意: 以下过程是 Windows 窗体设计器要求的
' 可以通过 Windows 窗体设计器对其进行修改。
' 请勿使用代码编辑器进行修改。
Friend WithEvents txtName As System.Windows.Forms.TextBox
Friend WithEvents txtDescription As System.Windows.Forms.TextBox
Friend WithEvents Label2 As System.Windows.Forms.Label
Friend WithEvents Label3 As System.Windows.Forms.Label
Friend WithEvents chkLogToEvent As System.Windows.Forms.CheckBox
Friend WithEvents chkLogToFile As System.Windows.Forms.CheckBox
Friend WithEvents txtFileName As System.Windows.Forms.TextBox
Friend WithEvents btnFileDialog As System.Windows.Forms.Button
Friend WithEvents btnOkay As System.Windows.Forms.Button
Friend WithEvents btnCancel As System.Windows.Forms.Button
Friend WithEvents txtStepName As System.Windows.Forms.TextBox
Friend WithEvents ckhUseGlobals As System.Windows.Forms.CheckBox
Friend WithEvents lblVersion As System.Windows.Forms.Label
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
Me.lblVersion = New System.Windows.Forms.Label()
Me.txtName = New System.Windows.Forms.TextBox()
Me.txtDescription = New System.Windows.Forms.TextBox()
Me.Label2 = New System.Windows.Forms.Label()
Me.Label3 = New System.Windows.Forms.Label()
Me.chkLogToEvent = New System.Windows.Forms.CheckBox()
Me.chkLogToFile = New System.Windows.Forms.CheckBox()
Me.txtFileName = New System.Windows.Forms.TextBox()
Me.btnFileDialog = New System.Windows.Forms.Button()
Me.btnOkay = New System.Windows.Forms.Button()
Me.btnCancel = New System.Windows.Forms.Button()
Me.txtStepName = New System.Windows.Forms.TextBox()
Me.ckhUseGlobals = New System.Windows.Forms.CheckBox()
Me.SuspendLayout()
'
' lblVersion
'
Me.lblVersion.Location = New System.Drawing.Point(24, 200)
Me.lblVersion.Name = "lblVersion"
Me.lblVersion.Size = New System.Drawing.Size(240, 24)
Me.lblVersion.TabIndex = 0
'
' txtName
'
Me.txtName.Location = New System.Drawing.Point(304, 208)
Me.txtName.Name = "txtName"
Me.txtName.Size = New System.Drawing.Size(72, 20)
Me.txtName.TabIndex = 1
Me.txtName.Text = "<Task Name>"
Me.txtName.Visible = False
'
' txtDescription
'
Me.txtDescription.Location = New System.Drawing.Point(96, 16)
Me.txtDescription.Name = "txtDescription"
Me.txtDescription.Size = New System.Drawing.Size(408, 20)
Me.txtDescription.TabIndex = 3
Me.txtDescription.Text = "<Task Desc>"
'
' Label2
'
Me.Label2.Location = New System.Drawing.Point(16, 16)
Me.Label2.Name = "Label2"
Me.Label2.Size = New System.Drawing.Size(104, 24)
Me.Label2.TabIndex = 2
Me.Label2.Text = "Description :"
'
' Label3
'
Me.Label3.Location = New System.Drawing.Point(16, 56)
Me.Label3.Name = "Label3"
Me.Label3.Size = New System.Drawing.Size(256, 16)
Me.Label3.TabIndex = 5
Me.Label3.Text = "Handled Step :"
'
' chkLogToEvent
'
Me.chkLogToEvent.Location = New System.Drawing.Point(96, 88)
Me.chkLogToEvent.Name = "chkLogToEvent"
Me.chkLogToEvent.Size = New System.Drawing.Size(256, 24)
Me.chkLogToEvent.TabIndex = 6
Me.chkLogToEvent.Text = "Log Error to the Windows Event Log"
'
' chkLogToFile
'
Me.chkLogToFile.Location = New System.Drawing.Point(96, 120)
Me.chkLogToFile.Name = "chkLogToFile"
Me.chkLogToFile.Size = New System.Drawing.Size(256, 24)
Me.chkLogToFile.TabIndex = 7
Me.chkLogToFile.Text = "Log Error to the Following File "
'
' txtFileName
'
Me.txtFileName.Location = New System.Drawing.Point(272, 120)
Me.txtFileName.Name = "txtFileName"
Me.txtFileName.Size = New System.Drawing.Size(208, 20)
Me.txtFileName.TabIndex = 8
Me.txtFileName.Text = "c:\dts.log"
'
' btnFileDialog
'
Me.btnFileDialog.Location = New System.Drawing.Point(480, 120)
Me.btnFileDialog.Name = "btnFileDialog"
Me.btnFileDialog.Size = New System.Drawing.Size(24, 24)
Me.btnFileDialog.TabIndex = 9
Me.btnFileDialog.Text = "..."
'
' btnOkay
'
Me.btnOkay.Location = New System.Drawing.Point(352, 200)
Me.btnOkay.Name = "btnOkay"
Me.btnOkay.TabIndex = 10
Me.btnOkay.Text = "OK"
'
' btnCancel
'
Me.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel
Me.btnCancel.Location = New System.Drawing.Point(432, 200)
Me.btnCancel.Name = "btnCancel"
Me.btnCancel.TabIndex = 11
Me.btnCancel.Text = "Cancel"
'
' txtStepName
'
Me.txtStepName.Location = New System.Drawing.Point(96, 56)
Me.txtStepName.Name = "txtStepName"
Me.txtStepName.ReadOnly = True
Me.txtStepName.Size = New System.Drawing.Size(408, 20)
Me.txtStepName.TabIndex = 12
Me.txtStepName.Text = "<Handled Step Description (Name)>"
'
' ckhUseGlobals
'
Me.ckhUseGlobals.Location = New System.Drawing.Point(96, 152)
Me.ckhUseGlobals.Name = "ckhUseGlobals"
Me.ckhUseGlobals.Size = New System.Drawing.Size(408, 40)
Me.ckhUseGlobals.TabIndex = 13
Me.ckhUseGlobals.Text = "Use Global Variables
(Properties will be read from the Globals at run-time)"
'
' frmProperty
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.CancelButton = Me.btnCancel
Me.ClientSize = New System.Drawing.Size(512, 247)
Me.Controls.AddRange(New System.Windows.Forms.Control()
{Me.ckhUseGlobals, Me.txtStepName, Me.btnCancel, Me.btnOkay,
Me.btnFileDialog, Me.txtFileName, Me.chkLogToFile,
Me.chkLogToEvent, Me.Label3, Me.txtDescription, Me.Label2,
Me.txtName, Me.lblVersion})
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
Me.MaximizeBox = False
Me.MinimizeBox = False
Me.Name = "frmProperty"
Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide
Me.Text = ".NET Error Handler Task Properties"
Me.ResumeLayout(False)
End Sub
#End Region
Private Sub frmProperty_Load(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles MyBase.Load
' 由于调用了我们自己的自定义方法,因此此处没有进行任何操作
Me.CenterToParent()
End Sub
#Region " Our Own Custom Methods "
Public Sub GetProperties()
' 首先建立包和任务引用
Dim objStep As Interop.DTS.Step
Dim objPC As Interop.DTS.PrecedenceConstraint
Dim objTask As Object
Dim strHandledStepInfo As String
Dim strDescription As String
Dim strFilename As String
Dim strVersion As String
' 构造对 Tasks 集合和 Package 对象的引用
objTasks = objMyTask.Parent
objPackage = objTasks.Parent
' 获取 CustomTasks 说明
strDescription = objCustomTask.Description
' 检查匹配 TaskNames 的步骤
' 计算优先约束的数目
For Each objStep In objPackage.Steps
If objStep.TaskName = objMyTask.Name Then
If objStep.PrecedenceConstraints.Count > 1 Then
MsgBox("This task can only use 1 precedence
constraint." & vbCrLf & _
"The first will be the one handled by the task.",
MsgBoxStyle.Critical, ".NET Error Handler")
ElseIf objStep.PrecedenceConstraints.Count < 1 Then
MsgBox("This task requires that a precedence " & _
"constraint be defined.", MsgBoxStyle.Critical,
".NET Error Handler")
objCustomTask.HandledStepName = ""
strHandledStepInfo = "<No Preceding Step Defined>"
Exit For
End If
' 获取对优先约束的引用并确定
' 用于处理程序的步骤信息
objPC = objStep.PrecedenceConstraints.Item(1)
objCustomTask.HandledStepName = objPC.StepName
strHandledStepInfo =
objPackage.Steps.Item(objPC.StepName).Description & _
" (" & objPC.StepName & ")"
' 如果存在优先步骤,则建立一个新说明
strDescription = "Handler for " &
objPackage.Steps.Item(objPC.StepName).Description
Exit For
End If
Next
' 将属性映射到 UI 元素
Me.txtDescription.Text = strDescription
Me.txtName.Text = objCustomTask.Name
If objCustomTask.LogFileName = "" Then
Me.txtFileName.Text = strFilename
ElseIf strFilename = "" Then
Me.txtFileName.Text = objCustomTask.LogFileName
Else
Me.txtFileName.Text = strFilename
End If
Me.chkLogToEvent.Checked = objCustomTask.LogToEventLog
Me.chkLogToFile.Checked = objCustomTask.LogToFile
Me.txtStepName.Text = strHandledStepInfo
Me.ckhUseGlobals.Checked = objCustomTask.UseGlobals
' 建立版本信息,以帮助了解所部署的版本
strVersion = "Version " &
System.Reflection.Assembly.GetExecutingAssembly.GetName().Version.Major.To
String()
strVersion = strVersion & "." &
System.Reflection.Assembly.GetExecutingAssembly.GetName().Version.Minor.To
String
strVersion = strVersion & "." &
System.Reflection.Assembly.GetExecutingAssembly.GetName().Version.Build.To
String
Me.lblVersion.Text = strVersion
End Sub
Public Sub SaveProperties()
' 将窗体元素映射到任务属性
objCustomTask.Description = Me.txtDescription.Text
objCustomTask.Name = Me.txtName.Text
objCustomTask.LogFileName = Me.txtFileName.Text
objCustomTask.LogToEventLog = Me.chkLogToEvent.Checked
objCustomTask.LogToFile = Me.chkLogToFile.Checked
objCustomTask.UseGlobals = Me.ckhUseGlobals.Checked
End Sub
#End Region
Private Sub frmProperty_Closing(ByVal sender As Object, ByVal e As
System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
objMyTask = Nothing
objCustomTask = Nothing
' 清除全局私有引用
objTasks = Nothing
objPackage = Nothing
End Sub
Private Sub btnOkay_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles btnOkay.Click
objCustomTask.bolUICancelled = False
Me.SaveProperties()
Me.Close()
End Sub
Private Sub btnCancel_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles btnCancel.Click
Me.Close()
End Sub
Private Sub btnFileDialog_Click(ByVal sender As System.Object, ByVal e
As System.EventArgs) Handles btnFileDialog.Click
Dim objFileDialog As New Windows.Forms.SaveFileDialog()
' 配置 FileDialog
objFileDialog.DefaultExt = ".log"
objFileDialog.Filter = "Log Files (*.log)|*.log"
objFileDialog.FileName = Me.txtFileName.Text
objFileDialog.Title = ".NET Error Handler : Select Log File"
objFileDialog.ShowDialog()
If objFileDialog.FileName() <> "" Then
Me.txtFileName.Text = objFileDialog.FileName()
End If
objFileDialog = Nothing
End Sub
End Class
FrmProperty.Resx 文件的内容
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns=""
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string"
minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms,
Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms,
Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="lblVersion.Modifiers" type="System.CodeDom.MemberAttributes,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="txtName.Modifiers" type="System.CodeDom.MemberAttributes,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="txtDescription.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="Label2.Modifiers" type="System.CodeDom.MemberAttributes,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="Label3.Modifiers" type="System.CodeDom.MemberAttributes,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="chkLogToEvent.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="chkLogToFile.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="txtFileName.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="btnFileDialog.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="btnOkay.Modifiers" type="System.CodeDom.MemberAttributes,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="btnCancel.Modifiers" type="System.CodeDom.MemberAttributes,
System, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="txtStepName.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="ckhUseGlobals.Modifiers"
type="System.CodeDom.MemberAttributes, System, Version=1.0.3300.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>Assembly</value>
</data>
<data name="$this.Name">
<value>frmProperty</value>
</data>
</root>
dotNetErrorHandler.VBPROJ
文件的内容
<VisualStudioProject>
<VisualBasic
ProjectType = "Local"
ProductVersion = "7.0.9466"
SchemaVersion = "1.0"
ProjectGuid = "{C136AEE2-5D34-4B77-8F71-7DACB8EF1F28}"
>
<Build>
<Settings
ApplicationIcon = ""
AssemblyKeyContainerName = ""
AssemblyName = "dotNetErrorHandler"
AssemblyOriginatorKeyFile = ""
AssemblyOriginatorKeyMode = "None"
DefaultClientScript = "JScript"
DefaultHTMLPageLayout = "Grid"
DefaultTargetSchema = "IE50"
DelaySign = "false"
OutputType = "Library"
OptionCompare = "Binary"
OptionExplicit = "On"
OptionStrict = "Off"
RootNamespace = "dotNetErrorHandler"
StartupObject = ""
>
<Config
Name = "Debug"
BaseAddress = "285212672"
ConfigurationOverrideFile = ""
DefineConstants = ""
DefineDebug = "true"
Def race = "true"
DebugSymbols = "true"
IncrementalBuild = "true"
Optimize = "false"
OutputPath = "bin\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "1"
/>
<Config
Name = "Release"
BaseAddress = "285212672"
ConfigurationOverrideFile = ""
DefineConstants = ""
DefineDebug = "false"
Def race = "true"
DebugSymbols = "false"
IncrementalBuild = "false"
Optimize = "true"
OutputPath = "bin\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "1"
/>
</Settings>
<References>
<Reference
Name = "System"
AssemblyName = "System"
/>
<Reference
Name = "System.Data"
AssemblyName = "System.Data"
/>
<Reference
Name = "System.XML"
AssemblyName = "System.Xml"
/>
<Reference
Name = "Interop.DTS"
AssemblyName = "Interop.DTS"
HintPath = "Interop.DTS.dll"
/>
<Reference
Name = "System.Drawing"
AssemblyName = "System.Drawing"
HintPath = "..\..\..\..\WINDOWS\Microsoft.NET\Framework
\v1.0.3705\System.Drawing.dll"
/>
<Reference
Name = "System.Windows.Forms"
AssemblyName = "System.Windows.Forms"
HintPath = "..\..\..\..\WINDOWS\Microsoft.NET\Framework
\v1.0.3705\System.Windows.Forms.dll"
/>
</References>
<Imports>
<Import Namespace = "Microsoft.VisualBasic" />
<Import Namespace = "System" />
<Import Namespace = "System.Collections" />
<Import Namespace = "System.Data" />
<Import Namespace = "System.Diagnostics" />
</Imports>
</Build>
<Files>
<Include>
<File
RelPath = "AssemblyInfo.vb"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "EHTask.vb"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "frmProperty.vb"
SubType = "Form"
BuildAction = "Compile"
/>
<File
RelPath = "frmProperty.resx"
DependentUpon = "frmProperty.vb"
BuildAction = "EmbeddedResource"
/>
</Include>
</Files>
</VisualBasic>
</VisualStudioProject>
|