实体框架 - 迁移 - 代码优先 - 每个迁移的种子数据

45

我正在研究迁移以清理我们的部署过程。在向生产环境推送更改时,需要尽量减少手动干预。

我遇到了三个主要问题,如果找不到一个干净的解决方法,它们将成为障碍。

1. 如何在每个迁移中添加种子数据:

我执行“add-migration”命令,这会生成一个新的迁移文件,并具有Up和Down函数。现在,我想通过Up和Down更改自动更改数据。我不想将种子数据添加到Configuration.Seed方法中,因为这将运行所有迁移,导致各种重复问题。

2. 如果上述方法不可行,如何避免重复?

我有一个枚举,我循环遍历以将值添加到数据库中。

foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
    context.Access.AddOrUpdate(
        new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
    );
}
context.SaveChanges();

尽管我使用了AddOrUpdate,但我仍然在数据库中获得重复项。上面的代码提出了我的第三个问题:

3. 如何种子化主键?

上述代码中的可枚举对象为:

public class Access
{
    public enum Level
    {
        None = 10,
        Read = 20,
        ReadWrite = 30
    }
    public int AccessId { get; set; }
    public string Name { get; set; }
}

我正在指定我想要的主键值,但Entity Framework似乎忽略了它。 它们最终仍然是1,2,3。 我该如何让它变成10,20,30?

这些是EF目前存在的限制吗?还是有意为之的限制,以防止我没有看到的其他灾难发生?

4个回答

29
  1. 当我有要插入的固定数据时,我会直接在Up()迁移中使用Sql("Insert ...")进行插入。请参见此页面中间的注释:如何插入固定数据
  2. 通过调用AddOrUpdate重载方法并指定自然键的标识表达式来防止种子方法中的重复数据 - 请参见这个答案这篇博客文章
  3. 默认情况下,整数类型的主键将作为自增字段创建。若要指定其他方式,请使用[DatabaseGenerated(DatabaseGeneratedOption.None)]属性

我认为这是关于初始化器和种子方法的一个很好的解释:Initializer和Seed方法

以下是使用AddOrUpdate方法的示例:

foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
    context.Access.AddOrUpdate(
        x => x.Name, //the natural key is "Name"
        new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
    );
}

1
如果您想使用SQL方法,但在编写所有查询时遇到了一些字符串转义或原样字符串的问题,那么您也可以使用SqlResource()方法。请参见http://www.jasoncavett.com/blog/managing-more-complicated-entity-framework-migrations/。也许在您的答案中提到这一点会很有趣? - JBert

16
作为解决第一项问题的可能方案,我实现了IDatabaseInitializer策略,它将仅运行每个待处理迁移的Seed方法,您需要在每个DbMigration类中实现自定义的IMigrationSeed接口,然后Seed方法将紧随每个迁移类的UpDown方法之后实现。
这有助于解决我的两个问题:
1. 将数据库模型迁移与数据库数据迁移(或种子数据)分组。 2. 检查应该真正运行哪部分Seed迁移代码,而不是检查数据库中的数据,而是使用已知的数据即刚刚创建的数据库模型。
该接口如下:
public interface IMigrationSeed<TContext>
{
    void Seed(TContext context);
}

以下是新的实现,将调用此Seed方法。
public class CheckAndMigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration>
    : IDatabaseInitializer<TContext>
    where TContext : DbContext
    where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>, new()
{
    public virtual void InitializeDatabase(TContext context)
    {
        var migratorBase = ((MigratorBase)new DbMigrator(Activator.CreateInstance<TMigrationsConfiguration>()));

        var pendingMigrations = migratorBase.GetPendingMigrations().ToArray();
        if (pendingMigrations.Any()) // Is there anything to migrate?
        {
            // Applying all migrations
            migratorBase.Update();
            // Here all migrations are applied

            foreach (var pendingMigration in pendingMigrations)
            {
                var migrationName = pendingMigration.Substring(pendingMigration.IndexOf('_') + 1);
                var t = typeof(TMigrationsConfiguration).Assembly.GetType(
                    typeof(TMigrationsConfiguration).Namespace + "." + migrationName);

                if (t != null 
                   && t.GetInterfaces().Any(x => x.IsGenericType 
                      && x.GetGenericTypeDefinition() == typeof(IMigrationSeed<>)))
                {
                    // Apply migration seed
                    var seedMigration = (IMigrationSeed<TContext>)Activator.CreateInstance(t);
                    seedMigration.Seed(context);
                    context.SaveChanges();
                }
            }
        }
    }
}

这里的好处是您有一个真正的EF上下文来操作种子数据,就像标准的EF种子实现一样。但是,如果例如您决定删除先前迁移中已经种植的表,则必须相应地重构现有的种子代码,否则可能会出现问题。
编辑: 作为在Up和Down之后实现种子方法的替代方案,您可以创建同一迁移类的部分类。我发现这很有用,因为它允许我在想要重新种植相同迁移时安全地删除迁移类。

