演练:扩展数据库项目部署以修改部署计划

可以创建部署参与者以便在部署 SQL 项目时执行自定义操作。 可以创建 DeploymentPlanModifierDeploymentPlanExecutor。 在执行计划之前,使用 DeploymentPlanModifier 更改计划,使用 DeploymentPlanExecutor 执行计划。 在本演练中,你将创建一个名为 SqlRestartableScriptContributor 的 DeploymentPlanModifier。 DeploymentPlanModifier SqlRestartableScriptContributor 将 IF 语句添加到部署脚本中的批处理中,使脚本在执行过程中出现错误时能够重新运行,直到这些批处理完成为止。

在本演练中,你将完成以下主要任务:

先决条件

你需要满足以下条件才能完成本演练:

  • 你必须已安装包含 SQL Server Data Tools 且支持 C# 开发的 Visual Studio 版本。

  • 必须具有包含 SQL 对象的 SQL 项目。

  • 可以向其部署数据库项目的 SQL Server 实例。

注意

本演练面向已熟悉 SQL Server Data Tools 的 SQL 功能的用户。 你还希望熟悉基本的 Visual Studio 概念,例如如何创建类库,以及如何使用代码编辑器将代码添加到类。

创建部署参与者

若要创建部署参与者,您必须执行以下任务:

  • 创建类库项目并添加所需的引用。
  • 定义从 DeploymentPlanModifier 继承的名为 SqlRestartableScriptContributor 的类。
  • 重写 OnExecute 方法。
  • 添加私有 Helper 方法。
  • 生成结果程序集。

创建类库项目

  1. 创建一个名为 MyOtherDeploymentContributor 的 C# 类库 (.NET Framework) 项目。
  2. 将文件“Class1.cs”重命名为“SqlRestartableScriptContributor.cs”。
  3. 在解决方案资源管理器中,右键单击项目节点,然后选择“ 添加引用”。
  4. 在“框架”选项卡上,选择“System.ComponentModel.Composition”。
  5. 从“项目”菜单中选择“管理 NuGet 包”选项。 安装“Microsoft.SqlServer.DacFx”的最新稳定版本。

接下来,开始向类中添加代码。

定义 SqlRestartableScriptContributor 类

  1. 在代码编辑器中,更新 class1.cs 文件以匹配以下 using 语句:

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Text;
    using Microsoft.SqlServer.Dac.Deployment;
    using Microsoft.SqlServer.Dac.Model;
    using Microsoft.SqlServer.TransactSql.ScriptDom;
    
  2. 更新类定义以匹配以下示例:

        /// <summary>
    /// This deployment contributor modifies a deployment plan by adding if statements
    /// to the existing batches in order to make a deployment script able to be rerun to completion
    /// if an error is encountered during execution
    /// </summary>
    [ExportDeploymentPlanModifier("MyOtherDeploymentContributor.RestartableScriptContributor", "1.0.0.0")]
    public class SqlRestartableScriptContributor : DeploymentPlanModifier
    {
    }
    

    现在,你已定义继承自 DeploymentPlanModifier 的部署参与者。 在生成和部署过程中,将从标准扩展目录中加载自定义参与者。 部署计划修改参与者将通过 ExportDeploymentPlanModifier 属性标识。 必须使用该属性才能发现参与者。 此属性应与以下函数修饰器类似:

    [ExportDeploymentPlanModifier("MyOtherDeploymentContributor.RestartableScriptContributor", "1.0.0.0")]
    
  3. 添加以下成员声明:

         private const string BatchIdColumnName = "BatchId";
            private const string DescriptionColumnName = "Description";
    
            private const string CompletedBatchesVariableName = "CompletedBatches";
            private const string CompletedBatchesVariable = "$(CompletedBatches)";
            private const string CompletedBatchesSqlCmd = @":setvar " + CompletedBatchesVariableName + " __completedBatches_{0}_{1}";
            private const string TotalBatchCountSqlCmd = @":setvar TotalBatchCount {0}";
            private const string CreateCompletedBatchesTable = @"
    if OBJECT_ID(N'tempdb.dbo." + CompletedBatchesVariable + @"', N'U') is null
    begin
    use tempdb
    create table [dbo].[$(CompletedBatches)]
    (
    BatchId int primary key,
    Description nvarchar(300)
    )
    use [$(DatabaseName)]
    end
    ";
    

    接下来,您将重写 OnExecute 方法以添加要在部署数据库项目时运行的代码。

