Как добавить новую миграцию кода с помощью только что созданной базы данных?

Я включил миграцию с кодом в своем проекте Entity Framework и добавил несколько миграций, которые выполняют такие действия, как переименование таблиц. Однако теперь я удалил базу данных и заставил инфраструктуру сущностей создать совершенно новую базу данных на основе моей последней модели данных. Если я попытаюсь запустить:

PM> Add-Migration TestMigration

... он говорит мне, что мне нужно сначала применить существующие миграции. Итак, я бегу:

PM> Update-Database

... но проблема в том, что он пытается обновить базу данных, которая не нуждается в обновлении; он уже основан на последней модели данных. Поэтому я получаю сообщение об ошибке, когда он пытается переименовать таблицу, которой сейчас не существует.

Можно ли каким-то образом указать миграции данных, что моя база данных обновлена ​​и не требует каких-либо миграций? Что я должен делать?


comment
Вы пробовали Update-Database -Force? (это всего лишь дикая догадка)   -  person Jcl    schedule 27.10.2012
comment
@Jcl не имеет значения.   -  person Jez    schedule 28.10.2012


Ответы (3)


Я нашел способ указать, что моя база данных обновлена, и это (что неудивительно) основано на изменении таблицы __MigrationHistory, которую миграция кода использует для определения того, какие миграции применять к БД при запуске Update-Database.

Кстати, во время исследования этого ответа я наткнулся на очень хороший справочник по командам миграции кода, который можно найти по адресу: http://dotnet.dzone.com/articles/ef-migrations-command.

Когда база данных автоматически создается с нуля с помощью EF, она всегда будет помещать одну запись в таблицу __MigrationHistory, и эта запись будет иметь MigrationId (currentDateTime)_InitialCreate. Это представляет собой начальное создание базы данных, которое только что выполнил EF. Однако ваша история миграции не будет начинаться с этого MigrationId, потому что вы начали с чего-то другого.

Чтобы «обмануть» миграцию с первым кодом, заставив ее думать, что вы находитесь в последней миграции, вам нужно удалить эту запись (currentDateTime)_InitialCreate из таблицы __MigrationHistory вновь созданной БД и вставить то, что было бы там, если бы у вас все еще был старый БД, к которой были применены миграции.

Итак, сначала удалите все из недавно созданной таблицы __MigrationHistory базы данных. Затем перейдите в консоль диспетчера пакетов и запустите:

PM> Update-Database -Script

Из получившегося SQL-скрипта вытащите все строки, начинающиеся с:

INSERT INTO [__MigrationHistory]...

Затем запустите эти операторы INSERT в контексте только что созданной базы данных. Убедитесь, что каждая из этих строк теперь существует в вашей таблице __MigrationHistory. При следующем запуске:

PM> Update-Database

... вы должны получить сообщение «Нет ожидающих миграций на основе кода». Поздравляем — вы обманули миграции кода, заставив их думать, что вы сейчас находитесь на последней миграции, и вы можете продолжить с того места, на котором остановились, добавляя новые миграции отсюда.

Я действительно думаю, что должен быть какой-то автоматизированный способ сделать это, встроенный в код EF, хотя... может быть, они должны добавить что-то вроде:

PM> Update-Database -MigrationsTableOnly

... в результате чего он уничтожит существующие записи в таблице миграций и просто вставит новые записи в историю миграций для каждой миграции, определенной в вашем проекте, но на самом деле не попытается запустить миграцию. Ах хорошо.

ОБНОВЛЕНИЕ
Я нашел способ хорошо автоматизировать это, используя метод Seed пользовательского инициализатора. По сути, метод Seed удаляет существующие данные истории миграции при создании БД и вставляет вашу историю миграции. В моем конструкторе контекста базы данных я регистрирую пользовательский инициализатор следующим образом:

