我在项目中使用实体框架 5,并且启用了迁移。
场景如下:
新的开发人员(dev1)加入并从源代码构建项目。由于以前的开发人员过去一直致力于该项目,因此存在现有的迁移。
当开发人员第一次运行 ASP.NET MVC 项目时,数据库会自动构建,并且不会出现错误。
然而,在那之后,另一个开发人员(dev2)添加了新的迁移。当 Dev1 尝试运行
Update-Database
时,将尝试运行所有 之前的 迁移。但它们已经被应用,因为它们是 Dev1 所看到的初始模型的一部分。这通常会导致架构错误,因为它试图应用已经存在的架构更改。
因此,最佳情况下,将本地数据库“快进”到当前迁移会很棒。但我不知道有什么方法可以做到这一点。或者,是否有其他方法来初始化数据库,以便我可以在初始化期间应用所有迁移?
我想出了一个窍门。
奔跑
Update-Database -Script
选择所有已经运行的迁移
INSERT INTO [__MigrationHistory] ([MigrationId], [Model], [ProductVersion]) VALUES
打开 Sql Server Management Studio,然后手动运行这些 sql 语句。
新的迁移应该可以正常工作。
团队环境中的实体框架迁移可能很棘手。尤其是与源代码控制相结合时。听起来您遇到了针对不同开发数据库运行不同代码优先迁移的问题。
针对特定数据库运行的所有迁移都存储在 __MigrationHistory 表中。如果您的项目中有迁移文件,并且 EF 在 __MigrationHistory 表中没有看到它,它将在您运行 Update-Database 时尝试执行它。如果您通过将记录插入 MigrationHistory 来欺骗 EF 认为它已经应用了这些迁移文件,那么它将首先破坏 EF 代码的最佳功能之一。您将无法使用 update-database -TargetMigration 来支持您的迁移。
如果每个开发人员都有自己的一组迁移,并且它们可能有重叠的更改,则在运行 update-database 时可能会导致各种 sql 错误。
我的解决方案是忽略与主动开发有关的所有迁移文件(因为它们往往会被大量调整)。当开发人员获得新的更改集时,他们只需创建自己的本地迁移。
一旦我准备好向生产环境发布一项新功能,我就会将所有这些更改整合到一个大迁移中,并且通常以我将应用程序增量到的版本号来命名它。我将这些迁移文件检查到源代码管理中。这些充当我的主迁移文件,使任何新数据库与生产保持同步。
当我进行新的主迁移时,所有开发人员都会将其本地更改恢复到上一个主要版本,删除他们不再需要的那些(因为它们包含在主迁移中),然后运行 update-database。
我采用了 Doug 的技术并创建了一个可自动执行该操作的 DatabaseInitializer。
只需使用
Database.SetInitializer(new CreateDbWithMigrationHistoryIfNotExists<EntityContext, Configuration>());
在您的 DbContext 中,如果不存在,它将创建一个新数据库,并更新迁移历史记录表以包含所有现有迁移。
这是课程:
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");
}
}
}
根据 Doug 的回答,如果您使用 ef core 版本
Update-Database -Script
将会抛出 A parameter cannot be found that matches parameter name 'Script'.
。
但是.net core cli中有很好的替代品(VS工具的替代品):
dotnet ef migrations script --idempotent
如果您现在有 dotnet ef cli,请先通过以下方式安装它们:
dotnet tool install --global dotnet-ef
然后您可以将生成的sql复制并直接选择您需要的内容到数据库。
EF核心解决方案:
public static void MarkCompleted(string migrationName)
{
var versionStr = GetEfVersion();
var ctx = new DbContextCreator(); // your ctx
var existing = ctx.Database.GetAppliedMigrations();
if (! existing.Contains(migrationName))
{
FormattableString sql = @$"INSERT INTO [dbo].[__EFMigrationsHistory]
([MigrationId] ,[ProductVersion])
VALUES ({migrationName},{versionStr})";
ctx.Database.ExecuteSql(sql);
}
}
public static string GetEfVersion()
{
// The EF Core version, as obtained from the
// AssemblyInformationalVersionAttribute of the EF Core assembly.
var asm = typeof(DbContext).Assembly;
var attr = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!;
var versionStr = attr.InformationalVersion;
return versionStr;
}