How do I force Entity Framework to mark a particul

2019-03-13 05:16发布

I'm using Entity Framework 5 in a project, and I've got Migrations enabled.

Here's the scenario:

A new developer (dev1) comes on and builds the project from source. There are existing migrations since previous developers had been working on the project in the past.

When that developer runs the ASP.NET MVC project for the first time, the database builds automatically, and no errors appear.

After that, however, a different developer (dev2) adds a new migration. When Dev1 tries to run Update-Database, all of the previous migrations are attempted to run. But they have already been applied, since they were part of the initial model as Dev1 saw it. This often results in a schema error since it's trying to apply a schema change that already exists.

So, optimally, it would be great to just "fast forward" the local database to the current migration. But I don't know of a way to do that. Alternatively, is there some other way to initialize the database so that I can apply all the migrations during initialization?

3条回答
不美不萌又怎样
2楼-- · 2019-03-13 05:35

I took Doug's technique and created a DatabaseInitializer that automates it.

Just use

 Database.SetInitializer(new CreateDbWithMigrationHistoryIfNotExists<EntityContext, Configuration>());

In your DbContext and it will create a new DB if one does not exist and update the migration history table to include all existing migrations.

Here is the class:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.Data.Entity.Migrations.Model;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml.Linq;

namespace EfStuff
{
    public class CreateDbWithMigrationHistoryIfNotExists<TContext, TMigrationsConfiguration> :
        IDatabaseInitializer<TContext>
        where TContext : DbContext
        where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>
    {
        private readonly Regex _pattern = new Regex("ProviderManifestToken=\"([^\"]*)\"");
        private readonly TMigrationsConfiguration _config;

        public CreateDbWithMigrationHistoryIfNotExists()
        {
            _config = Activator.CreateInstance<TMigrationsConfiguration>();
        }

        public void InitializeDatabase(TContext context)
        {
            if (context.Database.Exists()) return;
            context.Database.Create();

            var operations = GetInsertHistoryOperations();
            if (!operations.Any()) return;
            var providerManifestToken = GetProviderManifestToken(operations.First().Model);
            var sqlGenerator = _config.GetSqlGenerator(GetProviderInvariantName(context.Database.Connection));
            var statements = sqlGenerator.Generate(operations, providerManifestToken);
            statements.ToList().ForEach(x => context.Database.ExecuteSqlCommand(x.Sql));
        }

        private IList<InsertHistoryOperation> GetInsertHistoryOperations()
        {
            return
                _config.MigrationsAssembly.GetTypes()
                       .Where(x => typeof (DbMigration).IsAssignableFrom(x))
                       .Select(migration => (IMigrationMetadata) Activator.CreateInstance(migration))
                       .Select(metadata => new InsertHistoryOperation("__MigrationHistory", metadata.Id,
                                                                      Convert.FromBase64String(metadata.Target)))
                       .ToList();
        }

        private string GetProviderManifestToken(byte[] model)
        {
            var targetDoc = Decompress(model);
            return _pattern.Match(targetDoc.ToString()).Groups[1].Value;
        }

        private static XDocument Decompress(byte[] bytes)
        {
            using (var memoryStream = new MemoryStream(bytes))
            {
                using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
                {
                    return XDocument.Load(gzipStream);
                }
            }
        }

        private static string GetProviderInvariantName(DbConnection connection)
        {
            var type = DbProviderServices.GetProviderFactory(connection).GetType();
            var assemblyName = new AssemblyName(type.Assembly.FullName);
            foreach (DataRow providerRow in (InternalDataCollectionBase) DbProviderFactories.GetFactoryClasses().Rows)
            {
                var typeName = (string) providerRow[3];
                var rowProviderFactoryAssemblyName = (AssemblyName) null;
                Type.GetType(typeName, (a =>
                                            {
                                                rowProviderFactoryAssemblyName = a;
                                                return (Assembly) null;
                                            }), ((_, __, ___) => (Type) null));
                if (rowProviderFactoryAssemblyName != null)
                {
                    if (string.Equals(assemblyName.Name, rowProviderFactoryAssemblyName.Name,
                                      StringComparison.OrdinalIgnoreCase))
                    {
                        if (DbProviderFactories.GetFactory(providerRow).GetType().Equals(type))
                            return (string) providerRow[2];
                    }
                }
            }
            throw new Exception("couldn't get the provider invariant name");
        }
    }
}
查看更多
等我变得足够好
3楼-- · 2019-03-13 05:43

I figured out a hack.

Run Update-Database -Script

Pick out all the migrations that have already been run

INSERT INTO [__MigrationHistory] ([MigrationId], [Model], [ProductVersion]) VALUES

Open Sql Server Management Studio, and run those sql statements manually.

New migrations should work fine.

查看更多
来,给爷笑一个
4楼-- · 2019-03-13 05:55

Entity Framework migrations in a team environment can be tricky. Especially when combined with source control. It sounds like you are running into issues with different code-first migrations being run against different dev databases.

All the migrations that have been run against a particular database are stored in the __MigrationHistory table. If you have a migration file in your project and EF doesn't see it in the __MigrationHistory table it's going to try and execute it when you run Update-Database. If you trick EF into thinking that it's already applied these migration files by inserting the records into MigrationHistory it will break one of the nicest features of EF code first. You won't be able to role back your migrations using update-database -TargetMigration.

If each dev has their own set of migrations and it's possible for them to have overlapping changes this can result in all sorts of sql errors when you run update-database.

My solution to this has been to ignore all migration files having to do with active development (since they tend to be tweeked a lot). When a dev gets a new change set they just create their own local migrations.

Once I am ready to release a new feature to production I roll all those changes into one big migration and I usually name it after the version number that I am incrementing my app to. I check these migration files into source control. These act as my master migration files to bring any new database up to date with production.

When I make a new one of these master migrations all devs role back their local changes to the last major version, delete the ones they don't need anymore (because they are covered in master migration), then run update-database.

查看更多
登录 后发表回答