重写 OnExecute

  1. 将下面的方法添加到 SqlRestartableScriptContributor 类:

    /// <summary>
    /// You override the OnExecute method to do the real work of the contributor.
    /// </summary>
    /// <param name="context"></param>
    protected override void OnExecute(DeploymentPlanContributorContext context)
    {
         // Replace this with the method body
    }
    

    通过基类 DeploymentPlanContributor 重写 OnExecute 方法。 DeploymentPlanContributor 同时是 DeploymentPlanModifierDeploymentPlanExecutor 的基类。 将向 OnExecute 方法传递一个 DeploymentPlanContributorContext 对象,该对象提供对任何指定参数、源和目标数据库模型、部署计划以及部署选项的访问权。 在此示例中,我们获取了部署计划和目标数据库名称。

  2. 现在,将正文的开头添加到 OnExecute 方法:

    // Obtain the first step in the Plan from the provided context
    DeploymentStep nextStep = context.PlanHandle.Head;
    int batchId = 0;
    BeginPreDeploymentScriptStep beforePreDeploy = null;
    
    // Loop through all steps in the deployment plan
    while (nextStep != null)
    {
        // Increment the step pointer, saving both the current and next steps
        DeploymentStep currentStep = nextStep;
        nextStep = currentStep.Next;
    
        // Add additional step processing here
    }
    
    // if we found steps that required processing, set up a temporary table to track the work that you are doing
    if (beforePreDeploy != null)
    {
        // Add additional post-processing here
    }
    
    // Cleanup and drop the table
    DeploymentScriptStep dropStep = new DeploymentScriptStep(DropCompletedBatchesTable);
    base.AddAfter(context.PlanHandle, context.PlanHandle.Tail, dropStep);
    

    在此代码中,我们定义几个局部变量,并设置将处理部署计划中所有步骤的处理过程的循环。 循环完成后,必须执行一些后处理,然后删除在部署期间创建的临时表,以跟踪计划的执行进度。 主要类型为:DeploymentStepDeploymentScriptStep。 主要方法是 AddAfter。

  3. 现在,添加其他步骤处理来替换显示为“在此处添加其他步骤处理”的注释:

    // Look for steps that mark the pre/post deployment scripts
    // These steps are always in the deployment plan even if the
    // user's project does not have a pre/post deployment script
    if (currentStep is BeginPreDeploymentScriptStep)
    {
        // This step marks the beginning of the predeployment script.
        // Save the step and move on.
        beforePreDeploy = (BeginPreDeploymentScriptStep)currentStep;
        continue;
    }
    if (currentStep is BeginPostDeploymentScriptStep)
    {
        // This is the step that marks the beginning of the post deployment script.
        // We do not continue processing after this point.
        break;
    }
    if (currentStep is SqlPrintStep)
    {
        // We do not need to put if statements around these
        continue;
    }
    
    // if we have not yet found the beginning of the pre-deployment script steps,
    // skip to the next step.
    if (beforePreDeploy == null)
    {
        // We only surround the "main" statement block with conditional
        // statements
        continue;
    }
    
    // Determine if this is a step that we need to surround with a conditional statement
    DeploymentScriptDomStep domStep = currentStep as DeploymentScriptDomStep;
    if (domStep == null)
    {
        // This step is not a step that we know how to modify,
        // so skip to the next step.
        continue;
    }
    
    TSqlScript script = domStep.Script as TSqlScript;
    if (script == null)
    {
        // The script dom step does not have a script with batches - skip
        continue;
    }
    
        // Loop through all the batches in the script for this step. All the
        // statements  in the batch are enclosed in an if statement that checks
        // the table to ensure that the batch has not already been executed
        TSqlObject sqlObject;
        string stepDescription;
        GetStepInfo(domStep, out stepDescription, out sqlObject);
        int batchCount = script.Batches.Count;
    
    for (int batchIndex = 0; batchIndex < batchCount; batchIndex++)
    {
        // Add batch processing here
    }
    

    代码注释用于对处理过程进行说明。 粗略来说,此代码将查找你关注的步骤,跳过其他步骤并在你即将开始后期部署步骤时停止。 如果步骤包含必须用条件括起来的语句,我们将执行额外的处理。 主要类型、方法和属性包括 DacFx 库中的以下组件:BeginPreDeploymentScriptStepBeginPostDeploymentScriptStepTSqlObjectTSqlScript、脚本、DeploymentScriptDomStepSqlPrintStep

  4. 现在,通过替换显示为“在此处添加批处理”的注释来添加批处理代码:

        // Create the if statement that contains the batch's contents
        IfStatement ifBatchNotExecutedStatement = CreateIfNotExecutedStatement(batchId);
        BeginEndBlockStatement statementBlock = new BeginEndBlockStatement();
        ifBatchNotExecutedStatement.ThenStatement = statementBlock;
        statementBlock.StatementList = new StatementList();
    
        TSqlBatch batch = script.Batches[batchIndex];
        int statementCount = batch.Statements.Count;
    
        // Loop through all statements in the batch, embedding those in an sp_execsql
        // statement that must be handled this way (schemas, stored procedures,
        // views, functions, and triggers).
        for (int statementIndex = 0; statementIndex < statementCount; statementIndex++)
        {
            // Add additional statement processing here
        }
    
        // Add an insert statement to track that all the statements in this
        // batch were executed.  Turn on nocount to improve performance by
        // avoiding row inserted messages from the server
        string batchDescription = string.Format(CultureInfo.InvariantCulture,
            "{0} batch {1}", stepDescription, batchIndex);
    
        PredicateSetStatement noCountOff = new PredicateSetStatement();
        noCountOff.IsOn = false;
        noCountOff.Options = SetOptions.NoCount;
    
        PredicateSetStatement noCountOn = new PredicateSetStatement();
        noCountOn.IsOn = true;
        noCountOn.Options = SetOptions.NoCount;
        InsertStatement batchCompleteInsert = CreateBatchCompleteInsert(batchId, batchDescription);
        statementBlock.StatementList.Statements.Add(noCountOn);
    statementBlock.StatementList.Statements.Add(batchCompleteInsert);
        statementBlock.StatementList.Statements.Add(noCountOff);
    
        // Remove all the statements from the batch (they are now in the if block) and add the if statement
        // as the sole statement in the batch
        batch.Statements.Clear();
        batch.Statements.Add(ifBatchNotExecutedStatement);
    
        // Next batch
        batchId++;
    

    此代码创建一个 IF 语句以及一个 BEGIN/END 块。 然后,对批处理中的语句执行额外的处理。 完成后,我们将添加一个 INSERT 语句,以将信息添加到跟踪脚本执行的进度的临时表。 最后,更新批次,将原先的语句替换为包含这些语句的新 IF。 键类型、方法和属性包括: IfStatementBeginEndBlockStatementStatementListTSqlBatchPredicateSetStatementSetOptionsInsertStatement

  5. 现在,添加语句处理循环的正文。 替换显示为“在此处添加其他语句处理”的注释:

    TSqlStatement smnt = batch.Statements[statementIndex];
    
    if (IsStatementEscaped(sqlObject))
    {
        // "escape" this statement by embedding it in a sp_executesql statement
        string statementScript;
        domStep.ScriptGenerator.GenerateScript(smnt, out statementScript);
        ExecuteStatement spExecuteSql = CreateExecuteSql(statementScript);
        smnt = spExecuteSql;
    }
    
    statementBlock.StatementList.Statements.Add(smnt);
    

    对于批处理中的每个语句,如果语句的类型必须用 sp_executesql 语句包装,请相应地修改该语句。 然后,代码将语句添加到所创建的块的 BEGIN/END 语句列表中。 主要类型、方法和属性包括 TSqlStatementExecuteStatement

  6. 最后,添加后续处理部分来替代显示为“在此处添加其他后续处理”的注释:

    // Declare a SqlCmd variables.
    //
    // CompletedBatches variable - defines the name of the table in tempdb that tracks all
    // the completed batches. The temporary table's name has the target database name and
    // a GUID embedded in it so that:
    // * Multiple deployment scripts targeting different DBs on the same server
    // * Failed deployments with old tables do not conflict with more recent deployments
    //
    // TotalBatchCount variable - the total number of batches surrounded by if statements.  Using this
    // variable pre/post deployment scripts can also use the CompletedBatches table to make their
    // script rerunnable if there is an error during execution
    StringBuilder sqlcmdVars = new StringBuilder();
    sqlcmdVars.AppendFormat(CultureInfo.InvariantCulture, CompletedBatchesSqlCmd,
        context.Options.TargetDatabaseName, Guid.NewGuid().ToString("D"));
    sqlcmdVars.AppendLine();
    sqlcmdVars.AppendFormat(CultureInfo.InvariantCulture, TotalBatchCountSqlCmd, batchId);
    
    DeploymentScriptStep completedBatchesSetVarStep = new DeploymentScriptStep(sqlcmdVars.ToString());
    base.AddBefore(context.PlanHandle, beforePreDeploy, completedBatchesSetVarStep);
    
    // Create the temporary table we use to track the work that we are doing
    DeploymentScriptStep createStatusTableStep = new DeploymentScriptStep(CreateCompletedBatchesTable);
    base.AddBefore(context.PlanHandle, beforePreDeploy, createStatusTableStep);
    

    如果处理过程发现了一个或多个用条件语句环绕的步骤,则必须向部署脚本添加语句以定义 SQLCMD 变量。 变量包括:

    • CompletedBatches 包含部署脚本用于跟踪脚本执行时成功完成的批的临时表的唯一名称

    • TotalBatchCount 包含部署脚本中的批总数

    其他相关的类型、属性和方法包括:

    StringBuilderDeploymentScriptStepAddBefore.

    接下来,您将定义由此方法调用的 Helper 方法。

