自定义 SSIS 工作流任务

Custom SSIS workflow task

我有大量容器都遵循相同的基本前提:

当我从远程数据库提取数据时,我首先清空一个收集器table,将数据从远程数据库复制到收集器,计算收集器中的行数,如果有足够的行则我融入真实table。如果没有,我会发送一封包含错误消息的电子邮件。

我不想一遍又一遍地重复这个,而是想制作一个自定义组件。我认为这只是我要制作的过滤器组件,但我不太确定的是如何复制 Data Flow Task 片段。是否有人可以向我指出任何好的例子,或者只是让我知道我想做的事情是不可能的?

当我看到这样的问题时,Biml tends to offer the lowest barrier to creating a simple, repeatable solution. Biml is free, all it costs you is a registration email and install BimlExpress 进入您正在使用的任何版本的 Visual Studio/SSDT。

我假设我将从 AdventureWorks2014 Sales.Currency table 收集数据并将其传输到名为 dbo.SalesCurrency.[=24 的 tempdb 中的 table =]

我定义为

CREATE TABLE dbo.SalesCurrency
(
    CurrencyCode nchar(3) NOT NULL
,   Name nvarchar(50) NOT NULL
,   ModifiedDate datetime NOT NULL
);

鉴于此,让我们看看一些 Biml 概念。 Biml 是一种基于 XML 的方言,用于描述商业智能工件(以及一些)。如果您曾经使用脚本和标记的混合进行经典 ASP 开发,那么它是一个类似的概念,但由于 .NET 集成而更好。

  • <# #>这是一个multi-line块
  • <#= #>是单行表达式

很好,我该如何使用它?假设您已经安装了 BimlExpress,打开一个 SSIS 项目并右键单击项目部分和 select Add New Biml File。这样做两次,我们将重命名第二个。第一个是adriver,第二个是worker

脑筋急转弯

<Biml xmlns="http://schemas.varigence.com/biml.xsd">
    <Connections>
        <OleDbConnection Name="Source" ConnectionString="Data Source=localhost\dev2017;Initial Catalog=AdventureWorks2014;Provider=SQLNCLI11.0;Integrated Security=SSPI;" />        
        <OleDbConnection Name="Target" ConnectionString="Data Source=localhost\dev2017;Initial Catalog=tempdb;Provider=SQLNCLI11.0;Integrated Security=SSPI;" />        
    </Connections>
<#
string sourceQuery = "SELECT * FROM Sales.Currency;";
string targetSchemaTable = "[dbo].[SalesCurrency]";
string templateName = "so_56050574_include.biml";
dynamic customOutput;
#>
    <Packages>
        <#= CallBimlScriptWithOutput(templateName, out customOutput, sourceQuery, targetSchemaTable) #>
    </Packages>
</Biml>

第一行只是 xml 命名空间。

下一个块,连接 collections 我定义源和目标连接。我很有创意,将它们命名为 SourceTarget

接下来的几行看起来很像 C#,因为它们确实如此。我定义了我的源查询、完全限定的目标 table 名称、包含的方括号和我的模板文件的名称。最后一个变量 customOutput 在这里没有使用,但它是一个允许我从模板文件传回信息的包 - 即它构建的 SSIS 包的名称。

然后我定义一个包 collection 并制作一个包。我制作的包由我发送到 CallBimlScriptWithOutput 的任何内容定义,然后我使用我刚刚定义的变量。

看起来很复杂,其实不然。我喜欢这种方法的原因是,它不是将这些值硬编码到我的 driver 程序中,而是允许我采用元数据驱动的方法进行开发。我可以从电子表格、Sharepoint 列表、网络服务中查找这些值,无论我喜欢什么(或者我的客户提供的存储库)。

工人 biml

我将此文件命名为 so_56050574_include.biml,虽然里面有很多文字,但非常简单明了。

第一行帮助 Biml 设计体验中的 Intellisense。 接下来的两行指定这些变量将被传入——就像函数调用一样。我将能够在此文件的范围内像使用 .NET 变量一样使用它们。

接下来的几行有点古怪,但 SSIS 不喜欢重复的名称,也不喜欢名称中的 "bad" 个字符。我将包名称指定为 Populate Collector,然后我使目标 table 对 SSIS 安全。在文件的底部,您会看到我创建了一个名为 MakeSsisSafeName 的小方法,我用它来清理包名称。

我创建了一个包并给它起了一个好名字。该包有一个容器。在容器中,我创建了一些 SSIS 变量,我将需要这些变量来完成我的工作。该容器具有执行 SQL 任务 -> 数据流任务 -> 执行 SQL 任务 -> 执行 SQL 任务 -> 发送邮件任务

的任务
<#@ template designerbimlpath="/Biml/Packages" #>
<#@ property name="SourceQuery" type="string" #>
<#@ property name="TargetSchemaTable" type="string" #>

