다음을 통해 공유


연습: 데이터베이스 프로젝트 배포를 확장하여 배포 계획 수정

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 will always be 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 will be enclosed in an if statement that will check 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 will contain 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++;  
    
    

    이 코드는 BEGIN/END 블록이 포함된 IF 문을 만듭니다. 그런 다음 배치의 문에 대해 추가 처리를 수행합니다. 완료되면 INSERT 문을 추가하여 스크립트 실행 진행률을 추적하는 임시 테이블에 정보를 추가합니다. 마지막으로, 배치를 업데이트하고, 그 안에 해당 문이 포함된 새 IF로 기존에 존재했던 문을 바꿔서 업데이트합니다.키 형식, 메서드 및 속성에는 IfStatement, BeginEndBlockStatement, StatementList, TSqlBatch, PredicateSetStatement, SetOptions, 및 InsertStatement 등이 포함됩니다.

  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. 마지막으로 "Add additional post-processing here" 주석 위치에 사후 처리 섹션을 추가합니다.

    // Declare a SqlCmd variables.  
    //  
    // CompletedBatches variable - defines the name of the table in tempdb that will track  
    // 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 will use to track the work that we are doing  
    DeploymentScriptStep createStatusTableStep = new DeploymentScriptStep(CreateCompletedBatchesTable);  
    base.AddBefore(context.PlanHandle, beforePreDeploy, createStatusTableStep);  
    
    

    처리에서 조건문으로 둘러싸는 하나 이상의 단계를 찾은 경우 SQLCMD 변수를 정의하기 위해 배포 스크립트에 문을 추가해야 합니다. 변수는 다음과 같습니다.

    • CompletedBatches에는 스크립트가 실행할 때 성공적으로 완료된 일괄 처리를 추적하기 위해 배포 스크립트에서 사용되는 임시 테이블의 고유 이름이 포함됩니다.
    • TotalBatchCount에는 배포 스크립트의 총 배치 수가 포함됩니다.

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

    StringBuilder, DeploymentScriptStep 및 AddBefore.

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

도우미 메서드를 정의하려면

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

    메서드 설명
    CreateExecuteSQL 제공된 문을 EXEC sp_executesql 문으로 둘러싸도록 CreateExecuteSQL 메서드를 정의합니다. 키 형식, 메서드 및 속성에는 다음 DacFx API 구성 요소가 포함됩니다. ExecuteStatement, ExecutableProcedureReference, SchemaObjectName, ProcedureReferenceExecuteParameter.
    CreateCompletedBatchesName CreateCompletedBatchesName 메서드를 정의합니다. 이 메서드는 일괄 처리에 대 한 임시 테이블에 삽입 되는 이름을 만듭니다. 키 형식, 메서드 및 속성에는 다음과 같은 DacFx API 구성 요소인 SchemaObjectName이 포함됩니다.
    IsStatementEscaped IsStatementEscaped 메서드를 정의합니다. 이 메서드는 모델 요소의 형식을 IF 문 내에서 묶기 전에 EXEC sp_executesql 문에 문을 래핑해야 하는지 여부를 결정합니다. 키 형식, 메서드 및 속성에는 다음 DacFx API 구성 요소가 포함됩니다. 다음 모델 형식에 대한TSqlObject.ObjectType, ModelTypeClass, 및 the 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, AlterElementStep, 및 DropElementStep.
    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 will be 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 determins 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 will look 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. 다음으로 어셈블리 정보를 확장 디렉터리에 복사합니다. Visual Studio 2022는 시작 시 %Program Files%\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\SQLDB\DAC 디렉터리 및 하위 디렉터리에 있는 모든 확장을 확인하고 이를 사용할 수 있도록 합니다.

  2. 출력 디렉터리에서 %Program Files%\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\SQLDB\DAC 디렉터리로 MyOtherDeploymentContributor.dll 어셈블리 파일을 복사합니다. 기본적으로 컴파일된 .dll 파일의 경로는 YourSolutionPath\YourProjectPath\bin\Debug 또는 YourSolutionPath\YourProjectPath\bin\Release입니다.

배포 기여자 실행 또는 테스트

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

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

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

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

실행하려는 기여자 ID를 지정하려면 항상 SQL 프로젝트 파일을 업데이트해야 합니다. 다음 두 가지 방법 중 하나로 SQL 프로젝트를 업데이트할 수 있습니다.

  1. .sqlproj 파일을 수동으로 수정해서 필요한 인수를 추가할 수 있습니다. 기여자가 구성에 필요한 검토 인수를 포함하지 않는 경우 또는 대량의 프로젝트에서 빌드 기여자를 다시 사용하지 않으려는 경우에는 이 작업을 선택해서 수행할 수 있습니다. 이 옵션을 선택할 경우, .sqlproj 파일에서 파일의 첫 번째 가져오기 노드 다음에 다음 문을 추가합니다.

    <PropertyGroup>  
      <DeploymentContributors>  
        $(DeploymentContributors); MyOtherDeploymentContributor.RestartableScriptContributor  
      </DeploymentContributors>  
    </PropertyGroup>  
    
  2. 두 번째 방법은 필요한 기여자 인수를 포함하는 대상 파일을 만드는 것입니다. 이는 기본값을 포함하므로 여러 프로젝트에 동일한 기여자를 사용하고 필요한 기여자 인수를 갖고 있는 경우에 유용합니다. 이 경우 MSBuild 확장 경로에 대상 파일을 만듭니다.

    1. %Program Files%\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” /> 노드 뒤에 다음 문을 추가하여 대상 파일을 가져옵니다.

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

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

참고 항목

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

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

SQL 프로젝트를 배포하고 배포 보고서를 생성하려면

  • 프로젝트는 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 SCHEMA는 IF 문 내에서 EXECUTE sp_executesql 문 내에 묶어야 하는 문 중 하나입니다. CREATE TABLE과 같은 문에는 EXECUTE sp_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 프로젝트 빌드의 ouput 아티팩트는 dacpac 파일입니다. dacpac 파일을 사용하여 명령줄에서 스키마를 배포할 수 있으며 빌드 머신 같은 다른 컴퓨터에서 배포를 사용하도록 설정할 수 있습니다. SqlPackage는 사용자가 dacpac을 배포하거나 배포 스크립트를 생성할 수 있는 다양한 옵션을 제공하는 명령줄 유틸리티입니다. 자세한 내용은 SqlPackage.exe를 참조하세요.

참고 항목

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

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

다음 단계

배포 계획이 실행되기 전에 다른 유형의 수정 사항을 실험해 볼 수 있습니다. 수행할 수 있는 다른 유형의 수정은 다음과 같습니다.

  • 버전 번호를 연결하는 모든 데이터베이스 개체에 확장 속성을 추가합니다.

  • 배포 스크립트에서 추가 진단 인쇄 문 또는 주석 추가 또는 제거

참고 항목

빌드 및 배포 기여자를 사용하여 데이터베이스 빌드 및 배포 사용자 지정
연습: 데이터베이스 프로젝트 빌드를 확장하여 모델 통계 생성
연습: 데이터베이스 프로젝트 배포를 확장하여 배포 계획 분석