使桌面客户端无需更新即可使用较新的 EF 数据库迁移

Make desktop client work with newer EF db migration without updating

目前我有一个 ERP,它是一个基于 Winforms 的客户端(带有 SQL 服务器),它使用 ClickOnce 在桌面上交付和更新。

当前版本首先使用 Entity Framework 4(基于 ObjectContext)和数据库。当数据库模式发生变化时,我对客户端进行更新的方式是一个四步过程:

  1. 在生产中使用兼容的列创建一个中间更新的数据库模式(在任何地方都允许 null 或具有默认值等)。旧客户端可以连接到该数据库并继续工作,就好像什么都没有改变一样
  2. 将桌面客户端更新到具有更新功能的中间版本,该更新功能说明了该中间架构但具有所有 "final schema" 功能
  3. 更新所有客户端并且所有记录都与 "final" 架构兼容后,使用所需的数据库约束对架构进行新更新
  4. 将所有客户端更新到映射到此最终模式的最终版本(它解决了数据库约束错误,并且需要这些模式更改才能工作)。

我发现这个过程虽然对我们来说有点麻烦,但对客户来说更好,他们可以在他们认为合适的时候更新,并且不会在工作中途被更新打扰(这可能涉及到有不想等待软件更新的客户。

现在我几乎完全重写了客户端(仍然是 Winforms),使用 EF6 和代码优先,并进行了迁移。

我一直在搜索文档,但找不到任何东西(现在似乎只有网络编程,通常可以同时更新数据库和网络客户端,而不会打扰用户),但是一旦我在生产中应用迁移,未更新的客户端不能再使用数据库。如果实例化上下文与数据库模式不同步,EF 将在实例化上下文时抱怨并抛出异常。

具体问题:有没有一种方法可以让 EF6 代码优先 dbcontext 与更新的数据库模式迁移一起工作,而不是编译的,只要它兼容?如果是这样的话,我可以继续做我目前正在做的事情。

如果有人想扩展实际答案,还有一个(我猜)基于意见的问题:有没有更好的方法来处理这种情况?我敢肯定我不是唯一遇到此问题的人,但是 Google 文档所需的关键字太宽泛,到目前为止,我的搜索中只出现了网络场景。

我目前正处于允许进行重大更改的客户端重写阶段,所以我不关心解决方案是否会使部分代码复杂化

当应用程序通过直接调用 DbContext.Database.Initialize 或实例化第一个 DbContext 来初始化模型数据库时,它会检查应用程序中的模型和数据库中的模型是否匹配。

为此,它计算模型哈希,并将其与存储在 __MigrationHistory table(或 EdmMetadata table 中的哈希进行比较,如果它是从 EF 4.x) 更新的。这是在 System.Data.Entity.Internal.ModelCompatibilityChecker.CompatibleWithModel 方法中完成的,该方法接收一个名为 throwIfNoMetadata 的参数,在内部实现中恰好是 false,因此如果没有元数据则不会抛出异常。[=26] =]

因此,如果您在数据库初始化之前以某种方式让这个 tables 消失,就可以避免错误。重要的一点是您必须在不使用 DbContext 的情况下进行此更改。如果不存在,数据库将尝试初始化,如果 table 存在,它将失败。所以你可以使用普通 ADO.NET 来删除 tables.

考虑到元数据 table 可以自动创建,例如通过应用迁移。

您还可以使用ctx.Database.CompatibleWithModel(true)检查数据库元数据是否存在以及是否兼容,以摆脱它。参数就是我上面说的throwIfNoMetadata

数据库初始值设定项中的兼容性检查:

默认的 DB Initializer 是 CreateDatabaseIfNotExists,它会检查 throwIfNoMetadata 设置为 false 的模型兼容性。这就是此解决方案有效的原因。但是,如果您实现自己的 DB Initializer 版本,但不会 运行 检查,它应该可以工作。

public virtual void InitializeDatabase(TContext context)
{
    Check.NotNull(context, "context");

    var existence = new DatabaseTableChecker().AnyModelTableExists(context.InternalContext);

    if (existence == DatabaseExistenceState.Exists)
    {
        // If there is no metadata either in the model or in the database, then
        // we assume that the database matches the model because the common cases for
        // these scenarios are database/model first and/or an existing database.
        if (!context.Database.CompatibleWithModel(throwIfNoMetadata: false, existenceState: existence))
        {
            throw Error.DatabaseInitializationStrategy_ModelMismatch(context.GetType().Name);
        }
    }
    else
    {
        // Either the database doesn't exist, or exists and is considered empty
        context.Database.Create(existence);
        Seed(context);
        context.SaveChanges();
    }
}