사용자 지정 마이그레이션 작업

MigrationBuilder API를 사용하면 마이그레이션 중에 다양한 종류의 작업을 수행할 수 있지만 이것이 전부는 아닙니다. 그러나 API는 확장 가능하므로 사용자 고유의 작업을 정의할 수 있습니다. API를 확장하는 방법에는 Sql() 메서드를 사용하거나 사용자 지정 MigrationOperation 개체를 정의하는 두 가지 방법이 있습니다.

설명을 위해 각 접근 방식을 사용하여 데이터베이스 사용자를 만드는 작업을 구현해 보겠습니다. 마이그레이션에서 다음 코드 작성을 사용하려고 합니다.

migrationBuilder.CreateUser("SQLUser1", "Password");

MigrationBuilder.Sql() 사용

사용자 지정 작업을 구현하는 가장 쉬운 방법은 MigrationBuilder.Sql()를 호출하는 확장 메서드를 정의하는 것입니다. 다음은 적절한 Transact-SQL을 생성하는 예제입니다.

public static OperationBuilder<SqlOperation> CreateUser(
    this MigrationBuilder migrationBuilder,
    string name,
    string password)
    => migrationBuilder.Sql($"CREATE USER {name} WITH PASSWORD '{password}';");

SQL 배치의 첫 번째 문이거나 유일한 문이어야 하는 경우 EXEC 함수를 사용합니다. 참조된 열이 현재 테이블에 없을 때 발생할 수 있는 idempotent 마이그레이션 스크립트의 파서 오류를 해결해야 할 수도 있습니다.

마이그레이션에서 여러 데이터베이스 공급자를 지원해야 하는 경우 MigrationBuilder.ActiveProvider 속성을 사용할 수 있습니다. 다음은 Microsoft SQL Server 및 PostgreSQL을 모두 지원하는 예제입니다.

public static OperationBuilder<SqlOperation> CreateUser(
    this MigrationBuilder migrationBuilder,
    string name,
    string password)
{
    switch (migrationBuilder.ActiveProvider)
    {
        case "Npgsql.EntityFrameworkCore.PostgreSQL":
            return migrationBuilder
                .Sql($"CREATE USER {name} WITH PASSWORD '{password}';");

        case "Microsoft.EntityFrameworkCore.SqlServer":
            return migrationBuilder
                .Sql($"CREATE USER {name} WITH PASSWORD = '{password}';");
    }

    throw new Exception("Unexpected provider.");
}

이 방법은 사용자 지정 작업이 적용될 모든 공급자를 알고 있는 경우에만 작동합니다.

MigrationOperation 사용

SQL에서 사용자 지정 작업을 분리하려면 이를 나타내는 사용자 고유의 MigrationOperation을 정의하면 됩니다. 그런 다음, 생성할 적절한 SQL을 결정할 수 있도록 작업이 공급자에게 전달됩니다.

public class CreateUserOperation : MigrationOperation
{
    public string Name { get; set; }
    public string Password { get; set; }
}

이 접근 방식을 사용하면 확장 메서드는 이러한 작업 중 하나를 MigrationBuilder.Operations에 추가하기만 하면 됩니다.

public static OperationBuilder<CreateUserOperation> CreateUser(
    this MigrationBuilder migrationBuilder,
    string name,
    string password)
{
    var operation = new CreateUserOperation { Name = name, Password = password };
    migrationBuilder.Operations.Add(operation);

    return new OperationBuilder<CreateUserOperation>(operation);
}

이 접근 방식을 사용하려면 각 공급자가 IMigrationsSqlGenerator 서비스에서 이 작업에 대해 SQL을 생성하는 방법을 알아야 합니다. 다음은 새 작업을 처리하기 위해 SQL Server 생성기를 재정의하는 예제입니다.

public class MyMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public MyMigrationsSqlGenerator(
        MigrationsSqlGeneratorDependencies dependencies,
        ICommandBatchPreparer commandBatchPreparer)
        : base(dependencies, commandBatchPreparer)
    {
    }

    protected override void Generate(
        MigrationOperation operation,
        IModel model,
        MigrationCommandListBuilder builder)
    {
        if (operation is CreateUserOperation createUserOperation)
        {
            Generate(createUserOperation, builder);
        }
        else
        {
            base.Generate(operation, model, builder);
        }
    }

    private void Generate(
        CreateUserOperation operation,
        MigrationCommandListBuilder builder)
    {
        var sqlHelper = Dependencies.SqlGenerationHelper;
        var stringMapping = Dependencies.TypeMappingSource.FindMapping(typeof(string));

        builder
            .Append("CREATE USER ")
            .Append(sqlHelper.DelimitIdentifier(operation.Name))
            .Append(" WITH PASSWORD = ")
            .Append(stringMapping.GenerateSqlLiteral(operation.Password))
            .AppendLine(sqlHelper.StatementTerminator)
            .EndCommand();
    }
}

기본 마이그레이션 SQL 생성기 서비스를 업데이트된 서비스로 바꿉니다.

protected override void OnConfiguring(DbContextOptionsBuilder options)
    => options
        .UseSqlServer(_connectionString)
        .ReplaceService<IMigrationsSqlGenerator, MyMigrationsSqlGenerator>();