다음을 통해 공유


따라하기 안내: 데이터베이스 프로젝트의 배포를 확장하고 배포 계획을 수정하는 방법

SQL 프로젝트를 배포할 때 사용자 지정 작업을 수행하는 배포 기여자를 만들 수 있습니다. 배포 계획 수정자(DeploymentPlanModifier) 또는 배포 계획 실행자(DeploymentPlanExecutor)를 생성할 수 있습니다. 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 메서드를 재정의합니다.
  • 비공개 도우미 메서드를 추가합니다.
  • 결과 어셈블리를 빌드합니다.

클래스 라이브러리 프로젝트 만들기

  1. MyOtherDeploymentContributor라는 이름의 C# 클래스 라이브러리(.NET Framework) 프로젝트를 만듭니다.
  2. 파일 이름을 "Class1.cs"에서 "SqlRestartableScriptContributor.cs"로 바꿉니다.
  3. 솔루션 탐색기에서 프로젝트 노드를 마우스 오른쪽 단추로 클릭한 다음 참조 추가를 선택합니다.
  4. [프레임워크] 탭에서 System.ComponentModel.Composition을 선택합니다.
  5. 프로젝트 메뉴에서 NuGet 패키지 관리 옵션을 선택합니다. Microsoft.SqlServer.DacFx에 대한 안정적인 최신 릴리스를 설치합니다.

다음으로 클래스에 코드를 추가하기 시작합니다.

SqlRestartableScriptContributor 클래스 정의

  1. 코드 편집기에서 다음 using 문과 일치하도록 class1.cs 파일을 업데이트합니다.

    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
    }
    

    OnExecute 메서드를 기본 클래스인 DeploymentPlanContributor에서 재정의합니다. DeploymentPlanContributorDeploymentPlanModifierDeploymentPlanExecutor 모두에 대한 기본 클래스입니다. DeploymentPlanContributorContext 개체는 OnExecute 메서드에 전달되어 지정된 인수, 대상의 원본 및 데이터베이스 모델, 배포 계획 및 배포 옵션에 대한 액세스를 제공합니다. 이 예제에서는 배포 계획 및 대상 데이터베이스 이름을 가져옵니다.

  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 라이브러리 의 다음 구성 요소가 포함됩니다. BeginPreDeploymentScriptStep, BeginPostDeploymentScriptStep, TSqlObject, TSqlScript, Script, DeploymentScriptDomStep, 및 SqlPrintStep.

  4. 이제 "Add batch processing here"라는 주석을 일괄 처리 코드로 교체하세요.

        // 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로 문장을 바꿔서 배치를 업데이트하세요. 키 형식, 메서드 및 속성에는 IfStatement, BeginEndBlockStatement, StatementList, TSqlBatch, PredicateSetStatement, SetOptionsInsertStatement가 포함됩니다.

  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 에는 배포 스크립트의 총 일괄 처리 수가 포함됩니다.

    기타 관심 형식, 속성 및 메서드는 다음과 같습니다.

    StringBuilder, DeploymentScriptStepAddBefore.

    다음으로 이 메서드에서 호출하는 도우미 메서드를 정의합니다.