添加辅助方法

  • 必须定义多个帮助程序方法。 重要方法包括:

    方法 Description
    CreateExecuteSQL 定义 CreateExecuteSQL 方法,以便在 EXECsp_executesql 语句内围住提供的语句。 主要类型、方法和属性包括以下 DacFx API 组件:ExecuteStatementExecutableProcedureReferenceSchemaObjectNameProcedureReferenceExecuteParameter
    CreateCompletedBatchesName 定义 CreateCompletedBatchesName 方法。 此方法创建一个名称,用于插入到批处理的临时表中。 密钥类型、方法和属性包括以下 DacFx API 组件: SchemaObjectName
    IsStatementEscaped 定义 IsStatementEscaped 方法。 此方法会确定模型元素的类型是否需要语句先包装在 EXECsp_executesql 语句中,然后才能将其包在 IF 语句中。 主要类型、方法和属性包括以下 DacFx API 组件:TSqlObject.ObjectType、ModelTypeClass 和以下模型类型的 TypeClass 属性:Schema、Procedure、View、TableValuedFunction、ScalarFunction、DatabaseDdlTrigger、DmlTrigger、ServerDdlTrigger。
    CreateBatchCompleteInsert 定义 CreateBatchCompleteInsert 方法。 此方法创建 INSERT 添加到部署脚本的语句,以跟踪脚本执行进度。 主要类型、方法和属性包括以下 DacFx API 组件:InsertStatement、NamedTableReference、ColumnReferenceExpression、ValuesInsertSource 和 RowValue。
    CreateIfNotExecutedStatement 定义 CreateIfNotExecutedStatement 方法。 此方法生成一个 IF 语句,用于检查临时批处理执行表是否指示已执行此批处理。 键类型、方法和属性包括:IfStatement、ExistsPredicate、ScalarSubquery、NamedTableReference、WhereClause、ColumnReferenceExpression、IntegerLiteral、BooleanComparisonExpression 和 BooleanNotExpression。
    GetStepInfo 定义 GetStepInfo 方法。 此方法提取有关用于创建步骤的脚本的模型元素的信息以及步骤名称。 感兴趣的类型和方法包括: DeploymentPlanContributorContextDeploymentScriptDomStepTSqlObjectCreateElementStepAlterElementStepDropElementStep
    GetElementName 为 TSqlObject 创建格式化名称。
  1. 添加下列代码可定义 Helper 方法:

    /// <summary>
    /// The CreateExecuteSql method "wraps" the provided statement script in an "sp_executesql" statement
    /// Examples of statements that must be so wrapped include: stored procedures, views, and functions
    /// </summary>
    private static ExecuteStatement CreateExecuteSql(string statementScript)
    {
        // define a new Exec statement
        ExecuteStatement executeSp = new ExecuteStatement();
        ExecutableProcedureReference spExecute = new ExecutableProcedureReference();
        executeSp.ExecuteSpecification = new ExecuteSpecification { ExecutableEntity = spExecute };
    
        // define the name of the procedure that you want to execute, in this case sp_executesql
        SchemaObjectName procName = new SchemaObjectName();
        procName.Identifiers.Add(CreateIdentifier("sp_executesql", QuoteType.NotQuoted));
        ProcedureReference procRef = new ProcedureReference { Name = procName };
    
        spExecute.ProcedureReference = new ProcedureReferenceName { ProcedureReference = procRef };
    
        // add the script parameter, constructed from the provided statement script
        ExecuteParameter scriptParam = new ExecuteParameter();
        spExecute.Parameters.Add(scriptParam);
        scriptParam.ParameterValue = new StringLiteral { Value = statementScript };
        scriptParam.Variable = new VariableReference { Name = "@stmt" };
        return executeSp;
    }
    
    /// <summary>
    /// The CreateIdentifier method returns a Identifier with the specified value and quoting type
    /// </summary>
    private static Identifier CreateIdentifier(string value, QuoteType quoteType)
    {
        return new Identifier { Value = value, QuoteType = quoteType };
    }
    
    /// <summary>
    /// The CreateCompletedBatchesName method creates the name that is inserted
    /// into the temporary table for a batch.
    /// </summary>
    private static SchemaObjectName CreateCompletedBatchesName()
    {
        SchemaObjectName name = new SchemaObjectName();
        name.Identifiers.Add(CreateIdentifier("tempdb", QuoteType.SquareBracket));
        name.Identifiers.Add(CreateIdentifier("dbo", QuoteType.SquareBracket));
        name.Identifiers.Add(CreateIdentifier(CompletedBatchesVariable, QuoteType.SquareBracket));
        return name;
    }
    
    /// <summary>
    /// Helper method that determines whether the specified statement needs to
    /// be escaped
    /// </summary>
    /// <param name="sqlObject"></param>
    /// <returns></returns>
    private static bool IsStatementEscaped(TSqlObject sqlObject)
    {
        HashSet<ModelTypeClass> escapedTypes = new HashSet<ModelTypeClass>
        {
            Schema.TypeClass,
            Procedure.TypeClass,
            View.TypeClass,
            TableValuedFunction.TypeClass,
            ScalarFunction.TypeClass,
            DatabaseDdlTrigger.TypeClass,
            DmlTrigger.TypeClass,
            ServerDdlTrigger.TypeClass
        };
        return escapedTypes.Contains(sqlObject.ObjectType);
    }
    
    /// <summary>
    /// Helper method that creates an INSERT statement to track a batch being completed
    /// </summary>
    /// <param name="batchId"></param>
    /// <param name="batchDescription"></param>
    /// <returns></returns>
    private static InsertStatement CreateBatchCompleteInsert(int batchId, string batchDescription)
    {
        InsertStatement insert = new InsertStatement();
        NamedTableReference batchesCompleted = new NamedTableReference();
        insert.InsertSpecification = new InsertSpecification();
        insert.InsertSpecification.Target = batchesCompleted;
        batchesCompleted.SchemaObject = CreateCompletedBatchesName();
    
        // Build the columns inserted into
        ColumnReferenceExpression batchIdColumn = new ColumnReferenceExpression();
        batchIdColumn.MultiPartIdentifier = new MultiPartIdentifier();
        batchIdColumn.MultiPartIdentifier.Identifiers.Add(CreateIdentifier(BatchIdColumnName, QuoteType.NotQuoted));
    
        ColumnReferenceExpression descriptionColumn = new ColumnReferenceExpression();
        descriptionColumn.MultiPartIdentifier = new MultiPartIdentifier();
        descriptionColumn.MultiPartIdentifier.Identifiers.Add(CreateIdentifier(DescriptionColumnName, QuoteType.NotQuoted));
    
        insert.InsertSpecification.Columns.Add(batchIdColumn);
        insert.InsertSpecification.Columns.Add(descriptionColumn);
    
        // Build the values inserted
        ValuesInsertSource valueSource = new ValuesInsertSource();
        insert.InsertSpecification.InsertSource = valueSource;
    
        RowValue values = new RowValue();
        values.ColumnValues.Add(new IntegerLiteral { Value = batchId.ToString() });
        values.ColumnValues.Add(new StringLiteral { Value = batchDescription });
        valueSource.RowValues.Add(values);
    
        return insert;
    }
    
    /// <summary>
    /// This is a helper method that generates an if statement that checks the batches executed
    /// table to see if the current batch has been executed.  The if statement looks like this
    ///
    /// if not exists(select 1 from [tempdb].[dbo].[$(CompletedBatches)]
    ///                where BatchId = batchId)
    /// begin
    /// end
    /// </summary>
    /// <param name="batchId"></param>
    /// <returns></returns>
    private static IfStatement CreateIfNotExecutedStatement(int batchId)
    {
        // Create the exists/select statement
        ExistsPredicate existsExp = new ExistsPredicate();
        ScalarSubquery subQuery = new ScalarSubquery();
        existsExp.Subquery = subQuery;
    
        subQuery.QueryExpression = new QuerySpecification
        {
            SelectElements =
            {
                new SelectScalarExpression  { Expression = new IntegerLiteral { Value ="1" } }
            },
            FromClause = new FromClause
            {
                TableReferences =
                    {
                        new NamedTableReference() { SchemaObject = CreateCompletedBatchesName() }
                    }
            },
            WhereClause = new WhereClause
            {
                SearchCondition = new BooleanComparisonExpression
                {
                    ComparisonType = BooleanComparisonType.Equals,
                    FirstExpression = new ColumnReferenceExpression
                    {
                        MultiPartIdentifier = new MultiPartIdentifier
                        {
                            Identifiers = { CreateIdentifier(BatchIdColumnName, QuoteType.SquareBracket) }
                        }
                    },
                    SecondExpression = new IntegerLiteral { Value = batchId.ToString() }
                }
            }
        };
    
        // Put together the rest of the statement
        IfStatement ifNotExists = new IfStatement
        {
            Predicate = new BooleanNotExpression
            {
                Expression = existsExp
            }
        };
    
        return ifNotExists;
    }
    
    /// <summary>
    /// Helper method that generates a useful description of the step.
    /// </summary>
    private static void GetStepInfo(
        DeploymentScriptDomStep domStep,
        out string stepDescription,
        out TSqlObject element)
    {
        element = null;
    
        // figure out what type of step we've got, and retrieve
        // either the source or target element.
        if (domStep is CreateElementStep)
        {
            element = ((CreateElementStep)domStep).SourceElement;
        }
        else if (domStep is AlterElementStep)
        {
            element = ((AlterElementStep)domStep).SourceElement;
        }
        else if (domStep is DropElementStep)
        {
            element = ((DropElementStep)domStep).TargetElement;
        }
    
        // construct the step description by concatenating the type and the fully qualified
        // name of the associated element.
        string stepTypeName = domStep.GetType().Name;
        if (element != null)
        {
            string elementName = GetElementName(element);
    
            stepDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}",
                stepTypeName, elementName);
        }
        else
        {
            // if the step has no associated element, just use the step type as the description
            stepDescription = stepTypeName;
        }
    }
    
    private static string GetElementName(TSqlObject element)
    {
        StringBuilder name = new StringBuilder();
        if (element.Name.HasExternalParts)
        {
            foreach (string part in element.Name.ExternalParts)
            {
                if (name.Length > 0)
                {
                    name.Append('.');
                }
                name.AppendFormat("[{0}]", part);
            }
        }
    
        foreach (string part in element.Name.Parts)
        {
            if (name.Length > 0)
            {
                name.Append('.');
            }
            name.AppendFormat("[{0}]", part);
        }
    
        return name.ToString();
    }
    
  2. 保存对 SqlRestartableScriptContributor.cs 的更改。

