实体框架 - 迁移 - 代码优先 - 每次迁移播种

Posted

技术标签:

【中文标题】实体框架 - 迁移 - 代码优先 - 每次迁移播种【英文标题】:Entity Framework - Migrations - Code First - Seeding per Migration 【发布时间】:2013-09-16 13:43:20 【问题描述】:

我正在研究迁移以清理我们的部署流程。将更改推送到生产环境时所需的人工干预越少越好。

我在迁移系统中遇到了 3 个主要问题。如果我想不出一个干净的方式绕过它们,它们就会成为阻碍者。

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 的这些限制,还是故意限制以防止我没有看到的其他类型的灾难?

【问题讨论】:

【参考方案1】:

您好,我在此链接中为您的问题找到了非常有用的信息: 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 的这些限制,还是故意限制以防止我没有看到的其他类型的灾难?” 我不知道!

【讨论】:

我已经通过种子方法获得了我的数据。但即使我使用 AddOrUpdate,它也会不断添加重复项。问题是当我使用“添加迁移”时,它不会创建自己的 configuration.seed。因此,无论您执行哪种迁移,它仍然执行不是我想要的常见 Seed 方法。我希望每个迁移文件有一个单独的种子方法。 看我也有类似的问题。我在 DbMigrationsConfiguration 构造函数中做了什么;您必须设置 MigrationsNamespace 例如 this.MigrationsNamespace = "DataAccessLayer.Repository.Migrations";并且在希望的迁移文件中,您必须根据 DbMigrationsConfiguration 修改命名空间。经过长时间的斗争,我自己创建了这个技巧,现在实体框架只会出现在希望的迁移文件中。我希望这能解决您的问题 2. 我认为最终,迁移仍处于婴儿阶段,需要一些时间来发展。我已经添加了我最终做的事情,听起来你创建了一个全新的迁移文件夹,每个文件夹都有一个迁移文件。有一天我会尝试,但现在我已经浪费了太多时间,需要快点。感谢您的帮助! AddOrUpdate 方法的第一个参数是为了防止重复。在上面的示例中,如果存在匹配的现有“FullName”,则它不会更新。因此,如果您得到重复,请检查该参数。【参考方案2】:

好的,所以我有点抨击我已经设法将 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; 

【讨论】:

【参考方案3】:
    当我有想要通过迁移插入的固定数据时,我使用对 Sql("Insert ...") 的调用将插入直接放入 Up() 迁移中。请参阅本页中间的注释:how to insert fixed data 您可以通过调用 AddOrUpdate 重载来防止 Seed 方法中的重复,该重载采用指定自然键的标识符表达式 - 请参阅 this answer 和 this blog entry。 默认情况下,整数主键被创建为标识字段。要以其他方式指定,请使用[DatabaseGenerated(DatabaseGeneratedOption.None)] 属性

我认为这是对Initializer and Seed methods的一个很好的解释

这是一个如何使用 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() 
    );

【讨论】:

如果你想走 SQL 路线,但在编写所有带有字符串转义或逐字字符串的查询时遇到了一些麻烦,那么你也可以使用SqlResource() 方法。见jasoncavett.com/blog/…也许在你的回答中提到这很有趣?【参考方案4】:

作为第 1 项的可能解决方案,我实现了 IDatabaseInitializer 策略,它将仅运行每个待定迁移的 Seed 方法,您需要在每个 @987654323 中实现自定义 IMigrationSeed 接口@classes,Seed 方法将在每个迁移类的 UpDown 方法之后立即实现。

这有助于为我解决两个问题:

    使用数据库数据迁移(或种子)对数据库模型迁移进行分组 检查种子迁移代码的哪一部分应该真正运行,而不是检查数据库中的数据,而是使用刚刚创建的数据库模型的已知数据。

界面是这样的

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 之后实现种子方法的替代方法,您可以创建同一 Migration 类的部分类,我发现这很有用,因为它允许我在想重新播种相同的迁移时安全地删除迁移类.

【讨论】:

这是天才!!!为此,您需要更多积分。我所做的唯一更改是尝试/最终围绕更新,以便在一次迁移失败时种子将继续。同样在更新之后,调用 GetDatabaseTransaction() 并与挂起进行比较,这样只有成功的迁移才会种子。还将 Seed 调用包装在它自己的事务中(再次,以防万一失败。) 哇,伙计!我整天都在寻找在启用迁移时正确完成的数据库播种,最终发现了这一点。 我曾经非常热衷于这个答案,但它有严重的缺点:1)每个迁移的播种方法没有事务,并且 Up 和 Seed 方法之间也没有耦合(稍后运行)。一旦 Up 方法成功,您就只有一次机会让 Seed 方法工作。 2) Seed 方法很难测试,因为它只能被调用一次,大多数时候您正在处理更新的数据库。尝试再次运行 Update-Database 将不会再将您的迁移移至 pendingMigrations 列表中,因此将永远不会再次调用种子方法.... ... 3) 因为永远不会再次调用 Seed 方法,所以您可能会在数据库更改时忘记更新它们。我有一个示例,其中这些 Seed 方法之一将插入默认用户。在某些时候,数据库更改为要求填写所有用户详细信息(即实体属性不可为空),但 Seed 方法并未初始化这些属性。最终结果:现有安装可以工作,因为过去调用了正确的 Seed 方法,新安装尝试插入无法存储在当前数据库模型中的实体。 @JBert 关于 1,您在他的评论中看到 Joshua 的变化了吗?他改进了异常/事务处理,关于 2 我真的不明白你期望做什么。此策略目标是仅在运行迁移时运行 Seed,当然您只迁移数据库一次,因此 Seed 每次迁移仅执行一次,这是所需的行为(从而避免重复数据插入等)。如果您想测试种子,我建议您撤消迁移,然后再次迁移(使用 --TargetMigration 选项)感谢您的 cmets。

以上是关于实体框架 - 迁移 - 代码优先 - 每次迁移播种的主要内容,如果未能解决你的问题,请参考以下文章

实体框架代码优先 - 初始代码迁移不起作用

独立于模式的实体框架代码优先迁移

实体框架代码优先迁移 - 我可以针对以前的迁移

调试代码优先的实体框架迁移代码

实体框架代码优先:启用迁移错误

如何为实体框架代码优先迁移设置隔离级别