摘自:http://www.microsoft.com/china/MSDN/library/archives/library/dnSQL2k/html/SQL_busintbpwithDTS.asp
将 DTS 用于业务智能解决方案的最佳实践
Trey Johnson 和 Mark Chaffin
Encore Development
2002年9月
摘要:探索在数据仓库框架 (Data Warehousing Framework) 中使用 DTS 以捕获和提供数据,从而实现业务智能解决方案的最佳实践。
目录
- 简介
- DTS 和数据仓库框架
- 包设计实践
- 在包设计中包含元数据的指导原则
- 分析服务管理实践
- DTS 中的决策支持对象
- 创建分区
- 克隆分区
- 合并分区
- 删除分区
- 管理 OLAP 分区的指导原则
- 分析服务处理任务
- 在主线程中执行
- 使用 DTSRUN 执行分析服务处理
- 为分析服务处理任务编程
- 使用 DTS 执行分析服务处理的指导原则
- 实践摘要
- 审核和错误处理实践
- Fail Package on First Error
- 实践摘要
- 增强 DTS 功能实践
- 小结
- 附录 A - 关于作者
- 附录 B - .NET 自定义任务的代码列表
简介
业务智能 (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 解决方案在提醒组织注意日常数据质量问题方面发挥了重要作用,且不会破坏分析数据的可用性。
注意事项
此实践和各方法的注意事项是有限的,但确实存在。关键的注意事项包括:
- 报告数据质量
尽管曾将报告数据质量作为一个优点提出,但是具有重大维度问题或事实数据质量问题的组织可能会认为按照此方式报告数据是 BI 解决方案的缺点。管理有关数据质量的期望值并建立对此方法正确性的认识可以确保组织看到识别质量好的数据和质量差的数据的价值。
以往,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。需要指出的是,不熟悉注册表编辑的用户请不要尝试此操作,因为这可能会获得意外的结果。
编辑注册表之前需要了解以下项目:
- DLL 的完整路径(例如,C:\dotNetErrorHandler\bin\dotNetErrorHandler.dll)
- 具有图标资源的 DLL 或 .ICO 文件的完整路径(可选)
- 任务的说明(应保持简短 - 例如,.NET Error Handler)
- DLL 的 GUID - 可在 AssemblyInfo.vb 文件中的以下条目下找到该程序集的 GUID:
<Assembly: Guid("GUID DEFINED HERE")>
- 任务的 ProgID -(例如,从 COM 下的 CreateObject 中调用此对象的方法,例如,“dotNetErrorHandler.EHTask”)
建立以上所有项目后,便可以准备创建注册表条目文件来更新注册表。此方法的注册表条目文件模板为:
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 关系数据库、数据转换服务和分析服务的更多信息。要获得其他信息,请访问以下资源:
- Microsoft SQL Server Web 站点 http://www.microsoft.com/sql(英文)。
- Microsoft SQL Server 开发人员中心 http://msdn.microsoft.com/sqlserver(英文)。
- SQL Server 期刊 http://www.sqlmag.com/(英文)。
- 有关 SQL Server 的 Microsoft Official Curriculum 课程。有关最新课程的信息,请访问 http://www.microsoft.com/traincert(英文)。
附录 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>