接下来,你将生成类库。

对程序集进行签名和生成

  1. “项目 ”菜单上,选择 “MyOtherDeploymentContributor 属性”。

  2. 选择“ 签名 ”选项卡。

  3. 选择“ 对程序集进行签名”。

  4. “选择强名称密钥文件”中,选择“ <新建>”。

  5. 在“创建强名称密钥” 对话框的“密钥文件名称” 中,键入“MyRefKey” 。

  6. (可选)可以为强名称密钥文件指定密码。

  7. 选择“确定”

  8. 在“文件”菜单上,单击“全部保存”

  9. 在“生成”菜单上,选择“生成解决方案”

    接下来,必须安装程序集,以便在部署 SQL 项目时加载它。

安装部署参与者

若要安装部署参与者,必须将程序集和关联 .pdb 文件复制到 Extensions 文件夹。

安装 MyOtherDeploymentContributor 程序集

  1. 接下来,将程序集信息复制到 Extensions 目录。 Visual Studio 2022 启动时,它会标识目录和子目录中的任何扩展 %ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\SQLDB\DAC ,并使它们可供使用。

  2. MyOtherDeploymentContributor.dll 程序集文件从输出目录复制到 %ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\SQLDB\DAC 该目录。 默认情况下,已 .dll 编译文件的路径为 YourSolutionPath\YourProjectPath\bin\DebugYourSolutionPath\YourProjectPath\bin\Release

