Existe uma boa maneira de estender as Migrações de Primeiro Código?

9

Estou iniciando um novo projeto que usa o Entity Framework. Eu pesquisei minhas opções de como criar o banco de dados e descobri que as Migrações de Primeiro Código fazem mais sentido (veja a parte inferior se você precisa saber por quê). O Code-First Migrations me permite baixar para um significado arbitrário de SQL, eu ainda tenho controle total. Na prática, descobri que o problema é que a queda para o SQL parece terrivelmente repetitiva para algumas das tarefas comuns.

Para os meus propósitos, não me importo que as extensões na migração sejam agnósticas do provedor (o SQL que eu estou alinhando não é). No entanto, eu não estou realmente encontrando um bom ponto de junção ou extensão na estrutura de migrações para adicionar tais coisas.

Para dar um exemplo concreto, suponha que eu queira especificar uma coluna RowGuid para a replicação MS-SQL. Cada ocorrência assume a forma de

Sql(
    string.Format(
        "Alter Table {0} Alter Column {1} Add ROWGUIDCOL",
        table,
        column ));

Então eu escrevo métodos estáticos para me livrar de parte da redundância

Sql( MigrationHelper.SetRowGuid( table, column );

-ou -

MigrationHelper.SetRowGuid(Sql, table, column); //passing the Sql method

Possivelmente poderia fazer um desses métodos de extensão no DbMigration e acessá-los por this. Mas ainda assim isso parece fora de lugar:

CreateTable(
    "dbo.CustomerDirectory",
     c => new
         {
             Uid = c.Int(nullable: false),
             CustomerUid = c.Int(nullable: false),
             Description = c.String(nullable: false, maxLength: 50, unicode: false),
             RowGuid = c.Guid(nullable: false),
         })
     .PrimaryKey(t => t.Uid)
     .ForeignKey("dbo.Customer", t => t.CustomerUid);

this.SetRowGuid( Sql, "dbo.CustomerDirectory", "RowGuid" );
//Custom method here because of desired naming convention of Constraint
this.SetDefaultConstraint( Sql, "dbo.CustomerDirectory", "''" ):

Não é terrivelmente, mas ainda parece um truque para mim. Eu tenho que repetir o nome da tabela, e eu preciso ter certeza de que o nome da coluna gerada está correto. Eu acho que o nome da tabela precisa ser repetido muito, mas também as colunas. No entanto, o que estou realmente tentando fazer é adicionar à declaração de tabela que aconteceu onde os nomes dos nomes das tabelas e das colunas eram conhecidos.

No entanto, eu não consegui encontrar um bom ponto de extensão para estender a interface fluente ou estender as primeiras migrações de código de uma maneira que pareça consistente. Estou esquecendo de algo? Alguém achou uma boa maneira de fazer isso?

Algumas justificativas do porquê estou nesta situação:

Eu não gostei do que parecia ser uma solução comum de usar a solução de atributos personalizados comuns para indicar banco de dados sem mapeamento por alguns motivos, mas mais fortemente porque eles não são automaticamente selecionados por migrações, o que significa manutenção extra. As primeiras soluções do modelo foram eliminadas porque não fornecem controle total sobre o banco de dados. Database-First foi atraente por causa do controle; no entanto, ele não possui a funcionalidade de gerenciamento de alterações out-of-the-box Code-First Migrações fornece. Assim, o Code-First Migrations parecia ser um vencedor porque as mudanças dirigidas pelo modelo são automáticas e significava que haveria apenas uma coisa a ser mantida.

    
por vossad01 14.09.2012 в 06:30
fonte

2 respostas

5

Encontrei uma solução, embora não tenha certeza se é boa. Eu tive que ir um pouco mais abaixo da toca do coelho do que eu queria, e não é realmente um ponto de extensão.

Ele me permite escrever declarações como:

CreateTable(
    "dbo.CustomerDirectory",
     c => new
        {
            Uid = c.Int(nullable: false),
            CustomerUid = c.Int(nullable: false),
            Description = c.String(nullable: false, maxLength: 50, unicode: false),
            RowGuid = c.Guid(nullable: false),
        })
    .PrimaryKey(t => t.Uid)
    .ForeignKey("dbo.Customer", t => t.CustomerUid)
      //SqlValue is a custom static helper class
    .DefaultConstraint( t => t.Description, SqlValue.EmptyString)
      //This is a convention in the project
      //Equivalent to
      //  .DefaultConstraint( t => t.RowGuid, SqlValue.EmptyString)
      //  .RowGuid( t => t.RowGuid )
    .StandardRowGuid()
      //For one-offs
    .Sql( tableName => string.Format( "ALTER TABLE {0} ...", tableName" );

Eu não gosto:

  • O fato de que estou refletindo sobre membros particulares e normalmente não usaria essa solução
  • O lambda para selecionar uma coluna poderia retornar o nome errado da coluna se o parâmetro opcional "name" da definição da coluna fosse usado.

Só estou pensando em usá-lo aqui porque:

  • Nós enviamos o assembly EF, então temos certeza de que o usado terá esses membros.
  • Alguns testes de unidade nos informarão se uma nova versão quebrará isso.
  • É isolado para migrações.
  • Temos todas as informações que estamos refletindo para receber, por isso, se uma nova versão quebrar isso, poderemos implementar um hack para substituir essa funcionalidade.
internal static class TableBuilderExtentions
{
    internal static TableBuilder<TColumns> Sql<TColumns>(
        this TableBuilder<TColumns> tableBuilder,
        Func<string, string> sql,
        bool suppressTransaction = false,
        object anonymousArguments = null)
    {
        string sqlStatement = sql(tableBuilder.GetTableName());

        DbMigration dbMigration = tableBuilder.GetDbMigration();
        Action<string, bool, object> executeSql = dbMigration.GetSqlMethod();

        executeSql(sqlStatement, suppressTransaction, anonymousArguments);

        return tableBuilder;
    }

    [Pure]
    private static DbMigration GetDbMigration<TColumns>(this TableBuilder<TColumns> tableBuilder)
    {
        var field = tableBuilder.GetType().GetField(
            "_migration", BindingFlags.NonPublic | BindingFlags.Instance);
        return (DbMigration)field.GetValue(tableBuilder);
    }

    /// <summary>
    ///   Caution: This implementation only works on single properties.
    ///   Also, coder may have specified the 'name' parameter which would make this invalid.
    /// </summary>
    private static string GetPropertyName<TColumns>(Expression<Func<TColumns, object>> someObject)
    {
        MemberExpression e = (MemberExpression)someObject.Body;

        return e.Member.Name;
    }

    [Pure]
    private static Action<string, bool, object> GetSqlMethod(this DbMigration migration)
    {
        MethodInfo methodInfo = typeof(DbMigration).GetMethod(
            "Sql", BindingFlags.NonPublic | BindingFlags.Instance);
        return (s, b, arg3) => methodInfo.Invoke(migration, new[] { s, b, arg3 });
    }

    [Pure]
    private static string GetTableName<TColumns>(this TableBuilder<TColumns> tableBuilder)
    {
        var field = tableBuilder.GetType().GetField(
            "_createTableOperation", BindingFlags.NonPublic | BindingFlags.Instance);

        var createTableOperation = (CreateTableOperation)field.GetValue(tableBuilder);
        return createTableOperation.Name;
    }
}
    
por vossad01 14.09.2012 / 15:23
fonte
0

Não é uma solução genérica, mas você pode herdar de uma classe abstrata / de interface. Dado isso precisaria de algumas alterações de código, mas é razoavelmente limpo.

Eu usei esse padrão para definir minhas colunas de auditoria (UpdatedBy, UpdateDate etc.) para todas as tabelas.

    
por ravi 14.09.2012 / 08:25
fonte