2
这太厉害了!你需要更多的积分来证明。我所做的唯一更改是在Update周围加上try/finally,以便如果一个迁移失败,种子仍然可以继续。此外,在Update之后,调用GetDatabaseTransaction()并与pending进行比较,以便只有成功的迁移才会种子。还将Seed调用包装在自己的事务中(再次,以防万一失败)。 - Joshua
我曾经对这个答案非常热情,但它有严重的缺点: 1)每个迁移的种子方法都没有事务,并且Up和Seed方法之间也没有耦合(稍后运行)。一旦Up方法成功,您只有一次机会让Seed方法工作。 2)当只能调用一次种子方法时,很难测试种子方法,大多数时候您正在处理更新后的数据库。 尝试再次运行“Update-Database”将不再在“pendingMigrations”列表中包含您的迁移,因此种子方法将永远不会被调用... - JBert
  1. 因为 Seed 方法不会再次调用,所以当数据库发生变化时,您可能会忘记更新它们。我有一个例子,其中一个这些 Seed 方法将插入一个默认用户。在某个时候,数据库被更改为要求填写所有用户详细信息(即实体属性不可为空),但 Seed 方法没有初始化这些属性。 最终结果:现有的安装程序将正常工作,因为 Seed 方法在过去被正确调用,新的安装程序尝试插入无法存储在当前数据库模型中的实体。
- JBert
@JBert关于1,你看到Joshua在他的评论中做出的改变了吗?他改进了异常/事务处理。关于2,我真的不明白你希望做什么。这个策略的目的是仅在运行Migration时执行Seed,当然,您只需要迁移数据库一次,因此Seed将仅在每次迁移时执行一次,这是所需的行为(从而避免重复数据插入等)。如果您想测试种子,请撤消迁移,然后再次迁移(使用-TargetMigration选项)。感谢您的评论。 - Guillermo Ruffino
“仅运行一次”的行为对于“顺畅流程”来说是可以的,但我想提醒大家,当这种解决方案失败时,没有内置的重试机制,并且部署到生产环境时,可能无法使用--TargetMigration。Up()方法不同:它可能会失败多次。因为它在事务中运行,所以会回滚任何失败的更改并停止下一个迁移的运行,以便运行Update-Database(或软件中的Initializer)并修复问题。Joshua的修复程序无法解决这个问题,因此我现在更喜欢在Up()方法中使用SQL进行种子播种。 - JBert
显示剩余3条评论

3

你好,我在这个链接中找到了一些与你的问题相关的非常有用的信息: Safari Books Online

"1. 如何在每次迁移中添加种子数据:" 如你所见,在示例中,您需要为种子数据创建一个新配置。 此种子配置必须在迁移之后调用。

public sealed class Configuration : DbMigrationsConfiguration
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(SafariCodeFirst.SeminarContext context)
    {
        //  This method will be called after migrating to the latest version.

        //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
        //  to avoid creating duplicate seed data. E.g.
        //
        //    context.People.AddOrUpdate(
        //      p => p.FullName,
        //      new Person { FullName = "Andrew Peters" },
        //      new Person { FullName = "Brice Lambson" },
        //      new Person { FullName = "Rowan Miller" }
        //    );
        //
    }
}

"2. 如果不可能实现上述方法,我该如何避免重复?"

AddOrUpdate 可以帮助您避免重复,如果您在这里遇到错误,可能是配置错误,请发布调用堆栈。请参阅示例!

"3. 如何种子主键?"

这也取决于您的键定义。如果您的键DatabaseGenerated(DatabaseGeneratedOption.Identity),则无需提供它。在其他一些情况下,您需要创建一个新的主键,具体取决于键类型。

"这些是EF目前的限制还是有意限制以防止我看不到的某些灾难?"
据我所知,不是这样的!


2
我已经在Seed方法中获取了我的数据。但是即使我使用AddOrUpdate,它仍然会添加重复项。问题在于当我使用“add-migration”时,它不会创建自己的configuration.seed。因此,无论您执行哪个迁移,它仍会执行常见的Seed方法,这不是我想要的。我希望每个迁移文件都有一个单独的Seed方法。 - Talon
看,我有一个类似的问题。在 DbMigrationsConfiguration 构造函数中,你必须设置 MigrationsNamespace,例如:this.MigrationsNamespace = "DataAccessLayer.Repository.Migrations"; 然后在所需的迁移文件中,你必须根据 DbMigrationsConfiguration 修改命名空间。这个技巧是我自己经过长时间的奋斗发现的,现在 Entity Framework 只会进入所需的迁移文件。我希望这能解决你的问题2。 - Bassam Alugili
我认为归档迁移在最终阶段仍处于初级阶段,需要一些时间来发展。我已经添加了我最终所做的事情,听起来你创建了一个全新的迁移文件夹,并且每个文件夹只有一个迁移文件。有一天我会尝试它,但现在我已经浪费了太多时间,需要赶快进行。感谢你的帮助! - Talon
1
AddOrUpdate方法的第一个参数是用于防止重复项的。在上面的示例中,如果存在匹配的“FullName”,则不会更新。因此,如果您正在获取重复项,请检查该参数。 - Greg Gum

3

好的,经过一番努力,我已经成功地将EF压服了。

1. 我没有找到任何方法来查看特定迁移的数据。所有数据都必须放入通用的Configuration.Seed方法中。

2. 为了避免重复,我必须做两件事。 对于我的枚举,我编写了以下种子代码:

foreach (var enumValue in Enum.GetValues(typeof(Access.Level)))
{
    var id = (int)enumValue;
    var val = enumValue.ToString();

    if(!context.Access.Any(e => e.AccessId == id))
        context.Access.Add(
            new Access { AccessId = id, Name = val }
        );
}
context.SaveChanges();

基本上,只需检查是否存在并在不存在时添加。
3.为了使上述操作正常运行,您需要能够插入主键值。幸运的是,这个表将始终具有相同的静态数据,因此我可以停用自动增量。要做到这一点,代码如下所示:
public class Access
{
    public enum Level
    {
        None = 10,
        Read = 20,
        ReadWrite = 30
    }

    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int AccessId { get; set; }
    public string Name { get; set; }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接