运行或测试部署贡献者

若要运行或测试部署参与者,您必须执行以下任务:

  • 将属性添加到您计划构建的 .sqlproj 文件。

  • 通过使用 MSBuild 并提供适当的参数来部署数据库项目。

将属性添加到 SQL 项目 (.sqlproj) 文件

你必须始终更新 SQL 项目文件以指定要运行的参与者的 ID。 你可以通过以下两种方式之一更新 SQL 项目:

  1. 可以手动修改 .sqlproj 文件以添加所需的参数。 如果你的贡献者没有配置所需的任何贡献者参数,或者你不想在大量项目中重复使用构建贡献者,则可以考虑这样做。 如果选择此选项,请在文件中第一个导入节点之后将 .sqlproj 以下语句添加到该文件:

    <PropertyGroup>
      <DeploymentContributors>
        $(DeploymentContributors); MyOtherDeploymentContributor.RestartableScriptContributor
      </DeploymentContributors>
    </PropertyGroup>
    
  2. 第二种方法是创建包含所需的参与者参数的目标文件。 如果对多个项目使用相同的参与者,并且需要参与者参数,因为它包含默认值,这非常有用。 在此情况下,请在 MSBuild 扩展路径中创建目标文件:

    1. 请导航至 %ProgramFiles%\MSBuild

    2. 创建一个存储目标文件的新文件夹“MyContributors”。

    3. 在该目录中创建一个新文件“MyContributors.targets”,将下列文本添加到该文件中并保存该文件:

      <?xml version="1.0" encoding="utf-8"?>
      
      <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
        <PropertyGroup>
          <DeploymentContributors>$(DeploymentContributors);MyOtherDeploymentContributor.RestartableScriptContributor</DeploymentContributors>
        </PropertyGroup>
      </Project>
      
    4. 在您要运行相关贡献者的任何项目的 .sqlproj 文件中,通过将以下语句添加到 .sqlproj 文件中:<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /> 节点之后,导入目标文件。

      <Import Project="$(MSBuildExtensionsPath)\MyContributors\MyContributors.targets " />
      