public class MyDatabaseContext : DbContext {
    public MyDatabaseContext() : base() {
        Database.SetInitializer(new MyDatabaseContextMigrationHistoryInitializer());
    }

Сам пользовательский инициализатор выглядит так:

/// <summary>
/// This initializer clears the __MigrationHistory table contents created by EF code-first when it first
/// generates the database, and inserts all the migration history entries for migrations that have been
/// created in this project, indicating to EF code-first data migrations that the database is
/// "up-to-date" and that no migrations need to be run when "Update-Database" is run, because we're
/// already at the latest schema by virtue of the fact that the database has just been created from
/// scratch using the latest schema.
/// 
/// The Seed method needs to be updated each time a new migration is added with "Add-Migration".  In
/// the package manager console, run "Update-Database -Script", and in the SQL script which is generated,
/// find the INSERT statement that inserts the row for that new migration into the __MigrationHistory
/// table.  Add that INSERT statement as a new "ExecuteSqlCommand" to the end of the Seed method.
/// </summary>
public class MyDatabaseContextMigrationHistoryInitializer : CreateDatabaseIfNotExists<MyDatabaseContext> {
    /// <summary>
    /// Sets up this context's migration history with the entries for all migrations that have been created in this project.
    /// </summary>
    /// <param name="context">The context of the database in which the seed code is to be executed.</param>
    protected override void Seed(MyDatabaseContext context) {
        // Delete existing content from migration history table, and insert our project's migrations
        context.Database.ExecuteSqlCommand("DELETE FROM __MigrationHistory");
        context.Database.ExecuteSqlCommand("INSERT INTO __MigrationHistory (MigrationId, Model, ProductVersion) VALUES ('201210091606260_InitialCreate', 0x1F8B0800000000000400ECBD07601C499625262F6DCA7B7F4AF54AD7E074A..., '5.0.0.net40')");
        context.Database.ExecuteSqlCommand("INSERT INTO __MigrationHistory (MigrationId, Model, ProductVersion) VALUES ('201210102218467_MakeConceptUserIdNullable', 0x1F8B0800000000000400ECBD07601C499625262F6DCA7B7F4..., '5.0.0.net40')");
        context.Database.ExecuteSqlCommand("INSERT INTO __MigrationHistory (MigrationId, Model, ProductVersion) VALUES ('201210231418163_ChangeDateTimesToDateTimeOffsets', 0x1F8B0800000000000400ECBD07601C499625262F6D..., '5.0.0.net40')");
        context.Database.ExecuteSqlCommand("INSERT INTO __MigrationHistory (MigrationId, Model, ProductVersion) VALUES ('201210251833252_AddConfigSettings', 0x1F8B0800000000000400ECBD07601C499625262F6DCA7B7F4AF54AD7E..., '5.0.0.net40')");
        context.Database.ExecuteSqlCommand("INSERT INTO __MigrationHistory (MigrationId, Model, ProductVersion) VALUES ('201210260822485_RenamingOfSomeEntities', 0x1F8B0800000000000400ECBD07601C499625262F6DCA7B7F4AF5..., '5.0.0.net40')");
    }
}
person Jez    schedule 28.10.2012
comment
Спасибо за это. На самом деле это немного дерьмо - должна быть лучшая история для создания чистой базы данных на основе последней схемы. - person Isaac Abraham; 08.02.2013
comment
Я просто хотел добавить вычисляемый столбец... Может быть, ADO.NET проще, чем Entity Framework, в конце концов... viva new SqlCommand() - person Mzn; 04.03.2014

Перейдите к таблице MigrationHistory на сервере sql (в папке system), в ней есть строка для вашей миграции и хэш базы данных, который должен быть таким же, как и в вашем файле миграции, просто скопируйте его из базы данных в файл.

Другими словами, вам нужно синхронизировать таблицу MigrationHistory с фактическими миграциями.

person Anri    schedule 28.10.2012

Эта реализация не требует ручного управления записями, которые будут вставлены в __MigrationHistory. Миграции определяются из указанной сборки.

Может быть, это поможет.

Я благодарю @Jez за первоначальную идею.

/// <summary>
/// An implementation of IDatabaseInitializer that will:
/// 1. recreate database only if the database does not exist 
/// 2. actualize __MigrationHistory to match current model state (i.e. latest migration)
/// </summary>
/// <typeparam name="TContext">The type of the context.</typeparam>
public class CreateDatabaseIfNotExistsAndMigrateToLatest<TContext> : CreateDatabaseIfNotExists<TContext>
    where TContext : DbContext
{
    private readonly Assembly migrationsAssembly;

    /// <summary>
    /// Gets the migration metadata for types retrieved from <paramref name="assembly"/>. Types must implement <see cref="IMigrationMetadata"/>.
    /// </summary>
    /// <param name="assembly">The assembly.</param>
    /// <returns></returns>
    private static IEnumerable<IMigrationMetadata> GetMigrationMetadata(Assembly assembly)
    {
        var types = assembly.GetTypes().Where(t => typeof(IMigrationMetadata).IsAssignableFrom(t));
        var migrationMetadata = new List<IMigrationMetadata>();
        foreach (var type in types)
        {
            migrationMetadata.Add(
                (IMigrationMetadata)Activator.CreateInstance(type));
        }

        return migrationMetadata.OrderBy(m => m.Id);
    }

    /// <summary>
    /// Gets the provider manifest token.
    /// </summary>
    /// <param name="db">The db.</param>
    /// <returns></returns>
    private static string GetProviderManifestToken(TContext db)
    {
        var connection = db.Database.Connection;
        var token = DbProviderServices.GetProviderServices(connection).GetProviderManifestToken(connection);

        return token;
    }

    /// <summary>
    /// Gets the migration SQL generator. Currently it is <see cref="SqlServerMigrationSqlGenerator"/>.
    /// </summary>
    /// <returns></returns>
    private static MigrationSqlGenerator GetMigrationSqlGenerator()
    {
        return new SqlServerMigrationSqlGenerator();
    }

    /// <summary>
    /// Creates the operation for inserting into migration history. Operation is created for one <paramref name="migrationMetadatum"/>.
    /// </summary>
    /// <param name="migrationMetadatum">The migration metadatum.</param>
    /// <returns></returns>
    private static InsertHistoryOperation CreateInsertHistoryOperation(IMigrationMetadata migrationMetadatum)
    {
        var model = Convert.FromBase64String(migrationMetadatum.Target);

        var op = new InsertHistoryOperation(
            "__MigrationHistory",
            migrationMetadatum.Id,
            model,
            null);

        return op;
    }

    /// <summary>
    /// Generates the SQL statements for inserting migration into history table.
    /// </summary>
    /// <param name="generator">The generator.</param>
    /// <param name="op">The operation.</param>
    /// <param name="token">The token.</param>
    /// <returns></returns>
    private static IEnumerable<MigrationStatement> GenerateInsertHistoryStatements(
        MigrationSqlGenerator generator,
        InsertHistoryOperation op,
        string token)
    {
        return generator.Generate(new[] { op }, token);
    }

    /// <summary>
    /// Runs the SQL statements on database specified by <paramref name="db"/> (<see cref="DbContext.Database"/>).
    /// </summary>
    /// <param name="statements">The statements.</param>
    /// <param name="db">The db.</param>
    private static void RunSqlStatements(IEnumerable<MigrationStatement> statements, TContext db)
    {
        foreach (var statement in statements)
        {
            db.Database.ExecuteSqlCommand(statement.Sql);
        }
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="CreateDatabaseIfNotExistsAndMigrateToLatest{TContext}"/> class.
    /// </summary>
    /// <param name="migrationsAssembly">The migrations assembly.</param>
    public CreateDatabaseIfNotExistsAndMigrateToLatest(Assembly migrationsAssembly)
    {
        this.migrationsAssembly = migrationsAssembly;
    }

    protected override void Seed(TContext context)
    {
        base.Seed(context);

        // Get migration metadata for migrationAssembly
        var migrationMetadata = GetMigrationMetadata(migrationsAssembly);

        // Crate DbContext
        var db = Activator.CreateInstance<TContext>();
        // Remove newly created record in __MigrationHistory
        db.Database.ExecuteSqlCommand("DELETE FROM __MigrationHistory");

        // Get provider manifest token
        var token = GetProviderManifestToken(db);
        // Get sql generator
        var generator = GetMigrationSqlGenerator();

        foreach (var migrationMetadatum in migrationMetadata)
        {
            // Create history operation
            var op = CreateInsertHistoryOperation(migrationMetadatum);
            // Generate history insert statements
            var statements = GenerateInsertHistoryStatements(generator, op, token);
            // Run statements (SQL) over database (db)
            RunSqlStatements(statements, db);
        }
    }
}
person Michal Brndiar    schedule 24.07.2013
comment
Я столкнулся с этой проблемой с EF6. Я попытался реализовать это решение, но оказалось, что InsertHistoryOperation больше не доступна. Есть ли другой способ сгенерировать операцию для вставки истории без InsertHistoryOperation? - person goodface87; 12.12.2014
comment
Кто-нибудь знает, как этот подход можно использовать в EF6? похоже, они изменили классы HistoryOperation/MigrationOperation таким образом, чтобы остановить подобную ручную вставку. - person WillEllis; 24.09.2015