도우미 메서드 추가

  • 여러 도우미 메서드를 정의해야 합니다. 중요한 방법에는 다음이 포함됩니다.

    메서드 Description
    CreateExecuteSQL CreateExecuteSQL 메서드를 정의하여 제공된 문을 EXECsp_executesql 문으로 둘러싸도록 합니다. 키 형식, 메서드 및 속성에는 다음 DacFx API 구성 요소가 포함됩니다. ExecuteStatement, ExecutableProcedureReference, SchemaObjectName, ProcedureReferenceExecuteParameter.
    CreateCompletedBatchesName CreateCompletedBatchesName 메서드를 정의합니다. 이 메서드는 일괄 처리에 대 한 임시 테이블에 삽입 되는 이름을 만듭니다. 키 형식, 메서드 및 속성에는 다음과 같은 DacFx API 구성 요소 인 SchemaObjectName이 포함됩니다.
    IsStatementEscaped IsStatementEscaped 메서드를 정의합니다. 이 메서드는 모델 요소 형식이 문을 IF 문에 포함시키기 전에 EXECsp_executesql 문으로 래핑해야 하는지 여부를 결정합니다. 키 형식, 메서드 및 속성에는 다음과 같은 DacFx API 구성 요소가 포함됩니다: TSqlObject.ObjectType, ModelTypeClass, 및 TypeClass 특성. 모델 유형에는 Schema, Procedure, View, TableValuedFunction, ScalarFunction, DatabaseDdlTrigger, DmlTrigger, ServerDdlTrigger가 있습니다.
    CreateBatchCompleteInsert CreateBatchCompleteInsert 메서드를 정의합니다. 이 메서드는 INSERT 스크립트 실행 진행률을 추적하기 위해 배포 스크립트에 추가되는 문을 만듭니다. 키 형식, 메서드 및 속성에는 InsertStatement, NamedTableReference, ColumnReferenceExpression, ValuesInsertSource 및 RowValue와 같은 DacFx API 구성 요소가 포함됩니다.
    CreateIfNotExecutedStatement CreateIfNotExecutedStatement 메서드를 정의합니다. 이 메서드는 IF 임시 일괄 처리 실행 테이블이 이 일괄 처리가 이미 실행되었음을 나타내는지 확인하는 문을 생성합니다. 키 형식, 메서드 및 속성에는 IfStatement, ExistsPredicate, ScalarSubquery, NamedTableReference, WhereClause, ColumnReferenceExpression, IntegerLiteral, BooleanComparisonExpression 및 BooleanNotExpression이 포함됩니다.
    GetStepInfo GetStepInfo 메서드를 정의합니다. 이 메서드는 단계 이름 외에도 단계의 스크립트를 만드는 데 사용되는 모델 요소에 대한 정보를 추출합니다. 관심 있는 유형 및 메서드는 DeploymentPlanContributorContext, DeploymentScriptDomStep, TSqlObject, CreateElementStep, AlterElementStepDropElementStep입니다.
    GetElementName TSqlObject의 서식 지정된 이름을 생성합니다.
  1. 다음 코드를 추가하여 도우미 메서드를 정의합니다.

    /// <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\Debug or YourSolutionPath\YourProjectPath\bin\Release.

배포 구성 요소를 실행하거나 테스트하십시오.

배포 기여자를 실행 또는 테스트하려면 다음 작업을 수행해야 합니다.

  • 빌드하려는 .sqlproj 파일에 속성을 추가합니다.

  • MSBuild를 사용하고 적절한 매개 변수를 제공하여 데이터베이스 프로젝트를 배포합니다.

SQL 프로젝트(.sqlproj) 파일에 속성 추가

실행하려는 기여자 ID를 지정하려면 항상 SQL 프로젝트 파일을 업데이트해야 합니다. 다음 두 가지 방법 중 하나로 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 참가자를 실행하려는 프로젝트의 파일 내에서 파일의 Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" /.sqlproj 노드 다음에 <다음 문을 > 추가하여 대상 파일을 가져옵니다.

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

이러한 방법 중 하나를 수행한 후에는 MSBuild를 사용하여 명령줄 빌드에 대한 매개 변수를 전달할 수 있습니다.

참고

기여자 ID를 지정하려면 항상 "DeploymentContributors" 속성을 업데이트해야 합니다. 이 ID는 기여자 원본 파일의 "ExportDeploymentPlanModifier" 특성에 사용된 것과 동일한 ID입니다. 이 작업이 없으면 프로젝트를 빌드할 때 기여자가 실행되지 않습니다. "ContributorArguments" 속성은 실행할 기여자에 필요한 인수가 있을 경우에만 업데이트해야 합니다.

데이터베이스 프로젝트 배포

  • 프로젝트는 Visual Studio 내에서 일반적으로 게시 및 배포할 수 있습니다. SQL 프로젝트가 포함된 솔루션을 열고, 프로젝트의 상황에 맞는 메뉴에서 마우스 오른쪽 버튼을 클릭하여 게시... 옵션을 선택하거나, LocalDB로 디버그 배포하려면 F5 키를 사용합니다. 이 예에서는 “게시...” 대화 상자를 사용하여 배포 스크립트를 생성합니다.

    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 문이 보입니다. 예를 들어 CREATE SCHEMA 문에 다음과 같은 T-SQL 스크립트가 나타날 수 있습니다.

      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 SCHEMAIF 문 내에서 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 속성이 정의된 프로젝트에서 빌드된 dacpacs를 성공적으로 배포하려면 배포 참가자가 포함된 DLL을 사용 중인 컴퓨터에 설치해야 합니다. 배포를 성공적으로 완료하기 위해서는 필수로 표시되어 있기 때문입니다.

이 요구 사항을 방지하려면 파일에서 배포 참가자를 제외합니다 .sqlproj . 대신 AdditionalDeploymentContributors 매개 변수와 함께 SqlPackage를 사용하여 배포하는 동안 실행할 기여자를 지정합니다. 이는 특정 서버에 배포하는 것과 같은 특별한 상황에서만 기여자를 사용하려는 경우에 유용합니다.