在使用了这些方法之一后,您可以使用 MSBuild 来传入命令行生成的参数。

注意

您必须始终更新“DeploymentContributors”属性以指定您的参与者 ID。 此 ID 与参与者源文件中的“ExportDeploymentPlanModifier”属性中使用的 ID 相同。 如果没有这个,则在构建项目时不会运行贡献模块。 仅在具有运行参与者所需的参数时需要更新“ContributorArguments”属性。

部署数据库项目

  • 可以像在 Visual Studio 中一样正常发布或部署您的项目。 打开包含 SQL 项目的解决方案,然后从项目的右键单击上下文菜单中选择“发布...”选项,或使用 F5 将项目调试部署到 LocalDB。 在此示例中,我们使用“发布...”对话框来生成部署脚本。

    1. 打开 Visual Studio 并打开包含你的 SQL 项目的解决方案。

    2. 在解决方案资源管理器中右键单击项目,并选择“发布…”选项。

    3. 设置要发布到的服务器和数据库的名称。

    4. 从对话框底部的选项中选择“生成脚本”。 此操作将创建可用于部署的脚本。 我们可以检查此脚本,以验证是否已添加语句 IF ,以使脚本可重启。

    5. 查看生成的部署脚本。 在标记为“预先部署脚本模板”的节的前面,您将看到类似于下列 Transact-SQL 语法的内容:

      :setvar CompletedBatches __completedBatches_CompareProjectDB_cd1e348a-8f92-44e0-9a96-d25d65900fca
      :setvar TotalBatchCount 17
      GO
      
      if OBJECT_ID(N'tempdb.dbo.$(CompletedBatches)', N'U') is null
      BEGIN
      USE tempdb;
      CREATE TABLE [dbo].[$(CompletedBatches)]
      (
      BatchId INT PRIMARY KEY,
      Description NVARCHAR(300)
      );
      USE [$(DatabaseName)];
      END
      

      稍后在部署脚本中,您会看到在每个批处理周围都有一个 IF 语句包围着原始语句。 例如,以下 T-SQL 脚本可能针对语句 CREATE SCHEMA 显示:

      IF NOT EXISTS (SELECT 1
                     FROM [tempdb].[dbo].[$(CompletedBatches)]
                     WHERE [BatchId] = 0)
          BEGIN
              EXECUTE sp_executesql @stmt = N'CREATE SCHEMA [Sales] AUTHORIZATION [dbo]';
              SET NOCOUNT ON;
              INSERT [tempdb].[dbo].[$(CompletedBatches)] (BatchId, Description)
              VALUES (0, N'CreateElementStep Sales batch 0');
              SET NOCOUNT OFF;
          END
      

      CREATE SCHEMA是必须包含在IF语句中的EXECUTEsp_executesql语句之一。 语句(如 CREATE TABLE 不需要 EXECUTEsp_executesql 语句),类似于以下示例:

      IF NOT EXISTS (SELECT 1
                     FROM [tempdb].[dbo].[$(CompletedBatches)]
                     WHERE [BatchId] = 1)
          BEGIN
              CREATE TABLE [Sales].[Customer]
              (
                  [CustomerID] INT IDENTITY (1, 1) NOT NULL,
                  [CustomerName] NVARCHAR (40) NOT NULL,
                  [YTDOrders] INT NOT NULL,
                  [YTDSales] INT NOT NULL
              );
              SET NOCOUNT ON;
              INSERT [tempdb].[dbo].[$(CompletedBatches)] (BatchId, Description)
              VALUES (1, N'CreateElementStep Sales.Customer batch 0');
              SET NOCOUNT OFF;
          END
      

      注意

      如果部署与目标数据库相同的数据库项目,则生成的报表并不十分有意义。 若要获得更有用的结果,请将更改部署到数据库或部署新的数据库。

使用生成的 dacpac 文件进行的命令行部署

SQL 项目生成的输出项目是一个 .dacpac 文件。 .dacpac文件可用于从命令行部署架构,并且可以从其他计算机(例如生成计算机)启用部署。 SqlPackage 是一个具有各种选项的命令行实用工具,可让用户部署 dacpac 或生成部署脚本以及执行其他操作。 有关详细信息,请参阅 SqlPackage

注意

若要使用定义的 DeploymentContributors 属性成功部署从项目生成的 dacpac,必须在正在使用的计算机上安装包含部署参与者的 DLL。 这是因为这些 DLL 已标记为成功完成部署所需的项。

若要避免此要求,请从 .sqlproj 文件中排除部署参与者。 相反,请将 SqlPackage 与 AdditionalDeploymentContributors 参数结合使用来指定参与者在部署期间运行。 这在您仅希望在特定情况下(如部署到特定服务器)使用参与者时会很有用。