<#
string packageName = string.Format("Populate Collector {0}", MakeSsisSafeName(TargetSchemaTable));
CustomOutput.PackageName = packageName;
#>
<Package Name="<#= packageName #>" ConstraintMode="Linear">
    <Tasks>
        <Container Name="SEQC Collector" ConstraintMode="Parallel">
            <Variables>
                <Variable Name="RowCount" DataType="Int64">0</Variable>
                <Variable Name="QueryEmpty" DataType="String">TRUNCATE TABLE <#=TargetSchemaTable#></Variable>
                <Variable Name="QueryCount" DataType="String">SET NOCOUNT ON; SELECT COUNT_BIG(1) AS rc FROM <#=TargetSchemaTable#></Variable>
                <Variable Name="QuerySource" DataType="String"><#=SourceQuery#></Variable>
                <Variable Name="TargetSchemaTable" DataType="String"><#=TargetSchemaTable #></Variable>
            </Variables>
            <Tasks>
                <ExecuteSQL Name="SQL Empty Collector Table" ConnectionName="Target">
                    <VariableInput VariableName="User.QueryEmpty" />
                </ExecuteSQL>
                <Dataflow Name="DFT Populate Collector Table">
                    <Transformations>
                        <OleDbSource Name="OLESRC Query" ConnectionName="Source">
                            <VariableInput VariableName="User.QuerySource" />
                        </OleDbSource>
                        <OleDbDestination Name="OLEDST Target" ConnectionName="Target">
                            <TableFromVariableOutput VariableName="User.TargetSchemaTable" />
                        </OleDbDestination>
                    </Transformations>
                    <PrecedenceConstraints>
                        <Inputs>
                            <Input OutputPathName="SQL Empty Collector Table.Output" EvaluationValue="Success" />
                        </Inputs>
                    </PrecedenceConstraints>
                </Dataflow>
                <ExecuteSQL Name="SQL Count Collector Table Rows" ConnectionName="Target" ResultSet="SingleRow">
                    <VariableInput VariableName="User.QueryCount" />
                    <Results>
                        <Result Name="0" VariableName="User.RowCount" />
                    </Results>
                    <PrecedenceConstraints>
                        <Inputs>
                            <Input OutputPathName="DFT Populate Collector Table.Output" EvaluationValue="Success" />
                        </Inputs>
                    </PrecedenceConstraints>
                </ExecuteSQL>
                <ExecuteSQL Name="SQL Merge Collector Data" ConnectionName="Target">
                    <DirectInput>SELECT 1; -- simulate merge</DirectInput>
                        <PrecedenceConstraints>
                            <Inputs>
                                <Input OutputPathName="SQL Count Collector Table Rows.Output" EvaluationOperation="ExpressionAndConstraint" EvaluationValue="Success" Expression="@[User::RowCount] &gt; 0" />
                            </Inputs>
                        </PrecedenceConstraints>
                </ExecuteSQL>
                <!--
                <SendMail Name="Send Mail" ToLine="Foo@bar.com" ConnectionName="Target" Subject="Subject line">
                    <DirectInput>Body here, I think</DirectInput>
                        <PrecedenceConstraints>
                            <Inputs>
                                <Input OutputPathName="SQL Count Collector Table Rows.Output" EvaluationOperation="ExpressionOrConstraint" EvaluationValue="Success" Expression="@[User::RowCount] == 0" />
                            </Inputs>
                        </PrecedenceConstraints>
                </SendMail>
                -->
                <ExecuteSQL Name="SQL Pretend I send mail" ConnectionName="Target">
                    <DirectInput>SELECT 2; -- simulate merge</DirectInput>
                        <PrecedenceConstraints>
                            <Inputs>
                                <Input OutputPathName="SQL Count Collector Table Rows.Output" EvaluationOperation="ExpressionAndConstraint" EvaluationValue="Success" Expression="@[User::RowCount] ==0" />
                            </Inputs>
                        </PrecedenceConstraints>
                </ExecuteSQL>
            </Tasks>
        </Container>
    </Tasks>

</Package>

<#+
private static string MakeSsisSafeName(string name)
{
    return name.Replace("/", "_").Replace("\", "_").Replace(":", "_").Replace("[", "_").Replace("]", "_").Replace(".", "_").Replace("=", "_").Trim();
}
#>

右键单击 BimlScript brains 文件并select生成 SSIS 包

应该构建一个像这样的包,嘿,它有效!

未涵盖的内容

我不知道你是怎么用的。也许你有一个包含很多容器的大包,而你的愿景是只需按下按钮并添加另一个模板容器。 Biml 不会那样做。它不合并两个 SSIS 包——它用当前定义覆盖一个。但是,按照我定义所有这些的方式,您应该能够复制生成的 Container 并将其粘贴到现有的 SSIS 包中——假设它有两个名为 Source 和 Target 的连接。

连接也可能很棘手。如果您从 N 个源服务器收集数据,那么您可能需要一种循环机制来更改源值。这并不难。但是,如果您为每个收集器提取的源数据具有不同的签名,那么您需要每个定制的数据流任务。

正在发送电子邮件。我手头没有 SMTP 连接,所以我最好猜测发送邮件的样子,然后将其注释掉 <!-- ... --> 您需要在 brains 包中为您的 SMTP 服务器添加一个连接,然后然后配置 SendMail 任务以使用它。然后删除我的 "SQL Pretend I send mail" 任务。

最后,您会注意到这些名称在 worker Biml 中重复出现。这告诉引擎应该如何连接。如果你不喜欢我所说的东西,你需要在两个地方改变它。搜索和替换在这里会很方便 ;)

关于自定义工作流任务的问题 - 回答它

很好。这很糟糕。 DataFlow 的东西进入 COM objects 并且使用起来并不愉快。当您提供查询或来源时table,您需要检查元数据、add/remove 列和许多记录不完整的内容,并且需要大量的工作。这只是通过接口构建一个 "regular" 包。一旦你解决了这个问题,你就会考虑将该逻辑封装到一个自定义组件中,该组件曾经在 Codeplex 上用足够公平的样本进行记录,但现在已经死了,我不知道它是否已迁移到 github。哦,自定义任务和组件尤其依赖于版本,因此您可以针对各种二进制文件进行构建,以便为每个二进制文件获取一个 dll。然后您可能需要构建 UI 组件来帮助人们配置您的 SSIS task/component。然后您需要担心在每个开发人员的计算机上交付和安装它。以及服务器安装。

或者,我可以通过 Biml 定义一次并完成。